はじめに
Ruby on Railsでは、Rails Way:「設定より規約(CoC)」の原則に則った考え方を取り入れ、Railsのルールに従うことで効率的に開発できるようデザインされています。一方で、マネーフォワードやSansan, SmartHRを始めとする現代の象徴的SaaS企業では、Ruby on Rails特有の仕様が技術的負債となる事例は少なからず存在します。今回は、Railsにおける典型的な負債パターンであるリアーキについて、そのアーキテクチャ戦略を、自分なりに考察してみました💪
Railsにおけるリアーキの難しさ
Railsでのリアーキを阻む壁として、以下のような主要素が挙げられます:
- 型が死んでいる
- コードジャンプが適切に反映されず、定義にとべない(RubyMineですら怪しい)
- 名前空間をRails側が規約しているため、動かすのが難しい
- 既存のMVCを崩すのが難しい
他にも様々な要因が思いつく方はいると思いますが、結論として、Rails Wayの適応外になる変更は、Railsのルールとコンフリクトするため、変更が難しいですよという話になります。
設計ルール
Railsのアーキテクチャ設計では、次のルールを守る必要があります。それは、「Railsの世界を破壊しない」ことです。つまり、インフラストラクチャのコアがMVCモデルに集約する必要があります。これを踏まえて、実際にリアーキを考察していきます。
MVCアーキテクチャ
まずお浚いをします。Railsでは、デフォルトでMVC (Model-View-Controller) アーキテクチャが採用されています。責務をデータ管理、プレゼンテーション、アプリケーションロジックの3つに分割します。
規約したルーティングに基づき(config/routes.rb
)、ユーザーのリクエストしたエンドポイントに対応するControllerのメソッドを呼び出します。
-
ControllerはHTTPリクエストの責務吸収と、Modelのロジックを実行するアプリケーションサービス(ドメインサービスではない)を実行します
-
Modelはデータ管理とビジネスロジックを担当します。永続化のインフラストラクチャは、ActiveRecordにより負担されます
- Viewはプレゼンテーションレイヤを構築します。Modelから取得されるデータはContollerを介して、Viewで表示されます。APIモードでRailsを使用する場合、この責務は死亡して、MCモデルの責務で構築されることなります(ここで違和感を感じることができた方は、かなり設計力が高いです[後述])
ここまでMVCモデルについて、おさらいをしました。それでは、リアーキの世界に飛び込んでみます。
MVCアーキテクチャ内で変更を入れてみる
Railsの世界を破壊しないという話をしました。まずは、一切の破壊を許さずに、MVCアーキテクチャとして完結する変更を入れてみます。
Controllerの責務分離
まず、一番最初に解体できそうなControllerから責務を分離してみます。このケースにおける境界は、API ControllerとView Controllerに分離します。フルスタックフレームワークのため、APIの利用とViewとして利用したいユースケースを想定します。
構成としてはroutes.rb
で/api
用のパスをnamespaceで切り出します。また、app/controllers
配下でfrontend
とapi
のディレクトリを分けるなどが一般的です。
Controllerの責務分離を行うことで、APIで利用したいControllerとViewの組織を分断することができました。階層でapiとfrontend(なんでもいい)を名前空間で分けているため、実際に呼ばれるControllerは、Api::HogeController < Api::ApplicationController
となり、非常に都合が良いですね。
何かがおかしい
このパターンはよく見かける設計かと思われますが、MVCモデルは破綻しています。どこが破綻しているかというと、API Controllerが、レスポンスのプレゼンテーションを構築する責務を持っているからです。
したがって、この責務をViewに譲渡させてみます。
Viewの責務分離
前述の通り、API Controllerからプレゼンテーションの責務を引き剥がしていきます。API Responseの構築には、JBuilderというActionViewのテンプレートエンジンを使用します。ERBやHaml同様、View内でテンプレートを生成できます。
これにより、ControllerとViewで各責務を担保しつつ、リファクタリングを行うことができました。
Modelはどうする?
MVCモデル内でのModelの変更は少し複雑で、この後の話を聞いた方がわかりやすい、と思ったため後述に回しています。ぜひ最後まで読んでみてください。少なからず設計に対しての解像度が上がると思います🔥
ここからは、Rails Worldを飛び出した、本格的なリアーキの設計戦略について考察していきます。
ドメイン駆動設計(DDD)
DDDへのリアーキを考えます。繰り返しになりますが、設計方針はRails Worldを破壊しないことです。それに基づいて、DDDへのリアーキをデザインしてみます。
既存のMVCモデルを破壊せずに、DDDへ変更するには、MVCアーキテクチャにドメイン駆動設計を足し合わせる方法が有効そうです。簡単のため、関心ごと以外を全て排除して、以下のようにモデリングしてみます。
上図では、もともとModelが持っていたビジネスロジックとビジネスルールを、DDDの世界として切り出してます。Controllerは、これまでのHTTPリクエストを吸収する責任と、アプリケーションサービス(ユースケース)を実行する責務をえますが、ここでController自体がMVCモデルから、いわゆるクリーンアーキテクチャ(Layered Architecture)のController層の責務に変化していることに気づきたいです。設計における、「概念の拡張」に成功しています。
肥大化したModelはDomain Modelとして切り出し、ActiveRecordをデータコンテナ化することで、DBモデルとドメインモデルとして世界を分断します。これまでのアーキテクチャではModelはMVCモデルのModelとして機能しましたが、ここではActiveRecordというインフラストラクチャのインターフェースないしDBモデルとして概念の抽象化が行われます。この時点で、この世界の主役が、ModelからDomain Modelに切り替わったことがわかります。
Entityパターン
ドメインモデルの各要素について観察していきます。まずは主役であるEntityです。データをコピーする手間を省くために、ActiveRecordをデータコンテナとして、DDDのドメインオブジェクトに流します。この設計パターンはエンタープライズアプリケーションアーキテクチャ(PofEAA)として知られていますが、ActiveRecordパターン(RailsのActiveRecordではない)がまさに、当該設計に当てはまります。
余談ですがDeNAのPocochaはEntityパターンを採用しているみたいですね。
それはさておき、先ほどのアーキテクチャの詳細設計をしてみます。
Entityではドメイン駆動設計に基づき、ビジネスルールを定義していきます。ビジネスルールとは、言い換えると前提です。バリデーションのロジックをActiveRecordから拝借していますが、Entityでは存在のバリデーションを行うことで、主役となるドメインオブジェクトがどのような存在なのかを仮定しています。
またActiveRecordのメソッドを使用できるため(正確にはEntity内でActiveRecordの永続化に関するメソッドは使用しない)、存在の基本動作を定義できている状態です。
EntityとActiveRecordモデル(MVCにおけるModel)間での差分が少ないので、MVCからのリアーキにおいては、代替が簡単で相性のいいリアーキになります。
module Domain
module Entity
class User
attr_accessor :record
delegate :name, :age, :email, to: :record
# ActiveRecordのインスタンスを初期化
def initialize(record)
self.record = record
end
def valid?
record.valid?
end
end
end
end
Domain/Entityを名前空間でMVC Modelと分断し、以上のようにEntityを表現します。バリデーションについては、下記のようにActiveRecordで定義したバリデーション処理を呼び出しています。
class User < ApplicationRecord
validates :name, presence: true, length: { maximum: 50 }
validates :email, presence: true, uniqueness: true, format: { with: /\A[^@\s]+@[^@\s]+\z/, message: "is invalid" }
validates :password, presence: true, length: { minimum: 8 }
validates :age, numericality: { only_integer: true, greater_than_or_equal_to: 18, less_than_or_equal_to: 100 }, allow_nil: true
# 省
def mapper_method
Entity::User.new(self)
end
end
また、ActiveRecordのモデルクラスとEntityのクラスは1対多で紐づくため、ActiveRecordのモデルクラスに以下のようなマッパーメソッドを用意できます。これにより、多数のエンティティから共通のActiveRecordを呼び出せるので、Modelの肥大化を抑制できます。
※集約(Aggregate)についてもEntityと同じように考えることができるため、解説は行いません。
Repository
当然ですが不要です。ActiveRecordというORMは永続化のインフラストラクチャとして機能します。したがって、Repositoryの永続化責任を果たす必要はないので、Service層で直接ActiveRecordを召喚します。上図を見ていただくと、Repositoryが出てきていないことがわかります。アーキテクチャやオブジェクト指向に取り憑かれた方だと、Repositoryという責任概念が恒久で当てはまると勘違いしている場合が多いと思いますが、この場合シンプルに過剰分離です。
Service
Domain Service
基本的には不要かなぁというのが自分の見解です。ActiveRecordがある以上、操作をインターフェースとして切り出せない(出すべきではない)ので、業務サービスクラス(仮)ではActiveRecordモデルのメソッドを呼び出す手続的な操作が多くなりアプリケーションサービスと責務重複が起きます。ActiveRecordモデルの名前空間で管理できなくなるデメリットも考えると、そもそも作らないことが懸命な判断かと思われます。
Application Service
EntityとActiveRecordモデルを使用して永続化、データロード、トランザクション全てを、アプリケーションサービスの手続きとして完結させます。レイヤードアーキテクチャ概念で言うと、Repository層→Service層のステップを省略していきなりUsecase層まで到達します。このことからMVC to DDDの置換は3層アーキテクチャの責務分けに近い気がします。
class HogeBusinessService
def execute(params)
entity = Domain::Entity::HogeBusiness.new(params).mapper
unless entity.valid?
return { result: false, report: entity.record }
end
# 永続化処理やデータロードの一連の処理を記述
# トランザクションが必要ならこれ以降に記述する
entity.record.save!
{ result: true, report: entity.record }
rescue StandardError
{ result: false, report: entity.record }
end
end
Application Serviceなので純粋に手続的処理を書いて、エラーが出ればRaiseするだけなので詳しい内容は解説しません。手続き処理なのに、ロジック書いちゃっていいの?と思われる方もいらっしゃると思いますが、いいと思います。基本的にpublicはexecuteのみで、関数が肥大化しそうならprivateで切り出せば良いと思います。逆にこれ以上責務分けを行うとRailsの開発効率が死んでしまうので、アンチパターンだと判断します。
Tips: 責務で意識したいこと
Railsにおける、責務分けの考え方は、「他レイヤーと結合しない責務をどれだけ多く持てるか」と発想するのが、現段階で一番開発効率が良いと思います。
Clean Architecture
クラスのネストのネストのクラスのモジュールのクラスのネストのクラスのインターフェースが存在する世界
クリーンアーキテクチャレベルのモジュール結合度でやりたい場合、相当開発規模が大きくなっていることが多いです。それは、Railsにおいては、MVCアーキテクチャの各モデルが超巨大に膨れ上がっている状態です。
Controllerにおいては、エンドポイントの数にほぼ比例するため、そこまで量が増えませんが、Modelに関して言えば際限がないです。ですのでModel内での階層構造は、ActiveRecordで定義したActiveRecordモデルを主クラスとする、果てしないネストクラスと名前空間が広がっています。このイメージを持って、「本当に」リファクタリングを行うべきか慎重に考えた方が良いと思います。
アーキテクチャトロポジー
ここまで、MVC to DDDについて、一つひとつのパターンに着目して考察してきました。最後に自分の個人的な見解とその根拠を述べてこの記事を締めようと思います。
MVCアーキテクチャのまま大きくするのがベター
実際に、国内最大級のRails大規模開発に参加する一個人の意見としては、MVCアーキテクチャがRailsのアーキテクチャとして最も優れていると思っています。
固定概念に縛られすぎ問題
人間としての性なのか、世のアーキテクチャ議論を見てると名前に縛られすぎだろ、と思っています。
例えばRailsにおいて、ModelはEntityの広義です。Entityパターンの開設時に、「何が違うんですか?」と思った方は、まさに自分と同じ感覚を共有しているなと思います。ActiveRecordをデータコンテナとして、ドメインオブジェクトとして振る舞いの表現をするが、振る舞いの表現をするのにActiveRecordが必要という、単方向の依存が発生するので、EntityがModelの狭義であることは、一目瞭然でしょう。
Service Classは悪なのか?
同様の問題として、これもまた有名なサービスクラス問題があります。
これも、結局のところ、Service層という責務に取り憑かれているなと感じます。単純にModelの名前空間により(さらにその中で名前空間を使ったって構わない、)世界を分断して、ドメインでモジュール分割すればModel内で処理は完結しまし、これによる依存関係は自分が指定した名前空間単位で発生するため、テストも当該範囲のみで完結します。非常にTestableです。このことから考えて、「Service Classは悪か」などという疑問が出てくるのがそもそも本質じゃなくて、やってること一緒じゃんという回答になります。分ける上でServiceかどうかは対して問題じゃなく、「Rails上で責任が破綻しないモデリングを行えているか」にフォーカスした方が良いという結論になります。
最後に
ここまで、一般的に取り上げられるRailsのアーキテクチャ設計について取り上げました。汎用的で、抽象度の高い概念操作が多かったかと思います。ソフトウェア設計はとても奥が深く、この記事を書いているときも、「まだこんなに理解が浅かったのか」とげんなりしつつも、楽しく記事を書いていました。
抽象度の高い操作を行えるようになればなるほど、アーキテクチャについて深い理解度で設計を行うことができるようになるのではと思います。今後もさらに勉強します。
さらに学習する
今回のアーキテクチャ設計を聞いて、さらに学習してみたいと思った方は、以下の参考書がおすすめです。いずれも分厚くて抽象的な内容が多く含まれていますが、一般的に登竜門とされているものが多いようです。
ドメイン駆動設計を始めよう
2024年に発売された書籍ですが、オライリーの中でもトップクラスで読みやすい本でした。オライリーの難しいかき回しがあまり得意ではなく、理解するまでに何度も読み直してしまうのですが、この本は元々理解している領域、という前提はあるのですが、すごくわかりやすかったです。分厚くないのも好き。
Clean Architecture 達人に学ぶソフトウェアの構造と設計
ダントツでいい本でした。Robert. C. MartinもRuby使いらしく結構親近感ありますね。ちなみにこの本廃番になったらしいですね。実書籍を買っておいて良かったです。
オブジェクト指向設計実践ガイド
読んでない本載せんなって話ですが、Rails界隈ではバイブルだと聞いたので載せておきます。本当に中身知らないので大したレビューもないのですが、Rubyistたちからの評判は高いなと思います。そんなに気になってないので、買う気はあんまりないです。
ソフトウェアアーキテクチャの基礎
定番本中の定番本ですが、分厚いので、結構かいつまんで読んでいます。いかんせん分厚くて、表現も難しいので好きではないのですが、一番詳しく書かれているので、辞書みたいな使い方してますね。素読みできる方は本当に尊敬します。