Showing posts with label software development. Show all posts
Showing posts with label software development. Show all posts

2010-03-11

Compiling Java applications to native Windows executables

Being a Java developer really sucks when it comes to making end-user desktop applications. You want these applications to be light, fast and easily redistributable. Packaging a bunch of jars along with startup script is the worst thing you can do. It may work quite well in various Linux distributions, where you can make a .deb or .rpm which automatically installs JRE, puts startup script to /usr/local/bin and creates a nice launcher with icon in the applications menu. In Mac OS you can use that outdated JRE that comes by default and then easily bundle all scripts and jars into an .app. But in Windows it does not work that way. You can use JSmooth to make .exe out of .jar and use NSIS to create a nice installer, but there still is a problem. User may not have a compatible JRE, or he/she may choose not to have one just because Java sucks for running on it's bloated memory hogging virtual machine. I have to agree here. As a user, I have no faith in desktop applications that are written in Java.

So what is the solution? Compile to native executable and forget JRE. It is possible, but it will not be a walk in the park. First, you will have to get familiar with GNU Compiler Collection and particularly with GCJ.

Let's compile a Hello World application like this one:

public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello native world!");
    }
}
  1. Get the patched GCJ here: http://www.thisiscool.com/gcc_mingw.htm (120 MB)
  2. Extract it somewhere and add thisiscool-gcc/gcc-ejc/bin to your PATH
  3. Compile Hello.java as Hello.exe: gcj --main=Hello -o Hello Hello.java
  4. Enjoy your statically linked Hello.exe which prints Hello native world! and runs without JRE.
Wait. Hello.exe is 37MB? Holy crap! Well, it's a little overhead you have to pay for loosing the JRE, this .exe contains the whole garbage collection mechanism, etc. Also, you will be able to compile SWT GUI applications! It is possible to get smaller executables with MinGW tooling. Hello.java compiled with MinGW is about 3MB, but it has other issues, I couldn't manage to get the system output working. You may be more lucky though.

Anyway, when you compile native binaries, your Java code cannot be decompiled, so you have better protection than by using obfuscators. And the best thing is that end users won't complain that "it's crap because it's Java". They just wouldn't know.

