天天看點

Design Patterns in Ruby [Digest 7] Command

Command pattern command is an instruction to do something, something specific. It can be execute right now or later or specific time. In our GUI's do and undo actions it is wildly used.

If we have a Button base class:

class SlickButton
  #
  # Lots of button drawing and management
  # code omitted...
  #
  def on_button_push
    #
    # Do something when the button is pushed
    #
  end
end
           

There are many buttons that inherit from the SlickButton to overwrite the on_button_push method.

If some of the button's action is dynamically changing, the subclass will be an explosive increasing.

If we use Command:

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
           

The  base command

class SaveCommand
  def execute
    #
    # Save the current document...
    #
  end
end
           

Then the command is seperate from button, and the button can have any command they want even in runtime.

We can define many commands such as: CreateFile, DeleteFile, CopyFile and so on.

We can also build Composite command use Composite pattern:

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
           
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'))
           

The undo command is really common, the Command pattern support it by define a undo method in the command class:

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
           
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
           

The composite command will be :

class CompositeCommand < Command
  # ...
  def unexecute
      @commands.reverse.each { |cmd| cmd.unexecute }
  end
  # ...
end
           

The key thing about the Command pattern is that it separates the thought from the deed.

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.”

Even in the lightweight code block-based renditions of the Command pattern available in Ruby, the two-part aspect of this pattern adds some serious complexity to your code.

Make sure that you really need that complexity before you pull the Command pattern out of your bag of tricks.

The best example of the Command may be the ActiveRecord Migrations with the self.up and self.down method that enable database level do migrate and undo migrate.

It it much easy to do database migration, the latest version of rails can support specific migration do and undo by recording all the migrate number.

Another great example is Madeleine