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


概要

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

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

Template Method Pattern

  • 「変わらないもの」と「変わるもの」を親クラスと子クラスに分離する
  • 親クラスでは処理の流れ(変わらないもの)のみを定義する
  • 子クラスでは具体的な処理(変わるもの)を実装する

AbstractClass#template_method 内では抽象メソッド(operation1, operation2, operation3)の呼び出し順など、処理の骨格のみを定義。operation1〜operation3 の具体的な処理内容は子クラスで実装することによって、実装の変更に対する影響を局所化できる。

Java での実装

AbstractClass.java
public abstract class AbstractClass {
  abstract protected void operation1();
  abstract protected void operation2();
  abstract protected void operation3();

  public void templateMethod() {
    operation1();
    operation2();
    operation3();
  }
}
ConcreteClass.java
public class ConcreteClass extends AbstractClass {
  protected void operation1() {
    System.out.println("This is op 1.");
  }
  protected void operation2() {
    System.out.println("This is op 2.");
  }
  protected void operation3() {
    System.out.println("This is op 3.");
  }
}

Ruby での実装

Java の場合は親クラスで抽象メソッドとして定義したメソッドを子クラスでオーバーライドすることを強制できるが、Rubyにはそのような仕組みは存在しない。

そのため、子クラスでメソッドがオーバーライドされることを保証するために、親クラスでは例外を投げるようにする、という方法をとる。

AbstractClass
class AbstractClass
  def operation1
    raise "Must override method :operation1"
  end

  def operation2
    raise "Must override method :operation2"
  end

  def operation3
    raise "Must override method :operation3"
  end

  def template_method
    operation1
    operation2
    operation3
  end
end
ConcreteClass
class ConcreteClass < AbstractClass
  def operation1
    puts "This is op 1."
  end
  def operation2
    puts "This is op 2."
  end

  # operation3 の実装漏れ
end

この場合、ConcreteClass.new.template_method のように実行すると次のように 実行時例外 が発生する。

This is op 1.
This is op 2.
test.rb:11:in `operation3': Must override method :operation3 (RuntimeError)
    from test.rb:17:in `template_method'
    from test.rb:32:in `<main>'

もちろん実行してみるまでエラーは発見できませんが、それを確認できるようなユニットテストを行えば良い。つまり、Java ではコンパイル時に検証されていた部分をユニットテストにて検証するのです。

Sample Code

Abstract Class

class Worker
  def initialize
    @output = 0
  end

  # template method. 9時から18時まで仕事をしてその日の進捗を上司に報告します
  # :do_task の実装方法が変更されてもこのメソッド自体には影響がない
  def work
    for hour in 9...12
      hourly_log time: hour, memo: do_task()
    end

    hourly_log time: 12, memo: lunch()

    for hour in 13...18
      hourly_log time: hour, memo: do_task()
    end

    report
  end

  private

  # 抽象メソッド. 子クラスでオーバーライドします
  def do_task
    raise "Must override method :do_task"
  end

  # デフォルト実装を親クラスで定義しておいて、オーバーライドするかどうかを子クラスに任せることも
  def lunch
    "Lunch with colleagues."
  end

  def report
    puts
    puts "Hi boss! I'm #{self.class}."
    puts "Today I have finished #{@output} tasks!"
    puts
  end

  def hourly_log(time: , memo: )
    printf("%02d:00 %s\n", time, memo)
  end
end

ConcreteClass

class EarnestWorker < Worker
  # 抽象メソッドの実装. コンスタントに仕事をします
  def do_task
    @output += 1
    "Working."
  end

  # デフォルト実装とは異なる挙動にしたいのでオーバーライド
  def lunch
    @output += 1
    "Lunch and work!"
  end
end
class LazyWorker < Worker
  # 抽象メソッドの実装. ときたま寝てしまいます
  def do_task
    if [true, false].sample
      @output += 1
      "Working."
    else
      "Take a nap..."
    end
  end
end

呼び出し

workers = [LazyWorker.new, EarnestWorker.new]

workers.each do |w|
  w.work
end

実行結果

09:00 Working.
10:00 Take a nap...
11:00 Working.
12:00 Lunch with colleagues.
13:00 Working.
14:00 Working.
15:00 Working.
16:00 Take a nap...
17:00 Working.

Hi boss! I'm LazyWorker.
Today I have finished 6 tasks!

09:00 Working.
10:00 Working.
11:00 Working.
12:00 Lunch and work!
13:00 Working.
14:00 Working.
15:00 Working.
16:00 Working.
17:00 Working.

Hi boss! I'm EarnestWorker.
Today I have finished 9 tasks!

参考

Olsen, R. 2007. Design Patterns in Ruby