Rubyによるデザインパターンまとめ①

Rubyによるデザインパターン

Rubyによるデザインパターン

template method

利用する場面

あるインスタンスに行わせる処理の内容を、インスタンスの種類に応じて変えたい場合。

方法

  • クラスHogeにメソッドbehaveを定義する。
  • サブクラスsub1, sub2を定義し、それぞれでメソッドbehaveをオーバーライドする。

注意すべき点

  • 継承ベースであるため、メソッドbehave以外にも全部親クラスのことを引き継いでしまう。
  • サブクラスと振る舞いが対応しているため、サブクラスのインスタンスをつくった後は、そのインスタンスの振る舞いを容易に変更することができない。
class Vertebrate
end

class Mammal < Vertebrate
  def move
    puts :walk
  end
end

class Fish < Vertebrate
  def move
    puts :swim
  end
end

class Reptile < Vertebrate
  def move
    puts :crawl
  end
end

inu_tarou = Mammal.new
inu_tarou.move # => walk

saba_tarou = Fish.new
saba_tarou.move # => swim

tokage_tarou = Reptile.new
tokage_tarou.move # => crawl

strategy

利用する場面

あるインスタンスに行わせる処理の内容を、インスタンスの種類に応じて変えたい場合。 (詳細な戦略を各strategyクラスに隠蔽する)

方法

  • behaveを行うためのstrategyとしてクラスBehaviorをつくり、メソッドbehaveを定義する。
  • BehaviorのサブクラスSubBehavior1, SubBahavior2をつくり、メソッドbehaveをオーバーライドする。
  • SubBehaviorXだけで事足りるのであれば、基底クラスBehaviorはなくてもよい。(以下の例では名前空間でまとまりをもたせるためにモジュールのみ設けている。)
class Vertebrate
  attr_accessor :mover
  
  def initialize(mover)
    self.mover = mover
  end

  def move
    mover.move
  end
end

class Vertebrate
  attr_accessor :mover
  
  def initialize(mover)
    self.mover = mover
  end

  def move
    mover.move
  end
end

module Mover
  class Walker
    def move
      puts :walk
    end
  end

  class Swimmer
    def move
      puts :swim
    end
  end

  class Crawler
    def move
      puts :crawl
    end
  end
end

inu_tarou = Vertebrate.new(Mover::Walker.new)
inu_tarou.move # => walk

saba_tarou = Vertebrate.new(Mover::Swimmer.new)
saba_tarou.move # => swim

tokage_tarou = Vertebrate.new(Mover::Crawler.new)
tokage_tarou.move # => crawl

Template Methodと違って、あるインスタンスについて容易に振る舞いを切り替えることができる。 (戦略は変更することができる!)

tokage_tarou.mover = Mover::Walker.new
tokage_tarou.move # => walk

Strategyクラスとはつまるところ処理を記述したものに過ぎないので、Procオブジェクトに置き換えることができる。(クラスをつくる必要がない!)

class Vertebrate
  attr_accessor :mover
  
  def initialize(&mover)
    self.mover = mover
  end

  def move
    mover.call
  end
end

walker  = lambda { puts :walk  }
swimmer = lambda { puts :swim  }
crawler = lambda { puts :crawl }

saba_tarou = Vertebrate.new(&walker)
saba_tarou.move # => walk

inu_tarou = Vertebrate.new(&swimmer)
inu_tarou.move # => swim

tokage_tarou = Vertebrate.new(&crawler)
tokage_tarou.move # => crawl

予めProcをつくらずとも、インスタンスを生成するときにその場限りのブロックを渡してもよい。

tori_tarou = Vertebrate.new { puts :fly }
tori_tarou.move # => fly

command

利用する場面

  • あるインスタンスに行わせる処理の内容を、状況に応じて変えたり組み合わせたりしたい場合。
  • あるインスタンスに行わせる処理を、逐次ではなく貯めてからまとめて実行したい場合。

strategyとの関係

やってることはざっくり同じだと思う。 同じ呼び出し方のできる複数の処理方法を(classやprocとして)用意して、contextとなるインスタンスに1つまたは複数の処理方法をもたせて、polymorphicに呼び出す。 1つのcontextに1つだけ持たせる処理方法をstrategyと呼び、1つのcontextにいくつも持たせる処理方法をcommandと呼ぶのではないだろうか。(同時に複数の戦略をもつことはできないからね。)

observer

利用する場面

あるオブジェクトで何かが起きたときに、他のオブジェクトに知らせたい(=他のオブジェクトの決まりきったメソッドを呼び出したい)とき。

方法

  • 発信者のインスタンスに、@observersを持たせる。
  • 通知すべきことが発生したら、発信者は@observers.each { |observer| observer.update(self) }などにより知らせる。

その他

  • 他のオブジェクトに送りたいメッセージが決まりきったものでないならば、普通に書けばよいだけ。observerパターンは「お知らせするupdateメソッドを呼び出したいだけ」というような場合に用いる。
  • observer.updateの引数をselfにしてobserverの側で必要な情報を引き出すのをpull型、subject側で予め詳細なデータを用意して引数として渡すのをpush型と呼ぶ。
  • observerというパターン名ではあるが、実際に色々するのは、発信者=subjectの側であることに注意する。
  • observerに関するもろもろの処理をSubjectクラスorモジュールとして切り分け、これを継承or includeしてもよい。
  • Rubyにはobservableという組み込みメソッドがある。
  • 「これまでsubjectが登録されたobserverにニュースを配信するという例えを使ってきましたが、実際のところ、あるオブジェクトが他のオブジェクトのメソッドを呼び出す、という話をしているに過ぎません。」
  • observerもstrategy/commandも、あるオブジェクトが他のオブジェクトを呼び出す仕組みに過ぎない。呼び出されるオブジェクト自体が処理を表現しているならstrategy/command, 呼び出されるオブジェクトが単一の処理を表現するだけでなくより実体的なオブジェクトで、「observeする」とか「notifyされる」とかいう表現がしっくりくるならobserver、なんだと思う。