I wish Sun (I just can't say Oracle when referring to Java, it makes me sick, sorry) could create an official AOT Java compiler so developers would not have to go through hellfire to make native executables.

2009-06-07

Cheatsheet: Unicode characters for buttons and GUI elements

Before drawing your own graphics for various GUI buttons, you could try finding a Unicode character that represents the thing you want to do. For instance, up/down arrows can be made with 25B2 (▲) and 25BC (▼).

Here are some pictures with Unicode characters that you can use to build GUIs. First a quick guide to using these:



Now, the cheatsheets:











These were captured from a tool named Korais.

You can download these images in a single PDF file: unicode.gui.cheatsheet.pdf

2009-02-13

Screw all GUI builders

Are you making your GUI with a builder? Do you like the generated code you get? I hate it. Even though I like the idea of building GUI with visual means (WYSIWYG), I can't stand the mess that code generators produce. In addition to that, there are more serious downsides:

  • You don't know how exactly the generated code works. You don't need to. You start not to care and GUI application development becomes a process of drawing and adding simple event handlers here and there.

  • Most GUI builders force you to use single class for single window, so generated classes tend to have thousands of lines of code.

  • Most GUI builders don't want you to modify the generated code. And if you do, they either break or rewrite your code.

  • GUI builders force you to use an IDE, mostly one you started coding with. So if you start with NetBeans, you most likely be forced to stay with it for the whole project.

  • The generated code is far from being optimal. It's not resize-friendly, not dynamic enough, it has many hard-coded values, refactoring is most likely impossible, because builder would not allow that.

So, why are you using GUI builders? Is it because you're doing GUI apps during your day job, you need fast results and you don't want to learn more than you have to? Or you just have no choice? That's reasonable, but when you have a choice, consider learning how Swing or SWT works, spend some time reading the API docs and examining the code - it's amazing how fast and dynamic your GUI building process can get when you finally get a clear understanding HOW to use all the widgets and layouts. Let me show you. Here's a window from Hawkscope app that I'm making in my spare time. It was generated with Jigloo GUI builder in Eclipse. First let's see how it looks:



The code (all comments removed):
package com.varaneckas.hawkscope.gui;

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Cursor;
import org.eclipse.swt.layout.FormAttachment;
import org.eclipse.swt.layout.FormData;
import org.eclipse.swt.layout.FormLayout;
import org.eclipse.swt.program.Program;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;

import com.cloudgarden.resource.SWTResourceManager;
import com.varaneckas.hawkscope.Version;
import com.varaneckas.hawkscope.cfg.ConfigurationFactory;
import com.varaneckas.hawkscope.util.IOUtils;
import com.varaneckas.hawkscope.util.IconFactory;
import com.varaneckas.hawkscope.util.OSUtils;

public class AboutWindow extends org.eclipse.swt.widgets.Dialog {

private Shell dialogShell;
private Canvas logoCanvas;
private Label appNameLabel;
private Label appSloganLabel;
private Label appVersion;
private Label appHomepageValue;
private Button copyReportButton;
private Button closeButton;
private Label environmentLabel;
private Text environmentTextArea;
private Label appHomepageLabel;
private Label appReleasedValue;
private Label appReleasedLabel;
private Label appVersionValue;

public AboutWindow(final Shell parent, final int style) {
super(parent, style);
}

public synchronized void open() {
if (dialogShell != null && !dialogShell.isDisposed()) {
dialogShell.setVisible(true);
dialogShell.forceFocus();
return;
}
final Shell parent = getParent();
dialogShell = new Shell(parent, SWT.DIALOG_TRIM
| SWT.APPLICATION_MODAL);
{
SWTResourceManager.registerResourceUser(dialogShell);
}
dialogShell.setImage(IconFactory.getInstance()
.getUncachedIcon("hawkscope16.png"));
dialogShell.setText("About");

dialogShell.setLayout(new FormLayout());
dialogShell.layout();
dialogShell.pack();
dialogShell.setSize(516, 322);
{
copyReportButton = new Button(dialogShell, SWT.PUSH | SWT.CENTER);
FormData copyReportButtonLData = new FormData();
copyReportButtonLData.width = 125;
copyReportButtonLData.height = 29;
copyReportButtonLData.left = new FormAttachment(0, 1000, 314);
copyReportButtonLData.top = new FormAttachment(0, 1000, 252);
copyReportButton.setLayoutData(copyReportButtonLData);
copyReportButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent event) {
IOUtils.copyToClipboard(Version.getEnvironmentReport());
}
});
copyReportButton.setText("Co&py to Clipboard");
OSUtils.adjustButton(copyReportButton);
}
{
closeButton = new Button(dialogShell, SWT.PUSH | SWT.CENTER);
FormData closeButtonLData = new FormData();
closeButtonLData.width = 47;
closeButtonLData.height = 29;
closeButtonLData.left = new FormAttachment(0, 1000, 451);
closeButtonLData.top = new FormAttachment(0, 1000, 252);
closeButton.setLayoutData(closeButtonLData);
closeButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent event) {
dialogShell.dispose();
}
});
closeButton.setText("&Close");
OSUtils.adjustButton(closeButton);
}
{
environmentLabel = new Label(dialogShell, SWT.NONE);
FormData environmentLabelLData = new FormData();
environmentLabelLData.width = 486;
environmentLabelLData.height = 17;
environmentLabelLData.left = new FormAttachment(0, 1000, 12);
environmentLabelLData.top = new FormAttachment(0, 1000, 127);
environmentLabel.setLayoutData(environmentLabelLData);
environmentLabel.setText("Environment");
environmentLabel.setFont(SWTResourceManager.getFont("Sans", 10, 1));
}
{
environmentTextArea = new Text(dialogShell, SWT.MULTI | SWT.WRAP
| SWT.V_SCROLL | SWT.BORDER);
FormData environmentTextAreaLData = new FormData();
environmentTextAreaLData.width = 468;
environmentTextAreaLData.height = 90;
environmentTextAreaLData.left = new FormAttachment(0, 1000, 12);
environmentTextAreaLData.top = new FormAttachment(0, 1000, 150);
environmentTextArea.setLayoutData(environmentTextAreaLData);
environmentTextArea.setText(Version.getSystemProperties());
environmentTextArea.setEditable(false);
}
{
appHomepageValue = new Label(dialogShell, SWT.NONE);
appHomepageValue.addMouseListener(new MouseAdapter() {
@Override
public void mouseUp(MouseEvent event) {
Program.launch(Version.HOMEPAGE);
}
});
appHomepageValue.setCursor(new Cursor(dialogShell.getDisplay(),
SWT.CURSOR_HAND));
appHomepageValue.setForeground(
new Color(dialogShell.getDisplay(), 0, 0, 255));
FormData appHomepageValueLData = new FormData();
appHomepageValueLData.width = 242;
appHomepageValueLData.height = 17;
appHomepageValueLData.left = new FormAttachment(0, 1000, 256);
appHomepageValueLData.top = new FormAttachment(0, 1000, 104);
appHomepageValue.setLayoutData(appHomepageValueLData);
appHomepageValue.setToolTipText("Click to open in browser");
appHomepageValue.setText(Version.HOMEPAGE);
}
{
appHomepageLabel = new Label(dialogShell, SWT.NONE);
FormData appHomepageLabelLData = new FormData();
appHomepageLabelLData.width = 94;
appHomepageLabelLData.height = 17;
appHomepageLabelLData.left = new FormAttachment(0, 1000, 156);
appHomepageLabelLData.top = new FormAttachment(0, 1000, 104);
appHomepageLabel.setLayoutData(appHomepageLabelLData);
appHomepageLabel.setText("Homepage:");
appHomepageLabel.setFont(SWTResourceManager.getFont("Sans", 10, 1));
}
{
appReleasedValue = new Label(dialogShell, SWT.NONE);
FormData appReleasedValueLData = new FormData();
appReleasedValueLData.width = 242;
appReleasedValueLData.height = 17;
appReleasedValueLData.left = new FormAttachment(0, 1000, 256);
appReleasedValueLData.top = new FormAttachment(0, 1000, 81);
appReleasedValue.setLayoutData(appReleasedValueLData);
appReleasedValue.setText(Version.VERSION_DATE);
}
{
appReleasedLabel = new Label(dialogShell, SWT.NONE);
FormData appReleasedLabelLData = new FormData();
appReleasedLabelLData.width = 77;
appReleasedLabelLData.height = 17;
appReleasedLabelLData.left = new FormAttachment(0, 1000, 156);
appReleasedLabelLData.top = new FormAttachment(0, 1000, 81);
appReleasedLabel.setLayoutData(appReleasedLabelLData);
appReleasedLabel.setText("Released:");
appReleasedLabel.setFont(SWTResourceManager.getFont("Sans", 10, 1));
}
{
appVersionValue = new Label(dialogShell, SWT.NONE);
FormData appVersionValueLData = new FormData();
appVersionValueLData.width = 242;
appVersionValueLData.height = 17;
appVersionValueLData.left = new FormAttachment(0, 1000, 256);
appVersionValueLData.top = new FormAttachment(0, 1000, 58);
appVersionValue.setLayoutData(appVersionValueLData);
if (Version.isUpdateAvailable() == null) {
appVersionValue.setText(Version.VERSION_NUMBER);
if (ConfigurationFactory.getConfigurationFactory()
.getConfiguration().checkForUpdates()) {
appVersionValue.setToolTipText("Could not get version information.");
}
} else {
if (Version.isUpdateAvailable()) {
appVersionValue.setForeground(new Color(dialogShell
.getDisplay(), 255, 0, 0));
appVersionValue.setText(Version.VERSION_NUMBER
+ " (Update Available!)");
appVersionValue.setToolTipText("Click to go to update " +
"download page");
appVersionValue.setCursor(new Cursor(dialogShell
.getDisplay(), SWT.CURSOR_HAND));
appVersionValue.addMouseListener(new MouseAdapter() {
@Override
public void mouseUp(MouseEvent event) {
Program.launch(Version.DOWNLOAD_URL);
dialogShell.dispose();
}
});
} else {
appVersionValue.setText(Version.VERSION_NUMBER);
appVersionValue.setToolTipText("Latest available version!");
appVersionValue.setForeground(new Color(dialogShell
.getDisplay(), 0, 128, 0));
}
}
}
{
appVersion = new Label(dialogShell, SWT.NONE);
FormData appVersionLData = new FormData();
appVersionLData.width = 77;
appVersionLData.height = 17;
appVersionLData.left = new FormAttachment(0, 1000, 156);
appVersionLData.top = new FormAttachment(0, 1000, 58);
appVersion.setLayoutData(appVersionLData);
appVersion.setText("Version:");
appVersion.setFont(SWTResourceManager.getFont("Sans", 10, 1));
}
{
appSloganLabel = new Label(dialogShell, SWT.WRAP);
FormData appSloganLabelLData = new FormData();
appSloganLabelLData.width = 342;
appSloganLabelLData.height = 17;
appSloganLabelLData.left = new FormAttachment(0, 1000, 156);
appSloganLabelLData.top = new FormAttachment(0, 1000, 35);
appSloganLabel.setLayoutData(appSloganLabelLData);
appSloganLabel.setText(Version.APP_SLOGAN);
}
{
appNameLabel = new Label(dialogShell, SWT.NONE);
FormData appNameLabelLData = new FormData();
appNameLabelLData.width = 342;
appNameLabelLData.height = 17;
appNameLabelLData.left = new FormAttachment(0, 1000, 156);
appNameLabelLData.top = new FormAttachment(0, 1000, 12);
appNameLabel.setLayoutData(appNameLabelLData);
appNameLabel.setText("Hawkscope");
appNameLabel.setFont(SWTResourceManager.getFont("Sans", 10, 1));
}
{
final FormData logoCanvasLData = new FormData();
logoCanvasLData.width = 114;
logoCanvasLData.height = 109;
logoCanvasLData.left = new FormAttachment(0, 1000, 12);
logoCanvasLData.top = new FormAttachment(0, 1000, 12);
logoCanvas = new Canvas(dialogShell, SWT.RESIZE);
logoCanvas.addPaintListener(new PaintListener() {
public void paintControl(PaintEvent e) {
e.gc.drawImage(IconFactory.getInstance()
.getUncachedIcon("hawkscope128.png"), 0, 0, 128,
128, 0, 0, 114, 109);
}
});
logoCanvas.addMouseListener(new MouseAdapter() {
@Override
public void mouseUp(MouseEvent event) {
Program.launch(Version.HOMEPAGE);
}
});
logoCanvas.setCursor(new Cursor(dialogShell.getDisplay(),
SWT.CURSOR_HAND));
logoCanvas.setToolTipText("Click to visit Homepage");
logoCanvas.setLayoutData(logoCanvasLData);
}
dialogShell.setLocation(getParent().toDisplay(100, 100));
dialogShell.open();
Display display = dialogShell.getDisplay();
while (!dialogShell.isDisposed()) {
if (!display.readAndDispatch())
display.sleep();
}
}

}


