factory_botと学ぶデザインパターン【Strategyパターン】

最近factory_botのコードを読み始めました。 factory_botにはいくつかのデザインパターンが実装されていて、createbuildメソッドの定義には、Strategyパターンが使われています。
factory_botのコードを追いながら、Strategyパターンについて学ぶ記事です。

Strategyパターンの紹介

ポリモーフィズムを使ってアルゴリズムの切り替えを可能にするデザインパターンです。簡略化したコードでは以下のようなイメージです。呼び出し側は、状況に応じてどのアルゴリズムを使うかを選択してアルゴリズムの切り替えができます。

class AlgorithmA
  def print_name
    p 'AlgorithmA'
  end
end

class AlgorithmB
  def print_name
    p 'AlgorithmB'
  end
end

class Main
  attr_reader :algorithm

  def initialize(algorithm)
    @algorithm = algorithm
  end

  def execute
    algorithm.print_name
  end
end

# Client
Main.new(AlgorithmA.new).execute
Main.new(AlgorithmB.new).execute

もしStrategyパターンを使わないとすると条件分岐がMain#executeの中に入ってきます。

class Main
  def execute
    if some_condition
      p 'AlgorithmA'
    else
      p 'AlgorithmB'
    end
  end
end

こうなるとアルゴリズムのパターンが増えたときには条件分岐が増え、可読性・保守性が下がります。なので、executeの中の条件分岐を外(AlgorithmA, AlgorithmB)に追いやり、責務を移譲するのがStrategyパターンです。

結城浩さんのデザインパターンの本では、じゃんけんの戦略を呼び出し側で切り替えする実装例が紹介されています。

増補改訂版 Java言語で学ぶデザインパターン入門

factory_botのcreateメソッド

factory_botのcreatebuildなどのメソッドの箇所でStrategyパターンが使われています。実装の詳細を確認する前に、factrory_botの使い方についておさらいします。

factrory_botを使うと以下のように書いて、インスタンスの生成ができます。

let(:user) { create(:user) }

createメソッドは、factory_botがFactoryBot::Syntax::Methodsモジュールに定義されているメソッドです。include することによって使えるようになります。

rails_helper.rb
RSpec.configure do |config|
  config.include FactoryBot::Syntax::Methods
end

Strategyパターンを使ったcreateメソッドの定義

FactoryBot::Syntax::Methodscreateメソッドが直接書かれている訳ではなく、実装は外からdefine_methodによって定義されています。Strategyパターンを使って実装されています。

FactoryBot::Internal.register_default_strategiesで、メソッド名に対応するクラスを指定しています。

lib/factory_bot/internal.rb
def register_default_strategies
  register_strategy(:build, FactoryBot::Strategy::Build)
  register_strategy(:create, FactoryBot::Strategy::Create)
  register_strategy(:attributes_for, FactoryBot::Strategy::AttributesFor)
  register_strategy(:build_stubbed, FactoryBot::Strategy::Stub)
  register_strategy(:null, FactoryBot::Strategy::Null)
end

各メソッドをFactoryBot::Syntax::Methodsに定義するのは、lib/factory_bot/strategy_syntax_method_registrar.rbで行われます。

lib/factory_bot/strategy_syntax_method_registrar.rb
def define_singular_strategy_method
  strategy_name = @strategy_name

  define_syntax_method(strategy_name) do |name, *traits_and_overrides, &block|
    FactoryRunner.new(name, strategy_name, traits_and_overrides).run(&block)
  end
end

def define_syntax_method(name, &block)
  FactoryBot::Syntax::Methods.module_exec do
    if method_defined?(name) || private_method_defined?(name)
      undef_method(name)
    end

    define_method(name, &block)
  end
end

簡単に処理の流れを図示したものが以下になります。

なぜStrategyパターンが使われているのかを自分なりに考えてみると、以下の理由かなと思います。

  • traitを使う、_listのメソッドも合わせて定義するなど、基本的にやりたいことは同じ
  • ただ処理の中身は各メソッドで差異があるので、アルゴリズムを切り替えたい

最後に

factory_botで実装されているStrategyパターンを見てみました。
factory_botでは他にもObserverパターンなども入っているので、こちらも続編記事などで紹介したいと思います。

Source code