エンジニアがRubyでデザインパターンを学ぶ - Strategy Pattern


概要

「Rubyによるデザインパターン※」を読んでデザインパターンを勉強中。
Javaをやっていた人間としての目線で情報を整理してみます。

※残念ながら日本語版は絶版らしいですが原著はKindleで購入可能です。結構文章も読みやすいので極端な英語アレルギーがある人でなければオススメです。

他のデザインパターン

Template Method Pattern

Strategy Pattern

  • Template Method Pattern と同様に「変わるもの」と「変わらないもの」を分離する
  • 継承によりクラスの親子関係として分離するのではなく移譲を用いる
  • 具体的な処理は Context クラスから Strategy インターフェースを実装したクラスに移譲される
  • 同じインターフェースを実装してさえいれば処理内容の切り替えを用意に行う事が可能

Java での実装

Context

Context.java
public class Context {
  private String userName;
  private GreetingStrategy greetingStrategy;

  public void setGreetingStrategy(GreetingStrategy strategy) {
    this.greetingStrategy = strategy;
  }
  public void setUserName(String userName) {
    this.userName = userName;
  }
  public String getUserName() {
    return userName;
  }

  // ロジックがどのような文脈(Context)で呼び出されたのかを知らせるために this を渡しています
  public void greet() {
    greetingStrategy.greet(this);
  }
}

具体的なロジックは greetingStrategy で参照されるクラスに実装されているので、クラスを入れ替えることでロジックをごっそり変更することができます。また、ConcreteStrategy のロジックの変更、もしくは追加が行われてもこのクラスには影響がありません。

Strategy

Strategy.java
public interface GreetingStrategy {
  public void greet(Context ctx);
}

ConcreteStrategy

具体的なロジックはここで定義。必要な情報は Context オブジェクトから取得します。

InEnglishStrategy.java
public class InEnglishStrategy implements GreetingStrategy {
  public void greet(Context ctx) {
    System.out.println("Hello! " + ctx.getUserName() + "!");
  }
}
InJapaneseStrategy.java
public class InJapaneseStrategy implements GreetingStrategy {
  public void greet(Context ctx) {
    System.out.println("こんにちは!" + ctx.getUserName() + "さん!");
  }
}

呼び出し

Main.java
import java.util.Scanner;

public class Main {
  public static void main(String[] args) {
    System.out.print("Enter your name: ");
    String name = new Scanner(System.in).nextLine();

    Context ctx = new Context();
    ctx.setUserName(name);

    // 名前にマルチバイト文字が含まれていれば日本語で、
    // そうでなければ英語で挨拶するように Strategy を切り替えます
    if (str.length() == str.getBytes().length) {
      ctx.setGreetingStrategy(new InEnglishStrategy());
    } else {
      ctx.setGreetingStrategy(new InJapaneseStrategy());
    }

    ctx.greet();
  }
}

実行結果

$> java Main
Enter your name: John Doe
Hello! John Doe!
$> java Main
Enter your name: 名無しの権兵衛
こんにちは!名無しの権兵衛さん!

この例ではロジックがあまりにも単純なので Strategy Pattern を使うメリットはあまり無いですが、ロジックを動的に変更可能である、というのが大きなメリットです。

Ruby での実装

Context

Context.rb
class Context
  attr_accessor :user_name
  attr_writer :greeting_strategy

  def greet
    @greeting_strategy.greet(self)
  end
end

Strategy

不要。
ここが Java エンジニアにはなんとも気持ちの悪いところ。
Ruby においては「同じシグネチャのメソッドを持ったオブジェクトは同じインターフェースを実装している」と考えるべき。

ConcreteStrategy

InEnglishStrategy.rb
class InEnglishStrategy
  def greet(ctx)
    puts "Hello! #{ctx.user_name}!"
  end
end
InJapaneseStrategy.rb
class InJapaneseStrategy
  def greet(ctx)
    puts "こんにちは! #{ctx.user_name} さん!"
  end
end

呼び出し

main.rb
require_relative 'context'
require_relative 'in_english_strategy'
require_relative 'in_japanese_strategy'

print "Enter your name: "
name = gets.chomp

ctx = Context.new
ctx.user_name = name
if name.length == name.bytesize
  ctx.greeting_strategy = InEnglishStrategy.new
else
  ctx.greeting_strategy = InJapaneseStrategy.new
end
ctx.greet

実行結果

$> ruby main.rb
Enter your name: John Doe
Hello! John Doe!
$> ruby main.rb
Enter your name: 名無しの権兵衛
こんにちは! 名無しの権兵衛 さん!

Ruby での実装(よりRubyらしく)

この例のようにロジックがシンプルな場合、わざわざ ConcreteStrategy をクラスとして定義するまでもなく、次のようにブロックを渡すようにした方がシンプルでわかりやすく記述できる。

class Context
  attr_accessor :user_name

  def greet
    yield @user_name
  end
end
print "Enter your name: "
name = gets.chomp

ctx = Context.new
ctx.user_name = name
if name.length == name.bytesize
  ctx.greet {|name| puts "Hello! #{name}!" }
else
  ctx.greet {|name| puts "こんにちは! #{name} さん!" }
end

参考

Olsen, R. 2007. Design Patterns in Ruby