Now, a hand-rewritten version with no GUI builder:



The code:
package com.varaneckas.hawkscope.gui;

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Cursor;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.layout.FormAttachment;
import org.eclipse.swt.layout.FormData;
import org.eclipse.swt.layout.FormLayout;
import org.eclipse.swt.program.Program;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;

import com.varaneckas.hawkscope.Version;
import com.varaneckas.hawkscope.cfg.ConfigurationFactory;
import com.varaneckas.hawkscope.tray.TrayManager;
import com.varaneckas.hawkscope.util.IOUtils;
import com.varaneckas.hawkscope.util.IconFactory;

public class AboutShell {

private Shell shell;
private FormData layout;
private Font bold;
private Color red;
private Color green;
private Color blue;
private Cursor hand;
private Canvas logo;
private Label labelAppName;
private Label labelAppSlogan;
private Label labelVersion;
private Label labelReleased;
private Label labelHomePage;
private Label labelAppVersion;
private Label labelAppReleased;
private Label labelAppHomePage;
private Label labelEnvironment;
private Text textEnvironment;
private Button buttonCopyToClipboard;
private Button buttonClose;

public void open() {
if (shell != null && !shell.isDisposed()) {
shell.setVisible(true);
shell.forceFocus();
return;
}
createShell();
createResources();
createLogo();
createLabelAppName();
createLabelAppSlogan();
createLabelVersion();
createLabelReleased();
createLabelHomePage();
createLabelAppVersion();
createLabelAppReleased();
createLabelAppHomePage();
createLabelEnvironment();
createButtonClose();
createButtonCopyToClipboard();
createTextEnvironment();
shell.pack();
shell.open();
}

private void createResources() {
final FontData data = new FontData();
data.setHeight(10);
data.setStyle(SWT.BOLD);
bold = new Font(shell.getDisplay(), data);
red = new Color(shell.getDisplay(), 255, 0, 0);
green = new Color(shell.getDisplay(), 0, 128, 0);
blue = new Color(shell.getDisplay(), 0, 0, 255);
hand = new Cursor(shell.getDisplay(), SWT.CURSOR_HAND);
}

private void createShell() {
shell = new Shell(TrayManager.getInstance().getShell(), SWT.SHELL_TRIM);
final FormLayout layout = new FormLayout();
layout.spacing = 6;
layout.marginHeight = 12;
layout.marginWidth = 12;
shell.setLocation(shell.getParent().toDisplay(100, 100));
shell.setImage(IconFactory.getInstance()
.getUncachedIcon("hawkscope16.png"));
shell.setText("About");
shell.setLayout(layout);
shell.layout();
}

private FormData relativeTo(final Control top, final Control left) {
layout = new FormData();
layout.top = new FormAttachment(top);
layout.left = new FormAttachment(left);
return layout;
}

private FormData relativeToBottomRight(final Control right) {
layout = new FormData();
layout.bottom = new FormAttachment(100, 0);
if (right == null) {
layout.right = new FormAttachment(100, 0);
} else {
layout.right = new FormAttachment(right);
}
return layout;
}

private void createLogo() {
logo = new Canvas(shell, SWT.NONE);
logo.addPaintListener(new PaintListener() {
public void paintControl(PaintEvent e) {
e.gc.drawImage(IconFactory.getInstance()
.getUncachedIcon("hawkscope128.png"), 0, 0);
}
});
logo.addMouseListener(new MouseAdapter() {
@Override
public void mouseUp(MouseEvent event) {
Program.launch(Version.HOMEPAGE);
}
});
logo.setCursor(hand);
logo.setToolTipText("Click to visit Homepage");
layout = relativeTo(null, null);
layout.width = 128;
layout.height = 128;
logo.setLayoutData(layout);
}

private void createLabelAppName() {
labelAppName = new Label(shell, SWT.NONE);
labelAppName.setText(Version.APP_NAME);
labelAppName.setLayoutData(relativeTo(null, logo));
labelAppName.setFont(bold);
}

private void createLabelAppSlogan() {
labelAppSlogan = new Label(shell, SWT.NONE);
labelAppSlogan.setLayoutData(relativeTo(labelAppName, logo));
labelAppSlogan.setText(Version.APP_SLOGAN);
}

private void createLabelVersion() {
labelVersion = new Label(shell, SWT.NONE);
labelVersion.setText("Version:");
labelVersion.setFont(bold);
labelVersion.setLayoutData(relativeTo(labelAppSlogan, logo));
}

private void createLabelAppVersion() {
labelAppVersion = new Label(shell, SWT.NONE);
labelAppVersion.setText(Version.VERSION_NUMBER);
labelAppVersion.setLayoutData(relativeTo(labelAppSlogan, labelHomePage));
updateLabelAppVersion();
}

private void updateLabelAppVersion() {
if (Version.isUpdateAvailable() == null) {
if (ConfigurationFactory.getConfigurationFactory()
.getConfiguration().checkForUpdates()) {
labelAppVersion.setToolTipText("Could not get version information.");
}
} else {
if (Version.isUpdateAvailable()) {
labelAppVersion.setForeground(red);
labelAppVersion.setText(Version.VERSION_NUMBER
+ " (Update Available!)");
labelAppVersion.setToolTipText("Click to go to update " +
"download page");
labelAppVersion.setCursor(hand);
labelAppVersion.addMouseListener(new MouseAdapter() {
@Override
public void mouseUp(final MouseEvent event) {
Program.launch(Version.DOWNLOAD_URL);
shell.dispose();
}
});
} else {
labelAppVersion.setText(Version.VERSION_NUMBER);
labelAppVersion.setToolTipText("Latest available version!");
labelAppVersion.setForeground(green);
}
}
}

private void createLabelReleased() {
labelReleased = new Label(shell, SWT.NONE);
labelReleased.setText("Released:");
labelReleased.setFont(bold);
labelReleased.setLayoutData(relativeTo(labelVersion, logo));
}

private void createLabelAppReleased() {
labelAppReleased = new Label(shell, SWT.NONE);
labelAppReleased.setText(Version.VERSION_DATE);
labelAppReleased.setLayoutData(relativeTo(labelVersion, labelHomePage));
}

private void createLabelHomePage() {
labelHomePage = new Label(shell, SWT.NONE);
labelHomePage.setText("Homepage:");
labelHomePage.setFont(bold);
labelHomePage.setLayoutData(relativeTo(labelReleased, logo));
}

private void createLabelAppHomePage() {
labelAppHomePage = new Label(shell, SWT.NONE);
labelAppHomePage.setText(Version.HOMEPAGE);
labelAppHomePage.setLayoutData(relativeTo(labelReleased, labelHomePage));
labelAppHomePage.setCursor(hand);
labelAppHomePage.setForeground(blue);
labelAppHomePage.setToolTipText("Click to open in browser");
labelAppHomePage.addMouseListener(new MouseAdapter() {
@Override
public void mouseUp(final MouseEvent event) {
Program.launch(Version.HOMEPAGE);
}
});
}

private void createLabelEnvironment() {
labelEnvironment = new Label(shell, SWT.NONE);
labelEnvironment.setText("Environment");
labelEnvironment.setFont(bold);
labelEnvironment.setLayoutData(relativeTo(logo, null));
}

private void createButtonClose() {
buttonClose = new Button(shell, SWT.PUSH);
buttonClose.setText("&Close");
buttonClose.setLayoutData(relativeToBottomRight(null));
buttonClose.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(final SelectionEvent event) {
shell.dispose();
}
});
}

