Compendious Thunks Command Pattern Intro
Our apps respond to events—a search, a button click, a ping from a webhook—by taking action. How can we consistently decouple the event from the action to be taken? A classic, simple, but deceptively powerful technique is to use the Command Pattern.
In this second episode of our Compendious Thunks guest series with chef Chris Strom, you’ll learn that the command pattern is less about the language features involved, and more about how you organize the code that uses them.
Video transcript & code
In episode #461, I introduced new collaboration between RubyTapas and Chris Strom. I told you that for the next few weeks you would be seeing some selections from his terrific Compendious Thunks video series on software design patterns.
Just as a reminder, the Compendious Thunks examples are in the Dart language. If you’re just now joining us, you might want to pause here and review that episode before proceeding. It will give a the 5 minute nutshell introduction to Dart that will help you get the most value from what you’re about to see.
And what are you about to see? Today’s episode introduces the Command Pattern, a simple but deceptively powerful technique that decouples events — such as user interface interactions — from the concrete actions those events should trigger. Take it away, Chris!
(Note: if you’re wondering what a “tear-off method” looks like in Ruby, check out this post.)
The command in the command pattern couldn't be simpler: it is the combination of a thing and an action. It's an object and a method to be invoked on that object. That's it!
By itself, a command doesn't count as a Pattern™. It's just another object. What makes the pattern is the "how" and the "why" it gets used.
"How" command objects get used is via a set of similar objects. These invoker objects are usually (but are not limited to) UI elements—think buttons or menu items. And they need to be able to tell another object to do something.
The pattern arises from a natural desire for consistency. You don't want one button to call a function while another invokes a command via a
call() method while a third uses a
do() method. That's silly. The common sense desire for all buttons to invoke their commands in the same way is what gives rise to the pattern.
Consistency is not the sole benefit of the command pattern. Once you have a command object that can tell a series of objects to do something, it is a short distance to being able to tell those same objects to undo those actions. It also gets easier to combine actions into macro commands.
We'll look at undo and macro commands (among other things) in the complete series. For now, let's dive into some code.
[embed_dartpad id="4392714b92b33a4581703a20efaf00cb" title="Start with a simple Robot class"][/embed_dartpad]
We start with a
Robot class, which is fairly simple. It takes some x-y coordinates, defaulting to zero-zero. We have a location getter to report on how we are doing and where our robot is at. And we have
move() methods to move the robot in various directions. That's our robot.
In pattern terminology, that's going to be our receiver. It will "receive" the action via commands.
Next up, we need buttons to test this out. It will store a name (
Right, etc.). It also needs to store a command. The
Button constructor requires and assigns those two instance variables. Finally, when the button is pressed, the command is called.
In pattern terminology, that's going to be our invoker. It invokes the commands that tell objects to do something.
Down in the application code, we add a "
Right" button. The command will send the
moveRight() action to the robot receiver. Similarly, when the "
Left" button is pressed, we want the robot to move left. When we hit the "
Up" button, the robot should move up and when we hit the "
Down" button, the robot should go... down.
All right, let's play a little. We press the right button, which changes the robot's x-y location to.. (1,0). If we press the button twice, the robot's x-y location changes to (2,0). Then, we press left once, then up, and down. The net result of moving both up and down is no change in the Y position. By moving right twice and then back left once, we change the X position by one. So our final location should be 1,0... which it is.
So everything seems to be working. Great!
You'll notice here that our "command" is just a function. The buttons get functions that, when called, send
move() actions to the robot. Since they send actions to receivers, these functions very much count as official commands. And, since each function is called the same way (as a function), this counts as a command... pattern. Just because it's simple doesn't mean it's not worthy of the terminology.
Speaking of simple, if a language supports tear-off methods, these commands get simpler and prettier. A tear-off method is a language feature that allows the assignment of a method complete with its associated object. That is, if I assign the
move variable to
robot.moveUp and then call the
move variable, it will tell the robot to move up! That won't work in non-compendious languages, but it does in Dart.
So we can replace the function calls with tear-offs like so...
...and our command pattern code still works and it much prettier for it.
[embed_dartpad title="Pretty command code thanks to tearoffs" id="f388dddf060a4ebe04b7ab097484761f"][/embed_dartpad]
That's the compendious and quite thunky introduction to the command pattern. We're definitely not done yet. In the complete command series, we'll look at the implications and power of the command pattern including undo, using multiple receivers, macros, and dynamic commands.