新規開発のRailsアプリケーションの方針

方針

  • Railsに合う形でClean Architectureを導入(あるあるのFat Controller、Fat Modelと対応する形で表記)
    • 対Fat Controller
      • Usecase層導入(Serviceという名前はどのサービスを指すかわからなくなるので使わない。)
    • 対Fat Model
      • Modelの分離
        • UsecaseのアクターごとのModelを作成する。
      • 必要に応じてcomposed_ofを利用したValue Objectの導入
      • Usecaseの実装をModelに実装しない(Modelには一般的なルールを実装する。一部のUsecaseに処理が存在するという理由だけでModelに移動しない。一部の重複ならそれはたぶん共通Usecase)

具体

controllerのイメージ

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class HogeController
  sig { void }
  def show
    typed_params = TypedParams[ShowParams].new.extract!(params)

    u_params = Hoge::ShowUsecase::Params.new(
      actor: current_user,
      hoge_id: typed_params.hoge_id,
    )

    @res = Hoge::ShowUsecase.new(u_params).call
  end

  class ShowParams < T::Struct
    const :hoge_id, Integer
  end
end

usecaseのイメージ

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
module Usecase # Usecase用のインターフェース
  extend T::Sig
  extend T::Helpers
  interface!

  sig { abstract.returns(T::Struct) }
  def call
  end
end

class Hoge::ShowUsecase
  include Usecase
  extend T::Sig

  class Params < T::Struct
    const :actor, Admin
    const :hoge_id, Integer
  end

  # sig { void }
  # def precondition # or validate
  # end

  # sig { void }
  # def postcondition
  # end

  sig { params(params: Params).void }
  def initialize(params)
    @params = params
  end

  class Response < T::Struct
  end

  sig {override.returns(Response)}
  def call
    # 諸々実装
    Response.new
  end
end

modelのイメージ(たぶんこんな感じ)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Hoge < ApplicationRecord
end

class ActorA::Hoge < Hoge
  self.inheritance_column = :_type_disabled
end

class ActorB::Hoge < Hoge
  self.inheritance_column = :_type_disabled
end

言葉の定義

  • 単一責任の定義: 一つのアクターに対して責任を負う (Uncle Bobの定義を利用)

私見

  • Fat Controllerに関して
    • リソースあるいはリソース群の作成、更新、参照は特定のアクターの要求を満たすためにほぼ同時に実装の変更が入ることが多いのでRailsのControllerが単一責任を破っているとまでは思わない (削除は変更が連動して発生しないので少し特殊な気はする)。
    • Controller内でinner class使えばそこまで見通しは悪くなかったのでは?結局Controller内のメソッドのみで書こうとするのが問題の本質では?(掘り下げ続けると教育が云々とかまでたどり着くような…)
  • Fat Modelに関して
    • そもそもUsecaseに書くべきことがModelにあふれているのでは?2,3箇所しか出てない共通ロジックならばおそらくそれは共通Usecaseとして表現されてしかるべきものでは?(Usecaseのコンポジションで表現されるべきものなのでは?)1Usecaseでしか使われてないメソッドがモデルにあるとしたらどう考えても過剰な実装では?(何かしらの原理主義的な感じが…)
    • RailsのModelがどうのこうのというよりは単一責任の原則を守らず突き進んでいるから発生するのでは?
  • RailsのModelに関して
    • Entity, Gateway, Infra(といえば良いのか?)がくっついた立ち位置の認識。
    • アクターごとにModelを分離すれば単一責任は守られる
  • トランザクションに関して
    • 通常Usecaseが境界になるのでUsecase内で行う感じで、1Usecase1トランザクションかなと。
Built with Hugo
テーマ StackJimmy によって設計されています。