private void createButtonCopyToClipboard() {
buttonCopyToClipboard = new Button(shell, SWT.PUSH);
buttonCopyToClipboard.setText("C&opy to Clipboard");
buttonCopyToClipboard.setLayoutData(relativeToBottomRight(buttonClose));
buttonCopyToClipboard.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(final SelectionEvent event) {
IOUtils.copyToClipboard(Version.getEnvironmentReport());
}
});
}

private void createTextEnvironment() {
textEnvironment = new Text(shell, SWT.MULTI | SWT.WRAP
| SWT.V_SCROLL | SWT.BORDER);
textEnvironment.setText(Version.getEnvironmentReport());
textEnvironment.setEditable(false);
layout = relativeTo(labelEnvironment, null);
layout.right = new FormAttachment(100, 0);
layout.bottom = new FormAttachment(buttonClose);
layout.width = 500;
layout.height = 150;
textEnvironment.setLayoutData(layout);
}

}


If you compare the two versions, handcoded one is superior in most aspects. The code is smaller, more readable and text editor friendly. The window can be resized, it is better looking - in generated code the logo image was scaled due to dragging inacuracy. And, believe it or not, I've spent less time creating the handcoded GUI version than "drawing" the automated one and then hacking it's generated code. Of course, if you know your tools well, you can be much more productive with a GUI builder, but I'll rather learn the low-level GUI API than some commercial third party product that treats you like parents treat their kids with LEGO.

No more GUI builders for me.

2009-01-20

Dive into Java

Java Jump-start for experienced software developers. Another presentation I gave at work.

2008-11-25

Python MUD Game Example

I've found a small MUD-like single player game that I did with Python several years ago. Thought it would be nice to share the source. So here it is.

First of all, a live demo (type help. gone if my internet connection or home server is down.)

server down


And a "screenshot" (sorry for my bad sense of humor):


> help
welcome to mud. available commands are:
go, move, help, exit, look, say, take, drop, inventory, use
>
Unknown Command
> look
spajus sees deep green woods. There also seems to be: flower, bird
> touch bird
looks like a rainbow
> use bird
you do not have bird
> take bird
spajus puts bird in his inventory
> use bird
cuckarekoo! motherfucka!?!
> drop bird
bird was dropped..
> kill bird
Unknown Command
> look north
spajus sees shallow river. There also seems to be: object
> go north
spajus moves to shallow river
> look object
spajus sees a stinky one
> touch object
ewww...
> take object
spajus puts shit in his inventory
> use shit
you are sick, you know that?


Now, the engine.py

class MudObject:
def __init__(self, name, sight, collide = 'nothing happens', usability = 'unusable'):
self.name = name
self.sight = sight
self.collide = collide
self.usability = usability
def view(self):
return self.sight
def touch(self):
return self.collide
def use(self):
return self.usability
class MudPlayer:
def __init__(self, name):
self.inventory = {}
self.name = name
self.health = 100
def move(self, area):
return self.name + ' moves to ' + area.sight
def take(self, obj):
self.inventory[obj.name] = obj
return self.name + ' puts ' + obj.name + ' in his inventory'
def drop(self, name):
if self.inventory.has_key(name):
return self.inventory.pop(name)
def say(self, what):
return self.name + ' says: ' + what
def use(self, what):
if self.inventory.has_key(what):
return self.inventory[what].use()
else:
return 'you do not have ' + what

class MudArea:
def __init__(self, sight):
self.objects = {}
self.panorama = {}
self.sight = sight
self.inverted_directions = {'north':'south', 'south':'north', 'east':'west', 'west':'east'}
def addArea(self, direction, area):
area.panorama[self.inverted_directions[direction]] = self
self.panorama[direction] = area

def relocate(self, args):
try:
return self.panorama[args]
except KeyError:
return None
def addObject(self, name, obj):
if obj != None:
self.objects[name] = obj
return name + ' was dropped..'
def getObject(self, name):
if self.objects.has_key(name):
return self.objects.pop(name)
else:
return 'there is no ' + name + ' arround!'
def touchObject(self, name):
if self.objects.has_key(name):
return self.objects[name].touch()
else:
return 'there is no ' + name + ' arround!'
def view(self, args = 'arround'):
if (args != '' and args != 'arround'):
try:
return self.panorama[args].view()
except KeyError:
try:
return self.objects[args].view()
except KeyError:
return 'nothing.'
else:
objects = ', '.join([k for k, v in self.objects.items()])
if (objects != ''):
obsight = '. There also seems to be: ' + objects
else:
obsight = ''
return self.sight + obsight
import sys

class MudCommand:
""" welcome to mud. available commands are:
go, move, help, exit, look, touch, say, take, drop, inventory, use """
def __init__(self, char, area):
self.char = char
self.area = area

def go(self, args):
""" alias of move """
return self.move(args)

def use(self, args):
""" uses item from inventory """
return self.char.use(args)

def inventory(self, args):
""" displays inventory """
return self.char.name + ' has: ' + ', '.join(self.char.inventory)

def help(self, args):
""" gives you help on a topic"""
if args == '':
return self.__doc__
else:
try:
return getattr(self, args).__doc__
except AttributeError:
return 'help topic not found'

def exit(self, args):
""" exits game """
print 'bye bye!'
sys.exit()

def look(self, args):
""" lets you look arround """
return self.char.name + ' sees ' + self.area.view(args)

def take(self, args):
""" takes item from the ground """
try:
return self.char.take(self.area.getObject(args))
except AttributeError:
return 'you cannot take ' + args

def touch(self, args):
""" touches item from the ground """
return self.area.touchObject(args)

def drop(self, args):
""" drops item from inventory to current area """
return self.area.addObject(args, self.char.drop(args))

def move(self, args):
""" moves arround """
area = self.area.relocate(args)
if area != None:
self.area = area
return self.char.move(self.area)
else:
return 'There seems to be nothing that way.'

def say(self, args):
""" makes character talk """
return self.char.say(args)
class MudGame:
def __init__(self, char, area):
self.cmd = MudCommand(char, area)

def run(self):
while True:
command = raw_input('> ');
self.parse(command)

def parse(self, command):
comm = command.lower().split(' ')
try:
cmd = comm[0]
except IndexError:
cmd = 'help'
try:
args = comm[1:]
except IndexError:
args = []
try:
result = getattr(self.cmd, cmd)(' '.join(args).strip())
except AttributeError:
result = 'Unknown Command'
print result


And the main game script - mud.py:

from engine import *

