Before we dive right into code, let's talk a bit about what the Command Pattern is, and why you should use it.
The command design pattern is a way to abstract the idea of "command actions" for your application's point of view. When you think about an application, it typically involves taking user input and then doing something with it (in the most general sense). You can think of the "do something with it" as a command. When the user clicks a button, then there's an action that happens. Or, when the user selects a menu item, there's another action that happens. In many applications, these actions involve calling a set of methods that do all sorts of things. For example, when the user selects File->Open, a dialog appears and a document is opened in your application. For File->Save, you save the document and perhaps display a dialog to do so.
What the Command Pattern forces you to do is write concise code in a very modular way when it comes to executing actions. This may sound like it's a lot of work, but trust me, it pays off in the end. The way things work is quite simple.
There's an interface called Command which implements two methods: an Execute method and an Unexecute method. Each action that your application can perform comes in the form of a class that implements the Command interface. Finally, there is an invoker which is responsible for taking an abstract Command and invoking its methods. That's the entire pattern! You'll usually see the Command implementors referred to as "Concrete Commands" (meaning they actually do something, unlike the interface itself).
So let's dive in and see what the code for this pattern looks like. The most important part of the pattern is the interface, so let's take a look at it. I'm going to name my interface Action (because that's my preferred term for this), but you're welcome to be a traditionalist and name it Command.
[rbcode]
Interface Action
Sub Execute()
End Sub
Sub Unexecute()
End Sub
End Interface
[/rbcode]
There's nothing too daunting about the interface. As you can see, both our methods are quite simple -- one to trigger the command, and the other to undo it. These two simple methods are the crux of an undo/redo system.
So let's take a look at a contrived example of some commands. We're going to have two interface elements that cause actions to occur: a button and a menu item. These actions are going to be very simplistic to begin with -- they're just going to fire a MsgBox whenever an action occurs or is undone.
[rbcode]
Class ButtonAction Implements Action
Sub Execute()
MsgBox "Button Clicked"
End Sub
Sub Unexecute()
MsgBox "Button Unclicked"
End Sub
End Class
Class MenuAction Implements Action
Sub Execute()
MsgBox "Menu Selected"
End Sub
Sub Unexecute()
MsgBox "Menu Unselected"
End Sub
End Class
[/rbcode]
As you can see -- this is a rather contrived example. However, it will get the point across nicely. These are just two of any number of Actions your application can perform. So how do these actions actually get fired? Simple -- via the "invoker." We're going to make a module that's responsible for taking a generic command and executing it.
[rbcode]
Module Invoker
Sub Invoke( cmd as Action )
cmd.Execute
End Sub
End Module
[/rbcode]
So you are probably wondering how this gets you any closer to an Undo/Redo system. Well, it's really quite easy from here since you've implemented the Command Pattern -- you just need to make a module that manages two stacks of actions. One set of functions will handle the Undo stack, and the other set will handle the Redo stack. Once we've got them set up, we can modify the Invoker to add the action to the Undo stack once its executed the command. Here's what our stack management module will look like:
[rbcode]
Module UndoRedoSystem
Private Dim mUndoStack( -1 ) as Action
Private Dim mRedoStack( -1 ) as Action
Protected Sub AddToUndoStack( cmd as Action )
mUndoStack.Append( cmd )
End sub
Private Sub AddToRedoStack( cmd as Action )
mRedoStack.Append( cmd )
End Sub
Protected Function CanUndo() as Boolean
return UBound( mUndoStack ) >= 0
End Function
Protected Function CanRedo() as Boolean
return UBound( mRedoStack ) >= 0
End Function
Protected Sub UndoLastAction()
dim cmd as Action
if UBound( mUndoStack ) >= 0 then
// Grab the last action from our stack (and remove it)
cmd = mUndoStack.Pop
// Undo the action
cmd.Unexecute
// Add the action to our redo stack now
AddToRedoStack( cmd )
end if
End Sub
Protected Sub RedoLastAction()
dim cmd as Action
if UBound( mUndoStack ) >= 0 then
// Grab the last action from our stack (and remove it)
cmd = mRedoStack.Pop
// Do the action
cmd.Execute
// Add the action to our undo stack now
AddToUndoStack( cmd )
end if
End Sub
End Module
[/rbcode]
As you can see, none of the code is overly complicated. There's a function that lets you know whether there are actions on either of the stacks (so that you can enable your Undo and Redo menu items properly). There's also a function that lets you undo the last action (which automatically places it on the redo stack), and a function that let's you redo the last action (placing it on the undo stack). You can also modify this system to do other neat things, like a Repeat command, which re-does the last action.
The only thing left to do is to hook everything up to the system, which simply involves adding the action to the Undo stack when the command is invoked. So let's modify our invoker.
[rbcode]
Module Invoker
Sub Invoke( cmd as Action )
// Add the action to our undo stack
UndoRedoSystem.AddToUndoStack( cmd )
// Then execute the action
cmd.Execute
End Sub
End Module
[/rbcode]
Voila! Now everything is hooked up. You now have a full Undo and Redo system using the Command Pattern, and you'll notice that it didn't take too much work! There's really nothing too magical about it -- it's just a design principle to follow when appropriate.
If you're wondering how to make the Actions a little less contrived, the simplest answer is -- constructors are your friend. The way I usually handle things is to store needed information for the command on the Action implementor itself. Then I pass the information in when constructing the object. Now the Execute method should have everything it needs to be able to do its action. The Unexecute method should always be the opposite of the Execute in terms of outcome. So if you added a new object to your form, it should remove that object from the form (where the object and the form are both parameters passed into the Constructor).
This is great stuff! Any chance you will author a book called... um... REAL DESIGN PATTERNS??? Sign me up for the autographed copy.
~joe
Glad you like it! And I'm not working on anything official, but I am thinking of covering the major design patterns and how to use them in REALbasic. Perhaps as a set of whitepapers or something along those lines.
I really enjoyed this article - when it comes time rewrite the undo/redo system in my app, I may very well use this.
By the way, I forwarded it to my former CS teacher so she could read it.
Glad to hear you enjoyed it! And let me know if your CS teacher has anything constructive to add/say. :-)
Aaron,
I know many burgeoning RB developers who would love to see a design pattern book aimed at REALbasic. Such skills are key to a developer who wants to write maintainable code, and this example (above) is both well-thought, and easy to understand. As always, thanks for your generous contribuions to the RB community.
Even if the book was something available as a PDF, or as a CafePress on-demand book, it would be a boon to the RB community.
Copy Geoff oh this... The RB community wants more documentation as much as we want more features! I know four VB developers who downloaded the free Standard Edition a couple weeks back who are ready to make the jump. The only thing in the way is comprehensive documentation and training!
-Nate Smith
Well, I'm thinking of writing a series of articles on design patterns in REALbasic -- maybe it'll turn into a book. But only if time permits.
Glad people are enjoying the topics though!
Hi Aaron,
A link to download the project file would be a great help to beginners like me.
Just my 2c.