Tbpgr Blog

Employee Experience Engineer tbpgr(てぃーびー) のブログ

Template Methodパターンを適用したが,各継承クラスの類似度が高い場合にメタプログラミングにより重複をなくす

臭い名

Template Methodパターンの継承クラスの重複

臭い英名

Duplicated Template Method Child.

リファクタリング英名

Apply meta programming to remove duplicated code.

改善理由

Template Methodパターンを適用した上で各子クラスの処理が少ない場合
クラスやメソッドの宣言コードばかりになり、類似性が高くなる。
8割〜9割は同じコードを記述することになり、DRY原則に反する。

対応

宣言部のコードの重複を除去するためにメタプログラミングを利用する。

サンプルコード

要件

言語はRubyを使用。

前処理で日付のフォーマット指定、
テンプレートメソッドで任意の日付を設定、
後処理で指定フォーマットの任意の日を標準出力、
というTemplate Methodパターンを適用したクラスを想定します。

リファクタリング
# encoding: utf-8
require "date"
class Base
  def show
    format = get_format
    ret = each_execute
    output(ret, format)
  end

  protected
  def each_execute
    raise "not implemented!!"
  end

  private
  def get_format
    "%Y/%m/%d %H:%M:%S.%L"
  end

  def output(ret , format)
    puts ret.strftime(format)
  end
end

class Today < Base
  def each_execute
    Date.today
  end
end

class Now < Base
  def each_execute
    DateTime.now
  end
end

Today.new.show
Now.new.show
リファクタリング
# encoding: utf-8
require "date"
class Base
  def show
    format = get_format
    ret = each_execute
    output(ret, format)
  end

  protected
  def each_execute
    raise "not implemented!!"
  end

  private
  def get_format
    "%Y/%m/%d %H:%M:%S.%L"
  end

  def output(ret , format)
    puts ret.strftime(format)
  end
end

DATES = [Date.today, DateTime.now]
classes = []

DATES.each do |date|
  classes << Class.new(Base) do |klass|
    define_method(:each_execute) {date}
  end
end

CLASSES = [:Today, :Now]
CLASSES.each_with_index {|klass, i|eval "#{klass.to_s} = classes[#{i}]"}
[Today.new, Now.new].each(&:show)
変更後版に翌日クラスを追加
# encoding: utf-8
require "date"
class Base
  def show
    format = get_format
    ret = each_execute
    output(ret, format)
  end

  protected
  def each_execute
    raise "not implemented!!"
  end

  private
  def get_format
    "%Y/%m/%d %H:%M:%S.%L"
  end

  def output(ret , format)
    puts ret.strftime(format)
  end
end

DATES = [Date.today, DateTime.now, Date.today + 1]
classes = []

DATES.each do |date|
  classes << Class.new(Base) do |klass|
    define_method(:each_execute) {date}
  end
end

CLASSES = [:Today, :Now, :Tommorow]
CLASSES.each_with_index {|klass, i|eval "#{klass.to_s} = classes[#{i}]"}
[Today.new, Now.new, Tommorow.new].each(&:show)
出力
2013/09/19 00:00:00.000
2013/09/19 21:51:38.903
2013/09/20 00:00:00.000 # Tommorow版のみ表示
備考

Template Methodパターンを利用していても各子クラスの処理が複雑な場合は、
類似度が下がるのでこのパターンは好ましくない。
またメタプログラミングを利用するため複雑なパターンはさらに複雑になる。
あくまでシンプルで差異が少ない場合に適用した方がよいパターン。