#objects (name, description, on touch, on use)
rose = MudObject('rose', 'a red blossom with spikes', 'bites fingers!', 'wanna eat it or what?')
shit = MudObject('shit', 'a stinky one', 'ewww...', 'you are sick, you know that?')
gaidys = MudObject('bird', 'oh, a cock!', 'looks like a rainbow', 'cuckarekoo! motherfucka!?!')

#areas
woods = MudArea('deep green woods')
river = MudArea('shallow river')
hills = MudArea('orc hills')
house = MudArea('house of all gay')
meadow = MudArea('a green smelly meadow')

#attaching interactive stuff to areas
river.addObject('object', shit)
woods.addObject('flower', rose)
woods.addObject('bird', gaidys)
meadow.addObject('animal', gaidys)

#link all areas with bidirectional references
river.addArea('south', hills)
woods.addArea('north', river)
woods.addArea('west', house)
hills.addArea('east', meadow)
meadow.addArea('north', woods)

#create a player
char = MudPlayer('spajus')

#create a game with player and starting area
game = MudGame(char, woods)

#lets go!
game.run()


OK, and of course, unit tests:

import unittest
from engine import *

class TestMudObject(unittest.TestCase):
def setUp(self):
self.o = MudObject('object1', 'sight1', 'collision1', 'usage1')
self.o2 = MudObject('object2', 'sight2')

def test_view(self):
self.assertEqual(self.o.view(), 'sight1')
self.assertEqual(self.o2.view(), 'sight2')
self.assertNotEqual(self.o.view(), 'c')
self.assertNotEqual(self.o.view(), self.o2.view())

def test_touch(self):
self.assertEqual(self.o.touch(), 'collision1')
self.assertEqual(self.o2.touch(), 'nothing happens')
self.assertNotEqual(self.o.touch(), 'sight1')
self.assertNotEqual(self.o.touch(), self.o2.touch())

def test_use(self):
self.assertEqual(self.o.use(), 'usage1')
self.assertEqual(self.o2.use(), 'unusable')
self.assertNotEqual(self.o.use(), 'unsuable')
self.assertNotEqual(self.o.use(), self.o2.use())

class TestMudPlayer(unittest.TestCase):
def setUp(self):
self.p1 = MudPlayer('player1')
self.p2 = MudPlayer('player2')
self.area1 = MudArea('area1')
self.area2 = MudArea('area2')
self.o = MudObject('object1', 'sight1', 'collision1', 'usage1')
self.o2 = MudObject('object2', 'sight2')

def test_move(self):
f1 = self.p1.move
f2 = self.p2.move
a1 = self.area1
a2 = self.area2
self.assertEqual(f1(a1), 'player1 moves to area1')
self.assertEqual(f2(a1), 'player2 moves to area1')
self.assertEqual(f1(a2), 'player1 moves to area2')
self.assertEqual(f2(a2), 'player2 moves to area2')
self.assertNotEqual(f1(a1), 'player1 moves to area2')

def test_take_drop(self):
take = self.p1.take
use = self.p1.use
drop = self.p1.drop
inven = self.p1.inventory

o1 = self.o
o2 = self.o2

self.assertEqual(inven, {})
self.assertEqual(take(o1), 'player1 puts object1 in his inventory')
self.assertEqual(inven, {'object1':o1})
self.assertEqual(take(o2), 'player1 puts object2 in his inventory')
self.assertEqual(inven, {'object1':o1, 'object2':o2})

self.assertEqual(drop('object1'), o1)
#neesamo objekto dropint neina
self.assertRaises(TypeError, drop('object1'))
self.assertEqual(inven, {'object2':o2})
self.assertEqual(drop('object2'), o2)
self.assertEqual(inven, {})

def test_use(self):
p1 = self.p1
o1 = self.o
self.assertNotEqual(p1.use('object1'), 'usage1')
self.assertEqual(p1.use('object1'), 'you do not have object1')
p1.take(o1)
self.assertEqual(p1.use('object1'), 'usage1')

class TestMudArea(unittest.TestCase):
def setUp(self):
self.a1 = MudArea('area1')
self.a2 = MudArea('area2')
self.o1 = MudObject('obj1', 'sight1', 'collide1', 'use1')
self.o2 = MudObject('obj2', 'sight2')

def test_addArea(self):
#assignmentas turi buti veidrodinis
self.a1.addArea('north', self.a2)
self.assertEqual(self.a1.panorama, {'north':self.a2})
self.assertEqual(self.a2.panorama, {'south':self.a1})

def test_relocate(self):
self.a1.addArea('north', self.a2)
self.assertEqual(self.a1.relocate('north'), self.a2)
self.assertEqual(self.a2.relocate('north'), None)
self.assertEqual(self.a2.relocate('south'), self.a1)

def test_addObject(self):
self.assertEqual(self.a1.objects, {})
#dropped returninama todel, kad paprastai objectas zaidimo metu addinamas tada, kai playeris dropina ji.
#kreivai biski, bet ka padarysi :)
self.assertEqual(self.a1.addObject('something', self.o1), 'something was dropped..')
self.assertEqual(self.a1.objects, {'something':self.o1})
self.a1.addObject('other', self.o2)
self.assertEqual(self.a1.objects, {'something':self.o1, 'other':self.o2})
self.assertEqual(self.a1.addObject('something_clone', self.o1), 'something_clone was dropped..')
self.assertEqual(self.a1.objects, {'something':self.o1, 'other':self.o2, 'something_clone':self.o1})

def test_getObject(self):
self.assertEqual(self.a1.objects, {})
self.a1.addObject('something', self.o1)
self.a1.addObject('other', self.o2)
self.assertEqual(self.a1.getObject('something'), self.o1)
self.assertEqual(self.a1.objects, {'other':self.o2})
self.assertEqual(self.a1.getObject('something'), 'there is no something arround!')

def test_touchObject(self):
self.assertEqual(self.a1.objects, {})
self.a1.addObject('something', self.o1)
self.a1.addObject('other', self.o2)
self.assertEqual(self.a1.touchObject('something'), self.o1.touch())
self.assertNotEqual(self.a1.touchObject('obj2'), self.o2.touch())
self.assertEqual(self.a1.touchObject('ass'), 'there is no ass arround!')

def test_view(self):
view = self.a1.view
self.assertEqual(view(), 'area1')
#unindentified object/panorama
self.assertEqual(view('my brain'), 'nothing.')

self.a1.addObject('my brain', self.o1)
self.assertEqual(view('my brain'), self.o1.view())
#padarom dar idomiau. kadangi pridejom my brain, reikia parodyti ir tai..
self.assertNotEqual(view(), 'area1')
self.assertEqual(view(), 'area1. There also seems to be: my brain')
self.a1.addObject('duck', self.o2)
self.assertEqual(view(), 'area1. There also seems to be: my brain, duck') #', '.join(self.a1.objects)

self.assertEqual(view('north'), 'nothing.')
self.a1.addArea('north', self.a2)
self.assertEqual(view('north'), 'area2')
#kadangi priskyrem area2, turejo atsispindeti ir is ten paziurejus i priesinga north krypti (south), turi matytis area1 viewas
self.assertEqual(self.a2.view('south'), view())
#akurat.. matosi :)

