LoginSignup
8
7

More than 5 years have passed since last update.

Rubyデザインパターン 5日目 : Command

Posted at

Rubyデザインパターン学習のために、自分なりに読書の結果をまとめていくことに決めました。第5日目はCommandです。(http://www.amazon.co.jp/gp/product/4894712857/ref=as_li_qf_sp_asin_tl?ie=UTF8&camp=247&creative=1211&creativeASIN=4894712857&linkCode=as2&tag=morizyun00-22)

スクリーンショット 2015-07-27 11.25.28.png

 5日目 Command

5日目はCommandパターンです。
本書ではGUIアプリケーションの構築が例に挙げられています。
ユーザーがスクリーン上の特定のボタンをクリックすると、on_button_pushメソッドが呼ばれるクラスを構築するときの事例です。

Command サンプルコード

class Button
  attr_accessor :command

  def initialize(command)
    @command = command
  end
  # ボタンの描画と管理のためのコード
  #
  #
  def on_button_push
    @command.execute if @command
  end
end

class SaveCommand # コマンドオブジェクト
  def execute
    # 現在の文書を保存
    #
    #
  end
end

save_button = Button.new(SaveCommand.new)
save_button.on_button_push # => 保存を実行

これがCommandパターンの一例です。
より一般的なButtonクラスを構築し、それに委譲する形でオブジェクトを渡します。
Buttonクラスは不変部分であり、そこに可変のコマンドオブジェクトを実装する形です。

変わるところと変わらないところを分離するというパターン大原則にならっています。

引数にブロックを渡す

クラスではなくコードブロックを渡したいなら、こういった構成にするといいでしょう
※ ただし、常にブロック渡しが有効とも限らないので、クラスベースの委譲方式も選択肢として持っておくべきでしょう!

class Button
  attr_accessor :command
  def initialize(&command)
    @command = command
  end
  #
  #
  def on_button_push # ボタンが押されたら
    @command.call if @command
  end
end

実行コマンドを記録する


require 'fileutils'

class Command # 基底クラス
  attr_accessor :description

  def initialize(description)
    @description = description
  end

  def execute # 空のexecuteメソッド
  end
end

class CreateFile < Command # Create 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
end

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

  def execute
    File.delete(@path)
  end
end

class CopyFile < Command # Copy Command
  include FileUtils
  def initialize(source, target)
    super("Copy file: #{source} to #{target}")
    @source = source
    @target = target
  end

  def execute
    FileUtils.copy(@source, @target)
  end
end

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

cmds.execute
puts cmds.description

実行したコマンドを記録するためにComposite Commandクラスにdescriptionメソッドを定義してあります。

  def description
    description = ''
    @commands.each { |cmd| description += cmd.description + "\n" }
    description
  end

Observerとの違い

  • Commandパターン
    • add_commandでコマンドオブジェクトを@commands配列に登録し、それをイテレータを通して実行します。
  • Observerパターン
    • add_observerでオブザーバオブジェクトを@observers配列に登録し、それをイテレータを通して実行します。そしてオブザーバオブジェクトは基底クラスの変更を見守っています。

唯一の違いは、登録された側が基底クラス内のデータを監視しているかどうか、だけです。
Commandは渡した処理をただ単に実行するだけです。

コマンドを使ったUndo

単純なUndo機能を実装するのも簡単です

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

file = CreateFile.new('hoge.rb', "何妙法蓮華")
file.execute

# file.unexecute これで簡単にUndoが実行できます

 まとめ

実行したい機能を配列として持ち、それを一気に実行したりできるパターンです。
機能を束ねるといった色が強いでしょう。

パターンの構築方法としては Strategy + Composite + Observer = Command のようにも取れるのでしょうか。
Commandは、パターンというよりは概念に近いかもしれません。

またRubyで容易に書くことができるパターンの一つなので、汎用性も高く、使い所は多そうですね。

8
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
7