Shred IT!!!!

IT全般について試したこと・勉強したことを綴ったり、趣味について語るブログ

2. Rubyによるデザインパターン【Strategy】

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

今回もデザインパターンの一つ、 Strategy(ストラテジー) パターンの記事を書いてみる。

Template Method と Strategy を利用できるタイミングが似ているので、どういう時にどっちを使うか議論されているみたい。
参考:language agnostic - When to use template method Vs. Strategy? - Stack Overflow


前回同様、下記ソウルを忘れないこと。
参考:programming - Rubyによるデザインパターン5原則 - Qiita

  • 変わるものを変わらないものから分離する
  • プログラムはインターフェイスに対して行う(実装に対して行わない)
  • 継承より集約
  • 委譲、委譲、委譲
  • 必要になるまで作るな(YAGNI

調査

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 パターンを頭に浮かべるといいと思う。

おさらい
次回

次は Builder パターンでも記事にしようと思う。


下記サイトを参考にさせて頂きました。
ブロックによる Strategy パターン実装例もありました。

programming - Rubyによるデザインパターン【Strategy】-取り替え可能パーツ群を戦略的に利用せよ- - Qiita
ストラテジ Ruby 2.0.0 デザインパターン速攻習得 [Strategy][Design Pattern] - 酒と泪とRubyとRailsと