class MudCommandTest(unittest.TestCase):
def setUp(self):
self.p1 = MudPlayer('player1')
self.a1 = MudArea('area1')
self.a2 = MudArea('area2')
self.o1 = MudObject('obj1', 'sight1', 'collide1', 'use1')
self.o2 = MudObject('obj2', 'sight2', 'collide2', 'use2')
self.a2.addObject('bread', self.o1)
self.a2.addObject('pig', self.o2)
self.a1.addArea('east', self.a2)
self.c = MudCommand(self.p1, self.a1)

def test_go_move(self):
#MudArea.go === MudArea.move
#test wrong way
self.assertEqual(self.c.go('somewhere'), 'There seems to be nothing that way.')
#test walk arround
self.assertEqual(self.c.go('east'), 'player1 moves to area2')
self.assertEqual(self.c.go('west'), 'player1 moves to area1')

def test_use(self):
self.assertEqual(self.c.use('bla'), 'you do not have bla')
#lets go east and take something to test using
self.c.go('east')
self.c.take('bread')
#as bread was only the looks, we know it's actually obj1, so lets use it
self.assertEqual(self.c.use('obj1'), self.o1.use())

def test_inventory(self):
self.c.go('east')
self.c.take('bread')
self.assertEqual(self.c.inventory(None), 'player1 has: obj1')

def test_help(self):
self.assertEqual(self.c.help(''), self.c.__doc__)
self.assertEqual(self.c.help('move'), self.c.move.__doc__)
self.assertEqual(self.c.help('blabla'), 'help topic not found')

def test_look(self):
self.assertEqual(self.c.look(''), 'player1 sees ' + self.a1.view())
self.assertEqual(self.c.look('at my balls'), 'player1 sees nothing.')
self.assertEqual(self.c.look('east'), 'player1 sees ' + self.a2.view())

def test_take(self):
self.c.go('east')
self.assertEqual(self.c.take('bread'), 'player1 puts obj1 in his inventory')
self.assertEqual(self.p1.inventory, {'obj1':self.o1})
#already taken!
self.assertEqual(self.c.take('bread'), 'you cannot take bread')

def test_touch(self): #perv test.. :)
self.assertEqual(self.c.touch('self'), 'there is no self arround!')
self.assertNotEqual(self.c.touch('bread'), self.o1.touch())
self.c.go('east')
#kad paliesti reik pirma nueiti
self.assertEqual(self.c.touch('bread'), self.o1.touch())

def test_drop(self):
self.assertEqual(self.c.drop('smelly thing'), None)
self.c.go('east')
self.c.take('bread')
self.c.go('west')
self.assertEqual(self.c.drop('obj1'), 'obj1 was dropped..')
self.assertEqual(self.a1.objects, {'obj1':self.o1})

def test_say(self):
self.assertEqual(self.c.say('i love this game'), 'player1 says: i love this game')
self.assertNotEqual(self.c.say('python sucks'), 'player1 says: that\'s true!')

if __name__ == '__main__':
unittest.main()


To run the game:
python mud.py


To run the tests:
python testengine.py


Damn, I really miss such coding activities :)

2008-11-06

Software design tips from the creator of C++ programming language

[1] Know what you are trying to achieve
[2] Keep in mind that software development is a human activity
[3] Proof by analogy is fraud
[4] Have specific and tangible aims
[5] Don’t try technological fixes for sociological problems
[6] Consider the longer term in design and in the treatment of people
[7] There is no lower limit to the size of programs for which it is sensible to design before starting to code
[8] Design processes to encourage feedback
[9] Don’t confuse activity for progress
[10] Don’t generalize beyond what is needed, what you have direct experience with, and what can be tested
[11] Represent concepts as classes
[12] There are properties of a system that should not be represented as a class
[13] Represent hierarchical relationships between concepts as class hierarchies
[14] Actively search for commonality in the concepts of the application and implementation and represent the resulting more general concepts as base classes
[15] Classifications in other domains are not necessarily useful classifications in an inheritance model for an application
[16] Design class hierarchies based on behaviour and invariants
[17] Consider use cases
[18] Consider using CRC cards
[19] Use existing systems as models, as inspiration, and as starting points
[20] Beware of viewgraph engineering
[21] Throw a prototype away before it becomes a burden
[22] Design for change, focusing on flexibility, extensibility, portability, and reuse
[23] Focus on component design
[24] Let each interface represent a concept at a single level of abstraction
[25] Design for stability in the face of change
[26] Make designs stable by making heavily used interfaces minimal, general, and abstract
[27] Keep it small. Don’t add features "just in case"
[28] Always consider alternative representations for a class. If no alternative representation is plausible, the class is probably not representing a clean concept
[29] Repeatedly review and refine both the design and the implementation
[30] Use the best tools available for testing and for analysing the problem, the design, and the implementation
[31] Experiment, analyse, and test as early as possible and as often as possible
[32] Don’t forget about efficiency
[33] Keep the level of formality appropriate to the scale of the project
[34] Make sure that someone is in charge of the overall design
[35] Document, market, and support reusable components
[36] Document aims and principles as well as details
[37] Provide tutorials for new developers as part of the documentation
[38] Reward and encourage reuse of designs, libraries, and classes

I found these great tips in a classic programming book: The C++ Programming Language Third Edition by Bjarne Stroustrup, the creator of C++. If you want to learn C++ or deepen your knowledge, this is The Book.

2008-05-30

XSL Engine

Amazingly, some bright minds of the company I work at supported the idea to release one of our products as an open source software. It's nice to see a huge "Nothing is Free" business opening up a little. I'm proud to present the release of the XSL Engine:

XSLE at Google Code

XSL Engine is an XSL transformation server and client. It provides united processing of XSL transformations, independent of any programming environment. This can remove load from other applications. It features high throughput, with the possibility to increase the throughput of the XSL transformations by setting up new servers. It operates in an Apache Tomcat Web container. XSL documents are loaded into cache. XSL Includes are supported. PDF can be generated (using XSL-FO). XSL cache can be automatically replicated among remote servers.

2008-04-21

Make your Eclipse rock

Eclipse is one of the greatest IDEs available out there. In my opinion it's the best, because in comparison with other choices such as Intellij IDEA, Eclipse is free and open. People say NetBeans are getting good, however I am too skeptic to believe. As it's the matter of preference, I will stick with Eclipse.