サンプル

class Parent
  def update(subject, action)
    puts "大変だ!#{subject.name}が#{action}している!"
  end
end

class Baby
  attr_reader :name

  def initialize(name, *observers)
    @name = name
    @observers = observers
  end

  def cry
    puts "mewl"
    @observers.each { |observer| observer.update(self, :cry) }
  end
end

tom  = Parent.new
mary = Parent.new
Baby.new(:babee, tom, mary).cry
# => mewl
# => 大変だ!babeeがcryしている!
# => 大変だ!babeeがcryしている!

composite

利用する場面

部分と全体を同じように扱いたいとき

方法

以下を定義する。

  • component: 全てのノードの基底クラス
  • leaf: 末端ノード(必要に応じて抽象クラスを定義する)
  • composite: 末端以外のノード=枝を持つノード(必要に応じて抽象クラスを定義する)

サンプル

以下の構造をもつ組織をコードで表現する。

  • Company
    • SalesDep
      • CorporateSalesDiv
      • PrivateSalesDiv
    • DevelopmentDep
      • InfraDiv
      • AppDiv
    • GeneralAffairsDep
      • FinancialDiv
      • LegalDiv
# (抽象)component
class Group
  attr_accessor :super_group
end

# 抽象leaf
class Div < Group
  attr_reader :member_count, :budget
end

# 具象leaf
class CorporateSalesDiv < Div
  def initialize
    @member_count = 5
    @budget = 280
  end
end

class PrivateSalesDiv < Div
  def initialize
    @member_count = 4
    @budget = 200
  end
end

class InfraDiv < Div
  def initialize
    @member_count = 3
    @budget = 360
  end
end

class AppDiv < Div
  def initialize
    @member_count = 6
    @budget = 250
  end
end

class FinanceDiv < Div
  def initialize
    @member_count = 2
    @budget = 120
  end
end

class LegalDiv < Div
  def initialize
    @member_count = 1
    @budget = 60
  end
end

# 抽象composite
class CompositeGroup < Group
  attr_reader :sub_groups

  def initialize
    @sub_groups = []
  end

  def add_sub_groups(*sub_groups)
    sub_groups.each { |sub_group| sub_group.super_group = self}
    @sub_groups.push(*sub_groups)
  end

  def member_count
    @sub_groups.inject(0) {|sum, sub_group| sum += sub_group.member_count}
  end

  def budget
    @sub_groups.inject(0) {|sum, sub_group| sum += sub_group.budget}
  end
end

# 具象composite
class Company < CompositeGroup
  def initialize
    super
    add_sub_groups(SalesDep.new, DevelopmentDep.new, GeneralAffairsDep.new)
  end
end

class SalesDep < CompositeGroup
  def initialize()
    super
    add_sub_groups(CorporateSalesDiv.new, PrivateSalesDiv.new)
  end
end

class DevelopmentDep < CompositeGroup
  def initialize
    super
    add_sub_groups(InfraDiv.new, AppDiv.new)
  end
end

class GeneralAffairsDep < CompositeGroup
  def initialize
    super
    add_sub_groups(FinanceDiv.new, LegalDiv.new)
  end
end

上記により、以下が実現する。

> company = Company.new
> pp company
#<Company:0x007f85730c7060
 @sub_groups=
  [#<SalesDep:0x007f85730c6f20
    @sub_groups=
     [#<CorporateSalesDiv:0x007f85730c6d90
       @budget=280,
       @member_count=5,
       @super_group=#<SalesDep:0x007f85730c6f20 ...>>,
      #<PrivateSalesDiv:0x007f85730c6ca0
       @budget=200,
       @member_count=4,
       @super_group=#<SalesDep:0x007f85730c6f20 ...>>],
    @super_group=#<Company:0x007f85730c7060 ...>>,
   #<DevelopmentDep:0x007f85730c6bb0
    @sub_groups=
     [#<InfraDiv:0x007f85730c6b10
       @budget=360,
       @member_count=3,
       @super_group=#<DevelopmentDep:0x007f85730c6bb0 ...>>,
      #<AppDiv:0x007f85730c6ac0
       @budget=250,
       @member_count=6,
       @super_group=#<DevelopmentDep:0x007f85730c6bb0 ...>>],
    @super_group=#<Company:0x007f85730c7060 ...>>,
   #<GeneralAffairsDep:0x007f85730c69d0
    @sub_groups=
     [#<FinanceDiv:0x007f85730c6930
       @budget=120,
       @member_count=2,
       @super_group=#<GeneralAffairsDep:0x007f85730c69d0 ...>>,
      #<LegalDiv:0x007f85730c68e0
       @budget=60,
       @member_count=1,
       @super_group=#<GeneralAffairsDep:0x007f85730c69d0 ...>>],
    @super_group=#<Company:0x007f85730c7060 ...>>]>
> company.member_count
=> 21
> company.budget
=> 1270

感想

composite, おもっていたより複雑だった。これまででいちばんパターン!って感じがする。

ここまでの感想

意外とデザインパターンって整理されておらず、ごちゃとアイデアが並べられているだけのような印象を受けた。 (異なるデザインパターンでも「人間からの見え方が異なるだけでシステム的には同じ構造」とかあるんだな。) 「コードはパターンにとってそれほど重要でないということを覚えておいてください。意図が重要です。」 デザインパターンはコンピュータのためのものではなく、人間のためのものなんだな。 だからこそ親しみやすいし、だからこそ(捉え方によっては)どこか軟弱な感じもする。