詳解Railsデザインパターン:Interactor

9 minute read

高尾が解説する 詳解Railsデザインパターン・シリーズの「Interactor」編です。

他のデザインパターンも解説していますので、よろしければご覧ください。


Rails の Interactor パターンは Formオブジェクト と同様に Rails のコントローラーの肥大化するのを防ぐことに効果があるデザインパターンです。

類似するRailsのデザインパターンとして「Service Object」と呼ばれるものがあります。これについては Railsで重要なパターンpart 1: Service Object(翻訳)|TechRacho by BPS株式会社 が詳しいです。さらに Service Objectがアンチパターンである理由とよりよい代替手段(翻訳)|TechRacho by BPS株式会社 には問題点として以下が挙げられています。

本質的にService Objectパターンそのものには、コードベースを読みやすくする力も、メンテしやすくする能力も、concernをうまく分割する手腕もありはしない

本質的に Interactor パターンは Service Object パターンと同じなのですが、いくつかの制限があります。それによって、 Service Object の問題点が解消されています。

今回はこの Interactor パターンを扱います。

Google広告


関連記事・レポジトリ

まずは関連記事の紹介。どれも良記事。これらを読めば Interactor を理解できます。

次は Interactor パターンのコード例ですが、 collectiveidea/interactor の Interactors in the Controller にあります。

他にも collectiveidea/interactor の Clarity には Interactor の命名についてのヒントがあります。

TIP: Name your interactors after your business logic, not your implementation. CancelAccount will serve you better than DestroyUser as the account cancellation interaction takes on more responsibility in the future.
(翻訳: DeepL)
ヒント: インタラクタの名前は、実装ではなくビジネスロジックに基づいて命名してください。CancelAccountは、DestroyUserよりも、将来的にアカウント・キャンセルのインタラクションがより責任を負うようになったときに役立つでしょう。

Service Object vs Interactor

私は Service Object パターンの3つの問題を Interactor では以下のようにして解決していると考えています。

  • 問題1: コードベースを読みやすくする力がない
    • → コントローラーのコードのみ Interactor に移動させ、名前もドメインにあったものにする (例: DestroyUser ではなく CancelAccount)。モデルのコードはそのまま。
  • 問題2: メンテしやすくする能力がない
    • → インターフェースは call ただ 1 つ。引数と結果は context に格納。
  • 問題3: concernをうまく分割する手腕がない
    • → 複数の Interactor を束ねる Organizer を提供。例えば、共通の処理としてメール通知 Interactor を用意して、複数の Organizer から利用できる。

Interactorの実装

interactor geminteractor-rails gem を使えばアプリケーションに Interactor パターンを導入できます。説明も上記の関連記事・レポジトリで十分ですね。それくらい、 Interactor パターンはよく使われているのでしょうね。

とはいえ、それだけではなんなので、 interactor gem を参考にして実装した Interactor モジュールを以下に挙げます。hook、rollback、Organizerは省略しています。とてもシンプルです。

require "ostruct"

module Interactor
  class Failure < StandardError
    attr_reader :context

    def initialize(context)
      @context = context
      super
    end
  end

  class Context < OpenStruct
    class << self
      def build(context = {})
        self === context ? context : new(context)
      end
    end

    def fail!
      @failure = true
      raise Failure, self
    end

    def success?
      !failure?
    end

    def failure?
      @failure || false
    end
  end

  def self.included(base)
    base.class_eval do
      extend ClassMethods
      attr_reader :context
    end
  end

  module ClassMethods
    def call(context = {})
      new(context).tap(&:run).context
    end
  end

  def initialize(context = {})
    @context = Context.build(context)
  end

  def run
    call
  rescue Failure
  end

  def call
  end
end

今回、 interactor geminteractor-rails gem のコードを一通り読んだのですが、コード量も少なく、Railsの特定のバージョンに依存したコードもありません。Rails 自体をバージョンアップしても問題はなさそうです。万が一、問題があったとしてもモンキーパッチで回避できたり、PRを送ればすぐに修正してくれそうです。

まとめ

Interactor パターンを利用することでコントローラーのコードの一部を Interactor に移動させることができます。

Service Object パターンと違って以下の制限があります (interactor gemを使う場合)。

  • インターフェースは call ただ 1 つ
  • 引数、結果は context に格納
  • コントローラーの処理のみ Interactor に移動させ、モデルの処理はモデルのまま (これは制限というよりも、慣習とか規約ですね)

interactor geminteractor-rails gem を利用すれば簡単に Interactor を導入できます。Railsバージョンアップの際も問題ないでしょう。

タグ: ,

カテゴリー:

更新日時: