Like a grocery list, a Command pattern command is an instruction to do something, something specific. With the Command pattern, we construct objects that know how to perform some very specific actions. Commands are useful for keeping a running list of things that your program needs to do, or for remembering what it has already done. You can also run your commands backwards and undo the things that your program has done.
To apply the Command pattern to a button that listening to click event, we simply store a command object with each button.
class SlickButton
attr_accessor :command
def initialize(command)
@command = command
end
#
# Lots of button drawing and management
# code omitted...
#
def on_button_push
@command.execute if @command
end
end
We can then define different commands for all of the things that our buttons
class SaveCommand
def execute
#
# Save the current document...
#
end
end
# Call the Save button
save_button = SlickButton.new( SaveCommand.new )
We are trying to keep track of what we are about to do—or have done—we will need a class to collect all of our commands. Hmm, a class that acts like a command, but really is just a front for a number of subcommands. Sounds like a composite:
class CompositeCommand < Command
def initialize
@commands = []
end
def add_command(cmd)
@commands << cmd
end
def execute
@commands.each {|cmd| cmd.execute}
end
def description
description = ''
@commands.each {|cmd| description += cmd.description + "\\n"}
description
end
end
Other than being a nice use of another pattern, CompositeCommand allows us to tell the user exactly what we are doing to his or her system. We could, for example, create a new file, copy it to a second file, and then delete the first file:
cmds = CompositeCommand.new
cmds.add_command(CreateFile.new('file1.txt', "hello world\\n"))
cmds.add_command(CopyFile.new('file1.txt', 'file2.txt'))
cmds.add_command(DeleteFile.new('file1.txt'))
# To actually execute all of these file comings and goings, we simply call execute:
cmds.execute
When you use this pattern, you are no longer simply saying, “Do this”; instead, you are saying, “Remember how to do this,” and, sometime later, “Do that thing that I told you to remember.”
Redo—that is, the ability to unchange your mind and to reapply the change that you just undid—falls elegantly out of the same design. To redo something, we just start re-executing the commands, starting with the last one that was undone. We start by adding an unexecute method to the CreateFile command:
class CreateFile < Command
def initialize(path, contents)
super "Create file: #{path}"
@path = path
@contents = contents
end
def execute
f = File.open(@path, "w")
f.write(@contents)
f.close
end
def unexecute
File.delete(@path)
end
end
Things get a little more challenging with the DeleteCommand, because this command is inherently destructive: To undo a delete operation, we need to save the contents of the original file, *before we delete it*
In a real system, we would probably copy the contents of the file to some temporary directory, but for our example we will just read them into memory:
class DeleteFile < Command
def initialize(path)
super "Delete file: #{path}"
@path = path
end
def execute
if File.exists?(@path)
@contents = File.read(@path)
end
f = File.delete(@path)
end
def unexecute
if @contents
f = File.open(@path,"w")
f.write(@contents)
f.close
end
end
end