Yet, after a fresh and clean install Eclipse (v3.3 as of this moment) is not yet kicking. A few crucial things are missing. I'll give out my recipe to making Eclipse rock. So, let's get on with that:

  1. Fresh install from www.eclipse.org. As I'm NOT doing J2EE/Web/XML stuff at home, Eclipse IDE for Java Developers is the best choice for me.
  2. Give it more RAM! Default values are pathetic, so you should edit your eclipse.ini to look like this:
    -showsplash
    org.eclipse.platform
    --launcher.XXMaxPermSize
    256M
    -vmargs
    -Dosgi.requiredJavaVersion=1.5
    -Xms256m
    -Xmx1024m
    -XX:PermSize=256m
    Say bye to those possible out of memory and permgen space errors.
  3. Fine-tune the Preferences. Go straight to workspace and open Window -> Preferences.
    General:
    Check "Show heap status" - it's nice to see how much heap you've got.
    General -> Appearance:
    Uncheck "Show text on the perspective bar". That gives more space for perspective icons.
    Check "Show traditional style tabs". Performance.
    Uncheck "Enable animations". Performance.
    General -> Editors -> Text Editors:
    Check "Insert spaces for tabs". You may not want that if you're a tab fan.
    Check "Show print margin". 80 is good.
    Check "Show line numbers". Who would not want to see line numbers by default?
    General -> Editors -> Text Editors -> Spelling:
    Uncheck "Enable spell checking". Performance. And hell, that's no Microsoft Word.
    General -> Startup and Shutdown:
    Uncheck "Mylin Tasks UI" from "Plug-ins activated on startup". Unless you REALLY want to use Mylin. I think it sucks and terribly slows Eclipse down.
    General -> Workspace:
    Choose "UTF-8" as your "Text file encoding". Unicode is the way to go.
    Choose "Unix" as your "New text file line delimiter". This is also is the way to go.
    Java -> Code Style -> Formatter:
    Click "Edit..." on Eclipse [built-in] profile. Set new profile name. In "Identation" tab set "Tab policy" to "Spaces only". You may skip this if you're a tab dude.
    Java -> Compiler -> Errors/Warnings:
    You may want to harden your compiler warnings for more beautiful and strict development. I tend to override these:
    In "Code style": "Undocumented empty block" -> "Warning".
    In "Unnecessary code": "Unnecessary 'else' statement" -> "Warning".
    In "Unnecessary code": "Unnecessary cast or 'instanceof' operation" -> "Warning".
    In "Unnecessary code": "Unnecessary declaration of thrown checked exception" -> "Warning".
    Java -> Compiler -> Javadoc:
    Set "Malformed Javadoc comments:" -> "Warning".
    Set "Only consider members as visible as: " -> "Private".
    You want your Javadoc clean, don't you.
    Java -> Editor -> Content Assist -> Advanced:
    In content assist proposal list uncheck entries marked with "(Mylin)" and check the alternatives: "Other Java Proposals", "Template Proposals", "Type Proposals".
    In cycling list check all same entries: "Other Java Proposals", "Template Proposals", "Type Proposals".
    Web and XML -> XML Files -> Source:
    In "Formatting" section check "Indent using spaces" and set Indentation size to "2". Otherwise you will end up with tabs in your XML.
  4. Subversive - the best Eclipse plug-in for SVN support:
    Help -> Software Updates -> Find and Install. Search for new features to install. Add two New Remote Sites - "Subversive SVN Connectors" with URL: http://www.polarion.org/projects/subversive/download/eclipse/2.0/update-site/ and "Subversive plug-in" with URL: http://download.eclipse.org/technology/subversive/0.7/update-site/
    Check http://www.eclipse.org/subversive/downloads.php for latest URLs.
    Install new components. Skip Mylin integration and sources.
  5. Maven plug-in. You MUST know what Maven is, otherwise don't do Java. Seriously. You may choose from Tycho and Q: http://maven.apache.org/eclipse-plugin.html
    I was a long-time user of Tycho, however Q looks really promising. I'm trying it right now for the first time. Yeah, definitely, go for Q. Installation is easy, just add New Remote Site:
    Q4E: http://q4e.googlecode.com/svn/trunk/updatesite/
  6. XML Buddy. Your light weight swiss army knife in Eclipse XML editing. Download plug-in manually from http://xmlbuddy.com and drop it into your Eclipse plugins folder, then restart.
  7. JADClipse. Your daily Java decompiler. Best for those times when you need to go under the hood. Make this a habit. You will need JAD in your PATH: http://www.kpdus.com/jad.html
    Then download JADClipse plug-in from http://jadclipse.sourceforge.net/, drop it to Eclipse plugins folder and restart.
Enjoy your tools!

Hierarchical Model View Controller - beyond MVC

I would like to share a few articles I've written back in 2005/2006 while working on an open source project - Hierarchical MVC Web framework - Claw:

Long gone post #2.

