Паттерны проектирования в Ruby: Команда (Command)

9 ноября 2015, понедельник

Команда (Command) — поведенческий шаблон проектирования, представляющий действие. Объект команды инкапсулирует в себе само действие и его параметры. Рассмотрим применение паттерна на примере работы с файлами.

Создадим 3 команды для создания, копирования и удаления файла. Классы CreateFile, CopyFile, DeleteFile соответственно

class BaseCommand

  attr_reader :description

  def initialize(description)
    @description = description
  end

  def execute
    raise 'Method "execute" must be implemented!'
  end

  def unexecute
    raise 'Method "unexecute" must be implemented!'
  end

end


class CreateFile < BaseCommand

  def initialize(path, content)
    super("Create file: #{path}")
    @path = path
    @content = content
  end

  def execute
    f = File.open(@path, 'w')
    f.write(@content)
    f.close
  end

  def unexecute
    File.delete(@path) if File.exist?(@path)
  end

end


class CopyFile < BaseCommand

  def initialize(source, target)
    super("Copy file: #{source} to #{target}")
    @source = source
    @target = target
  end

  def execute
    if File.exists?(@target)
      @content = File.read(@target)
    end
    FileUtils.copy(@source, @target)
  end

  def unexecute
    if !@content.nil?
      f = File.open(@target, 'w')
      f.write(@content)
      f.close
    else
      File.delete(@target)
    end
  end

end


class DeleteFile < BaseCommand

  def initialize(path)
    super("Delete file: #{path}")
    @path = path
  end

  def execute
    if File.exists?(@path)
      @content = File.read(@path)
      File.delete(@path)
    end
  end

  def unexecute
    return if @content.empty?
    f = File.open(@path,"w")
    f.write(@content)
    f.close
  end

end

Каждый класс содержит 2 метода execute и unexecute. Для выполнения действия и его отмены соответственно.

Теперь создадим класс CompositeCommand, который будет работать с этими командами

class CompositeCommand

  def initialize
    @commands = []
  end

  def add_command(command)
    @commands << command
  end

  def up
    @commands.each {|cmd| cmd.execute}
  end

  def down
    @commands.reverse.each {|cmd| cmd.unexecute}
  end

  def description
    @commands.inject([]){|result, cmd| result << cmd.description}.join(' -> ')
  end

end

Создадим объект класса CompositeCommand и добавим несколько команд

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

cmds.up #выполнит все команды
p cmds.description # Create file: file1.txt -> Copy file: file1.txt to file2.txt -> Delete file: file1.txt
cmds.down #отменит выполнение всех комманд

И еще 2 примера использования паттерна Command

#Пример 1
class BaseCommand
  def execute
    raise 'Method "execute" must be implemented!'
  end
end

class AmplifyShieldCommand < BaseCommand
  def execute
    'Amplified Plutonium-Gamma Shield'
  end
end

class CalibrateDriverCommand < BaseCommand
  def execute
    'Calibrate Urbanium-Rod Driver'
  end
end

class TestCompilerCommand < BaseCommand
  def execute
    'Tested Jupiter Wave Compiler'
  end
end

class InstallRegulatorCommand < BaseCommand
  def execute
    'Installed Hydroelecric Magnetosphere Regulator'
  end
end

class Computer
  attr_reader :queue

  def initialize
    @queue = []
  end

  def add(command)
    @queue << command
  end

  def execute
    @queue.inject([]) { |result, command| result << command.execute }.join(' ~ ')
  end
end

class Reactor
  def initialize
    @functional = false
    @right_command = [
        'Amplified Plutonium-Gamma Shield',
        'Calibrate Urbanium-Rod Driver',
        'Tested Jupiter Wave Compiler',
        'Installed Hydroelecric Magnetosphere Regulator'
    ].join(' ~ ')
  end

  def fix(result)
    @functional = result == @right_command
  end

  def functional?
    @functional
  end
end


comp = Computer.new
comp.add(AmplifyShieldCommand.new)
comp.add(CalibrateDriverCommand.new)
comp.add(TestCompilerCommand.new)
comp.add(InstallRegulatorCommand.new)

reactor = Reactor.new
reactor.fix(comp.execute)
p reactor.functional? #true

#Пример 2
class BaseCommand
  attr_reader :hero

  def initialize(hero)
    @hero = hero
  end

  def execute
    raise 'Method "execute" must be implemented!'
  end
end

class GetMoneyCommand < BaseCommand
  MONEY_VALUE = 100

  def execute
    @hero.money += MONEY_VALUE
  end

  def unexecute
    @hero.money -= MONEY_VALUE
  end
end

class HealCharacter < BaseCommand
  HEALTH_VALUE = 5

  def execute
    @hero.health += HEALTH_VALUE
  end

  def unexecute
    @hero.health -= HEALTH_VALUE
  end
end

class Hero
  attr_reader :name
  attr_accessor :health, :money

  def initialize(name, health=100, money=800)
    @name = name
    @health = health
    @money = money
  end

  def info
    "Name: #{@name}, health: #{@health}, money: #{@money}"
  end

  def print_info
    p info
  end
end

class Turn
  def initialize
    @moves = []
  end

  def make_move(move)
    move.execute
    @moves << move
  end

  def undo_move
    @moves.pop.unexecute
  end
end


hero = Hero.new('Spartak')
turn = Turn.new
money = GetMoneyCommand.new(hero)
heal = HealCharacter.new(hero)

hero.print_info # "Name: Spartak, health: 100, money: 800"

turn.make_move(money)
turn.make_move(heal)

hero.print_info # "Name: Spartak, health: 105, money: 900"

turn.undo_move

hero.print_info # "Name: Spartak, health: 100, money: 900"

turn.undo_move

hero.print_info # "Name: Spartak, health: 100, money: 800"