Adapterパターン(GoF)

参考書籍一覧(Amazon アソシエイトリンク)

すでに存在している下位モジュールと上位モジュールの期待するインターフェースに齟齬がある場合に、その間を取り持つクラスを作るパターン。

例としてCSVファイルとTSV(タブ区切り)ファイル、Excelファイルのいずれかを読み込んで、そのファイルから特定のレコードを検索する処理を書いているとする。各々、専用のライブラリを使って処理することになっているが、インターフェースがバラバラなため以下のようなコードを書かざるを得ない状況だとする。

# loaded_fileはライブラリのインスタンス
def find_user(loaded_file, user_id)
  # CSVライブラリはfind_by_カラム名というメソッドで検索する
  if loaded_file.is_a?(CsvLibrary)
    return loaded_file.find_by_id(user_id)
  end
  # TSVライブラリはfindというメソッドにカラムと値を指定する方法で検索する
  if loaded_file.is_a?(TsvLibrary)
    return loaded_file.find(id: user_id)
  end
  # EXCELライブラリはsearchというメソッドにカラムと値を指定する方法で検索する
  if loaded_file.is_a?(ExcelLibrary)
    return loaded_file.search(id: user_id)
  end
end

以下のように書ければ見通しが良くなるのは明らかである。

def find_user(adapter, user_id)
  adapter.find(id: user_id)
end

そこで、各ライブラリに対してAdapterを用意する。

# これは委譲する例。継承した方が良い場合もある。
class CsvLibraryAdapter
  def initialize(file)
    @library = CsvLibrary.new(file)
  end

  # find(id: 1, name: 'alice')なら
  # @library.find_by_id_and_name(1, 'alice')を呼び出す
  def find(params)
    keys = params.keys.join('_and_')
    @library.send("find_by_#{keys}", *params.values)
  end
end

class TsvLibraryAdapter
  def initialize(file)
    @library = TsvLibrary.new(file)
  end

  def find(params)
    @library.find(params)
  end
end

class ExcelLibraryAdapter
  def initialize(file)
    @library = ExcelLibrary.new(file)
  end

  def find(params)
    @library.search(params)
  end
end

これで、ファイルを読み込む時にどのクラスをインスタンス化するか考える必要はあるものの、ファイルを検索するタイミングではfindメソッドに使い方を統一できた。

Javaなどと違い、Rubyはダックタイピングな言語のためインターフェースの定義は存在しないが、Adapterとしてどのようなインターフェースを実装しているのか意識することは重要である。

また、すでに存在しているライブラリに対して依存性逆転の原則を適用するのであれば、Adapterパターンを使うことになる。