2. Rubyによるデザインパターン【Strategy】
今回もデザインパターンの一つ、 Strategy(ストラテジー) パターンの記事を書いてみる。
Template Method と Strategy を利用できるタイミングが似ているので、どういう時にどっちを使うか議論されているみたい。
参考:language agnostic - When to use template method Vs. Strategy? - Stack Overflow
前回同様、下記ソウルを忘れないこと。
参考:programming - Rubyによるデザインパターン5原則 - Qiita
調査
Wikiで調べてみる。
参考:Strategy パターン - Wikipedia
Strategy パターンはアルゴリズムのセットを定義する方法を提供し、これらを交換可能にすることを目的としている。
Wikiの図の説明だが、Strategy インターフェースが定義され、そのインターフェースの実装を ConcreteStrategyA,B クラスで行う。
Context クラスは Strategy インターフェースで実装されたクラスを保有(内包)した上で、それに処理を委譲している。
Context クラスは利用する ConcreteStrategy を切り替えれば、手軽に処理を切り替えられる。
Template Method は 継承 がキモだったが、Strategy では 委譲 がキモになる。
概念
なるべくイメージしやすいことに例えて、Strategy パターンを考えてみる。
Z君 < 幹事 合コンに連れて行く(友達) #保有(人で表現しているからおかしいけど...) 盛り上げる 友達が盛り上げる #委譲 盛り上げ担当インターフェース 盛り上げる A君 < 盛り上げ担当インターフェース 盛り上げる コールする B君 < 盛り上げ担当インターフェース 盛り上げる 一発ギャグする
いつも合コンの幹事をしているZ君、でも、彼は合コンを盛り上げることができません。
そこで合コンには盛り上げ担当を連れて行っています。
Z君は合コンによって、例えば、A君にコールさせたり、B君に一発ギャグさせたりします。
Z君は他力本願ながら合コンをいつも盛り上げています。
さて、Strategy パターンのメリットを考えてみる。
- Z君自身が盛り上げるネタ(実装)を持っていなくても、友達の力を借りて(委譲)盛り上げることができる
- 盛り上げ担当を増やせば、Z君の盛り上げるネタの種類が増える
- 合コンに応じて、盛り上げるネタを切り替えられる
逆にデメリット。
- Z君は盛り上げ担当がやるネタの概要を知っていなければならない
- 合コンに応じて、連れて行く盛り上げ担当を切り替えなければならない
Strategy のキモは委譲、他力本願、そして、切り替え
実装
# -*- coding: utf-8 -*- class Z def initialize(friend) @friend = friend p "#{@friend.class.to_s}を連れて行く" end def liven_up p "盛り上げる" @friend.liven_up end end class LivenUpInterface def liven_up;end end class A < LivenUpInterface def liven_up p " コールする" end end class B < LivenUpInterface def liven_up p " 一発ギャグする" end end # A君を連れて行くパターン a = Z.new(A.new) a.liven_up # B君を連れて行くパターン b = Z.new(B.new) b.liven_up # 実行結果 # "Aを連れて行く" # "盛り上げる" # " コールする" # "Bを連れて行く" # "盛り上げる" # " 一発ギャグする"
Z クラスは LivenUpInterface を実装した A, B クラスを保有した上で、それらに処理を委譲している。
Z クラスは状況に応じて、A, B クラスを切り替えることで、処理の切り替えができる。
しかし、前回(Template Method)同様に注意点がある。
Ruby では Interface の定義ができない。
しかし、Ruby では、インターフェースや抽象クラスを継承・実装しなくても、ダックタイピングという特徴によりポリモーフィズムが実現できる。
class A def liven_up ...
かなり省略しているが、要するに LivenUpInterface を継承しなくても、Z クラスから呼ばれるクラスは liven_up メソッドさえ実装していれば動作する。
これらを踏まえて、Interface なしで実装してみる。
# Z class の実装は同じ module Friend class A def liven_up p " コールする" end end class B def liven_up p " 一発ギャグする" end end end # A君を連れて行くパターン a = Z.new(Friend::A.new) a.liven_up # B君を連れて行くパターン b = Z.new(Friend::B.new) b.liven_up
Interface で縛れないので、Friend という名前空間でクラスをまとめてみただけ。
やっぱり、実装を強制したいなら前回と同様に、
module Friend class Base def liven_up raise end end class A < Base ... end
Base クラスを追加し、liven_up メソッドで raise, サブクラスは Base を継承して liven_up メソッドをオーバーライドする。
こういう実装にするしかないかな。
あとは Ruby 標準ライブラリで 委譲 を実装することができる。
参考:Rubyist Magazine - 標準添付ライブラリ紹介 【第 6 回】 委譲
一つ目はForwardable の利用例。
require 'forwardable' class Z extend Forwardable def_delegators :@friend, :liven_up def initialize(friend) @friend = friend end end # a = Z.new(Friend::A.new) # a.liven_up
def_delegator や def_delegators メソッドにより、誰に何を委譲したいか定義できることが特徴。
もう一つ、SimpleDelegator を利用する例。
require 'delegate' class Z < SimpleDelegator def initialize(friend) super(friend) end end # a = Z.new(Friend::A.new) # a.liven_up
Forwardable は委譲の条件を定義できるのに対して、こちらは全てを委譲する。
まとめ
継承より集約、委譲を体現する Strategy パターン。
ほぼメリットしかないように思う。このパターンをバシッと使えるときが来たら、相当気持ちいいと思う。
デメリットというか、Strategy パターンを無理に使うことによる弊害はあるのかもしれない。
例えば、呼び出しが面倒になる。
# Strategy パターンでの呼び出し hoge = Hoge.new(array, Sort::Quick.new) hoge.sort # あるクラスのクラスメソッドによる呼び出し Sort::Quick.sort(array)
切り替えられるアルゴリズムの種類が少なくて、増える予定もなければ、Strategy パターンを無理に適用しない方がいいと思う。
あるクラスに実装を詰め込めんで、クラスメソッドで呼ぶだけで十分な場合もある。
業務での使いどころを考えるなら、例えば、画像エフェクトとか独自の集計アルゴリズムが複数ある場合とか。
よくある実装例としては、Template Methodと同じくテキストを HTML, XML, JSON などで出し分ける例。
Strategy パターンは切り替えられることがメリットなので、処理(アルゴリズム)を切り換える必要が出てきたら、一つの方法として Strategy パターンを頭に浮かべるといいと思う。
おさらい
- キモは委譲
- Ruby なら委譲には forwardable と delegate という標準ライブラリを利用できる
- Ruby ならインターフェース(メソッド)を統一しておけば、ダックタイピングによってポリモーフィズムを実現できる
- ある処理に対して複数のアルゴリズムがある場合に Strategy パターンはハマる
次回
次は Builder パターンでも記事にしようと思う。
下記サイトを参考にさせて頂きました。
ブロックによる Strategy パターン実装例もありました。
programming - Rubyによるデザインパターン【Strategy】-取り替え可能パーツ群を戦略的に利用せよ- - Qiita
ストラテジ Ruby 2.0.0 デザインパターン速攻習得 [Strategy][Design Pattern] - 酒と泪とRubyとRailsと