1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

watnow Advent Calendar 2024

Day 21

RailsでMVCアーキテクチャからドメイン駆動設計へリアーキするときに考えること

Last updated at Posted at 2024-12-22

はじめに

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つに分割します。

qiita_architecture_blog-default_mvc.drawio.png

規約したルーティングに基づき(config/routes.rb)、ユーザーのリクエストしたエンドポイントに対応するControllerのメソッドを呼び出します。

  • ControllerはHTTPリクエストの責務吸収と、Modelのロジックを実行するアプリケーションサービス(ドメインサービスではない)を実行します

  • Modelはデータ管理とビジネスロジックを担当します。永続化のインフラストラクチャは、ActiveRecordにより負担されます

  • Viewはプレゼンテーションレイヤを構築します。Modelから取得されるデータはContollerを介して、Viewで表示されます。APIモードでRailsを使用する場合、この責務は死亡して、MCモデルの責務で構築されることなります(ここで違和感を感じることができた方は、かなり設計力が高いです[後述])

ここまでMVCモデルについて、おさらいをしました。それでは、リアーキの世界に飛び込んでみます。

MVCアーキテクチャ内で変更を入れてみる

Railsの世界を破壊しないという話をしました。まずは、一切の破壊を許さずに、MVCアーキテクチャとして完結する変更を入れてみます。

Controllerの責務分離

まず、一番最初に解体できそうなControllerから責務を分離してみます。このケースにおける境界は、API ControllerView Controllerに分離します。フルスタックフレームワークのため、APIの利用とViewとして利用したいユースケースを想定します。

構成としてはroutes.rb/api用のパスをnamespaceで切り出します。また、app/controllers配下でfrontendapiのディレクトリを分けるなどが一般的です。

qiita_architecture_blog-advanced_mvcc.drawio.png

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内でテンプレートを生成できます。

qiita_architecture_blog-advanced_mvvcc.drawio.png

これにより、ControllerとViewで各責務を担保しつつ、リファクタリングを行うことができました。

Modelはどうする?

MVCモデル内でのModelの変更は少し複雑で、この後の話を聞いた方がわかりやすい、と思ったため後述に回しています。ぜひ最後まで読んでみてください。少なからず設計に対しての解像度が上がると思います🔥


ここからは、Rails Worldを飛び出した、本格的なリアーキの設計戦略について考察していきます。

ドメイン駆動設計(DDD)

DDDへのリアーキを考えます。繰り返しになりますが、設計方針はRails Worldを破壊しないことです。それに基づいて、DDDへのリアーキをデザインしてみます。

既存のMVCモデルを破壊せずに、DDDへ変更するには、MVCアーキテクチャにドメイン駆動設計を足し合わせる方法が有効そうです。簡単のため、関心ごと以外を全て排除して、以下のようにモデリングしてみます。

qiita_architecture_blog-mvc_to_ddd.drawio.png

上図では、もともと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パターンを採用しているみたいですね。

それはさておき、先ほどのアーキテクチャの詳細設計をしてみます。

qiita_architecture_blog-mvc_to_ddd_detail.drawio.png

Entityではドメイン駆動設計に基づき、ビジネスルールを定義していきます。ビジネスルールとは、言い換えると前提です。バリデーションのロジックをActiveRecordから拝借していますが、Entityでは存在のバリデーションを行うことで、主役となるドメインオブジェクトがどのような存在なのかを仮定しています。

またActiveRecordのメソッドを使用できるため(正確にはEntity内でActiveRecordの永続化に関するメソッドは使用しない)、存在の基本動作を定義できている状態です。

EntityとActiveRecordモデル(MVCにおけるModel)間での差分が少ないので、MVCからのリアーキにおいては、代替が簡単で相性のいいリアーキになります。

app/domains/entities/user.rb
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層アーキテクチャの責務分けに近い気がします。

app/services/hoge_business_service.rb
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たちからの評判は高いなと思います。そんなに気になってないので、買う気はあんまりないです。

ソフトウェアアーキテクチャの基礎

定番本中の定番本ですが、分厚いので、結構かいつまんで読んでいます。いかんせん分厚くて、表現も難しいので好きではないのですが、一番詳しく書かれているので、辞書みたいな使い方してますね。素読みできる方は本当に尊敬します。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?