It is a common pattern in Java community (NOTE: I thought so back then, though found out that it's not really common at all), while it seems like PHP developers think there is no heaven like MVC. For those, who are interested in HMVC concepts, read this (and notice the date – quite an oldie, eh?):

http://www.javaworld.com/javaworld/jw-07-2000/jw-0721-hmvc_p.html

I haven't found a single open source PHP framework with HMVC implementation, that's why Claw was born. Are there any more of this kind? Did I miss anything? Please let me know if I did. Anyway, the long URL above would most probably be helpful for Java GUI application developers, I must say that I do not understand more than a half of that document, and never actually read the code examples carefully, so I think it is easier for me to rewrite the concepts the way I understand them and the way they work in Claw Framework. Before that you may take a glance at another Javish explanation of Hierarchical MVC here:

http://www.thecentric.com/wiki/index.php/HMVC_Tutorial

Did not get that one either, but at least it's shorter and easier to read. If you really bothered yourself to at least briefly look through that information (and shame on you if you did not, and I know you did not), you should have found out the key advantages of HMVC pattern are something like:

  • Defined intralayer communication and isolation from higher layers
  • Defined interlayer communication with minimal coupling
  • Localization of exposure to third-party code
  • Reduced dependencies between disparate parts of the program
  • Encouraged reuse of code, components, and modules
  • Increased extensibility while easing maintainability

Back to our shop architecture – here is the draft of my HMVC-inspired implementation:



For those who see a spaceship or a piece of DNA chain – bigger blocks are controllers, small tablets – controller actions (you can call them functions, methods or events if you like), black arrows show possible routes to some point of application and dotted lines show which action belongs to which controller. Now imagine the flattened requests:

http://www.shop.com/
http://www.shop.com/list/
http://www.shop.com/list/category
http://www.shop.com/list/category/4
http://www.shop.com/list/search/query.hammer
http://www.shop.com/list/category/4/product/2524
http://www.shop.com/list/search/query.hammer/product/2524
http://www.shop.com/product/2524
http://www.shop.com/list/search/query.hammer/page.4/product/2524
http://www.shop.com/list/category/25/page.42/product/11945/reviews
http://www.shop.com/list/search/query.big+fork/sort.price/order.asc/page.18/product/114/review
http://www.shop.com/product/1545/reviews

These requests would go easily without any custom mappings, URL rewriting or any other hacks you would have to do in plain old MVC. I guess these links need no special explanation, but just in case, let's examine the longest one piece by piece:

/ IndexPage – root controller for the following hierarchy
/list ListPage – controller for accessing product lists
/search ListPage::search -search action invoked on list controller
/query.big+fork ListPage::search – argument 'query' => 'big fork'
/sort.price ListPage::search – argument 'sort' => 'price'
/order.asc ListPage::search – argument 'order' => 'asc'
/page.18 ListPage::page – argument 'page' => 18
/product ProductPage – controller for single product related logic
/114 ProductPage – numeric argument => 114
/review ProductPage::review – action for adding new review


We simply form this chain element by element, so the application grows like a tree, and when you need a new branch, you simply put a tiny piece of code on top of the tree, without having to touch what has already grown big. But you have all the information inherited in the way. In our example above, when user is reviewing product #114, application is aware that he/she came from a product list page 18 of search for “big fork” by price, so when it comes to rendering navigation and layouts, you could even show search results along with review form, if you would want that for some weird reason. But it's nice to have the power, no? Those who ever developed large systems should be nodding.
How do controllers inherit data? The picture below should help you visualize the process:



As application is executed, each controller (Page) may set some variables. Child controllers can set their own, though, when you are trying to get a variable that current controller does not have, system falls back to parent and checks if this variable is not there, and if it is, the value is returned. Recursion goes to the bottom of the chain if required. And since any point you can redeclare variables, so beyond a certain point the value will be as redeclared (though parent controllers will still use the old one). As you build your application layering the controllers one upon another, at the end of what I call “chain reaction”, application collects the flattened set of variables and passes them to final view (which is flattened after playing the puzzle within same reaction):



This is simply a composite view, with parts and layers, that each controller inherits from the parent and can manipulate it, rewrite it, etc. Final view is rendered only when chain reaction is over, and view layers are “flat” in final position. This is the time when variables are stamped in, so before final controller hits the breaks, nothing is rendered – this lazy kind of work saves resources.

Model View Controller and Enterprise Applications

I would like to share a few articles I've written back in 2005/2006 while working on an open source project - Hierarchical MVC Web framework - Claw:

Long gone post #1.

At this era of frameworks popping out of nowhere, crowds of people being obsessed with Ruby on Rails and Zend releasing their own framework, it's really a headache to choose the right tools for the right job. One thing is for sure – every good web developer knows – Model-View-Controller pattern is the right way to go.

Yes, MVC is a great pattern, no doubt that it's necessary to separate your business logic from graphical representation and database implementation. However, let's try implementing the following application using MVC:



What we have here is a simple structure of online shop, where users can choose the goods either by browsing categorized list of products, or by using the search feature. Product items may also have customer reviews, and visitors may add their own opinion for every product in the store. In real world you application structures are far more complicated than this one, but still, let's try modeling the MVC approach for this application.

Model

As a great fan of Object Oriented Programming, I would use a Domain Model pattern for the business logic part of the application. My components would be:

Category – tied up to the Product with many-to-many relation

  • getProducts - retrieves a list (an array) of Product objects, which belong to the Category
  • getTitle - retrieves a string with Category title
Product – tied up to Category with many-to-many relation, and to Review with one-to-many relation
  • getCategories - retrieves a list of Category objects, which the Product belongs to
  • getTitle - retrieves a string with Product title
  • getPrice – retrieves a price value object (to make it simple – just float value)
  • getReviews – retrieves a list of Reviews for this Product
  • addReview(Review) – adds a Review for this Product
Review
  • getAuthor – gets the author of Product review
  • getContent – gets the text
  • getRating – gets rating given to Product by the reviewer
After having my Domain Model ready, I would take care of persisting it to Database. Here you are on your own. I would go for Active Record implementation this time, as my model is dead simple. For more sophisticated cases Active Record would be a pain in the rear, so I would go for a serious ORM tool (such as Propel – http://propel.phpdb.org for PHP or Hibernate – http://www.hibernate.org for Java). So, we're done with our Model. No sweat! Time for the letter V in the MVC triad.

View

Personally for me, View part of any web application is the major headache, especially when you have got a State of Art prototype to follow, and your designer often complains that checkbox should be two pixels down in Internet Explorer, but four pixels up in Opera. Anyway, despite these annoying things implementing View in the MVC triad is a piece of cake. I prefer a Two Step View even in simplest situations, so I am going for it. There would be a plain HTML skeleton with empty body as the second step layer for each View, and the first step is upon the Controller.
The following problem may occur if you would want the following things done properly within your Views for current application architecture – How to Back-link Product items with the list they were accessed from. It may be the Category List, Search Result List or none, if user went directly to product. Of course, user can use browser back button, but what if he/she goes to read Product Reviews, afterwards, add a new one, then returns to Product View? I believe back button will not help you here. You could store required information in session or cookie variables, however, this adds unwanted complexity and introduces possible new bugs. Avoiding this functionality may annoy visitors, forcing them to repeat their search again, perhaps navigate through result pages to reach the point they were at. For now, let's keep our mind off this and see how to deal with Controllers.

Controller

Finally, we are about to liven up our application with user request processors, referred as Controllers. Normally, MVC frameworks use a Front Controller as one entry point for the whole application, which routes URL requests, invoking Input Controllers and their actions. How could the Controller architecture look with our current Online Shop application? It would probably end up like this:

Front Controller – the entry point which routes URL requests

Front
  • index – the action that is taken when user comes directly to your application root. Main page layout is set for View, some promo Product links attached, navigation menu for Product categories and search input
  • search – this is the action that search input leads to. It processes search query and forms a list of results, paging is involved.
  • category – here we have a list of Products by category, perhaps there would be options to sort items by title, price, ascending and descending order, also, including paging.
Product
  • index – action is taken when user views the product. Some identification should be passed along with request variables.
  • reviews – displays current product reviews
  • review – displays a form where user can add the review for the Product. It also processes form variables, validates them and writes the review to database.
Simple as that. We have mapped out our application with merely two Input Controllers! Let's see how some user URL request scenarios may look like and what would they map to:
  • / -> Front/index
  • /search -> Front/search (may display a search form with advanced options)
  • /search?q=sledgehammer&page=3 -> Front/search (third page of search for “sledgehammer”)
  • /search/sledgehammer/3 -> Front/search (same third page of search for “sledgehammer” with custom request rewriting – I will go on using this style of requests from now on)
  • /category/tools -> Front/category (category type is tools, first page of listing)
  • /category/food/price/asc/4 -> Front/category (food Products listed by price in ascending order, fourth page of navigation)
  • /product/2455 -> Product/index (would display information of product #2455)
  • /product/2455/reviews -> Product/reviews (displays all reviews for product #2455)
  • /product/2455/review -> Product/reviews (displays a form where user can submit her/his own review)
Everything looks nice, this scheme would work perfectly well, but the problem is – there are certain points where you loose the flow. Likewise, imagine you were at /search/sledgehammer/3, and clicked on a lovely “Almighty Masher”, which linked to /product/2455. You've certainly wanted some reviews of this great tool in action, and followed the “User Reviews” link to /product/2455/reviews. After reading for a while, you discover that “Almighty Masher” is just one overpriced piece of junk, and you want to fall back to the search you were doing. Well, back button is fine, click one, click two, and we're back on the track! But what if your application required users to be logged in to read the reviews? When you happily click “User Reviews” link, an unexpected screen appears asking you to log in, or even worse, offering you to register. Wise applications usually redirect user back to where he was going after logging in / registration process. It does not break the back button, you say? Perhaps, but it sure decreases the comfort of navigation (especially if your registration process had 7 steps).

Another disadvantage is that if you are in /product/2455/reviews and you still want to see the information of your product in the Reviews View, you will have to pull out the Product using the ID which comes along with request. And you will have to do this in /product/2455/review as well, or the reviewer may simply forget the exact title of the item. That leads to code duplication – every method of Product input controller must have some lines that find the Product. It's like shooting darts to different areas on the target – each area gets hit on it's own, as you cannot hit for both “10” and “15” with single shot due to the fact that there is no union on the target grid. With MVC pattern developers usually map the native applications tree (or in sophisticated cases – graph) hierarchy into plain two-dimensional grid enriched with variables. It can be compared to mapping Objects with many references, collections and relationships into Relational Database Management System. It's simple when your application is Farmer Joe's personal guest book, but what if you're working on something as large as E-bay or Amazon.com? You would sweat cats and dogs and spend months on writing meta-data and configuring your ORM tools to make your beautiful hierarchy broken down to the grid. And you would sure have to do the same with your controllers. But do you have to? I would not take this burden upon my shoulders, as my choice is Hierarchical MVC.