DDDを使ってRailsアプリをリファクタリング

  • 90
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

経緯

casyというインターネットを使って手軽に家事代行を頼むことができるサービスのプログラマをしています。
Webだけでなく、スマホアプリも出すことにあたり、Webアプリサーバ(Rails)から機能を切り出し、APIサーバ(Rails)を別途作成し、Webアプリの場合はWebアプリサーバからAPIサーバを呼び出し、アプリからは直接APIサーバを呼び出すような仕組みにしました。
ただ、全部の機能をAPIサーバに移すのは容易なことではなかったため、いくつかの機能はまだWebアプリサーバに残っていて、アプリよりもWebのほうが機能が多い状態となっています。
今回残りの機能をAPIサーバに持ってくるにあたり、下記2つのアプローチがありました。
1. 既存のソースコードからViewを切り離してほぼそのまま持ってくる
2. 設計を見直し、大幅にリファクタする
チーム内で議論した結果、スタートアップといえども、今後を見据えて、基盤となるAPIサーバのプログラムはコストをかけてでも綺麗にしようということになり2をとることになりました。
その際にリファクタの指針となるものが必要となり、いいと言われ続けているDDDを導入することになりました。

DDDを導入してみた感想

やっぱり難しいです。
「エリック・エヴァンスのドメイン駆動設計」と「実践ドメイン駆動設計」をテキストにして、ここ1ヶ月ちょっとで移植する機能のいくつかを設計、実装しました。
正直思っていた倍ぐらいの時間がかかりました。
ユビキタス言語をつくって、コンテキスト図を作ってレビューするところまでは、すでにある機能で自分自身もサービスをよく知っているので、すんなりといきました。
ただ、そこから実装となった時にすごく悩みました。
テキストにした2冊は、難解なうえに長いので、正直把握できたと思えるレベルにまでいけませんでした。
また、Railsの作りとDDDの作りは違うため、どこまでDDDの通りに従うべきかに対して悩み続けてました。
もし完全に従うとしたら、なんのためにRailsを使っているのかよくわからない状態のものが出来てしまいますし、とはいえ、中途半場に従うと移植前と変わらない複雑なものができてしまいそうです。
周りに納得してもらえるような実装がいつできるのか、いやそもそもそれが私に可能なのか不安になってきました。
アルゴリズムはわかっているのに、実装がすすめられないという今までにない経験でした。
DDDの思想である複雑なものを単純にさせるということの難しさを思い知りました。
ただ、1つの機能が実装し終えた時に手応えを感じました。
どこにどの機能が実装されているのかを完璧に頭の中に把握することができ、次に取り掛かる今回とちょっとちがった機能に対して、エンティティの取得を変更するだけで、コアドメインをそのまま使いまわせることがわかったからです。
実際その通りにでき、出来上がったプログラムもエンティティの取得のメソッドが違うだけなので、一目でどこの実装が違うのかがわかり、とても見やすいものができました。
レビューでもすごく読みやすくなったと言ってもらえました。ただ、書くことに関しては、書けるかどうか不安とのコメントももらいました。
結論としては、DDDでいきつつも、Railsの標準的な書き方で問題ないところは、標準的な書き方でいくこととなりました。

どんな感じで設計、実装していったか?

Directory構成

app/applications アプリケーションサービス
app/entities エンティティ
app/values  値オブジェクト
app/services ドメインサービス
app/repositories レポジトリ
app/factories ファクトリ
app/models ActiveRecord

コンテキストマッピングの作成

主要なエンティティやメインサービスを記述し、ルートエンティティやコンテキストがわかるような図を作成することで、誰がどんな役割を持つのかを明確化。
コンテキストの分離は必ず意識します。例えば注文の成立はスタッフのスケジュールが空いていることが必須とはいえ、コンテキストが違います。予め提案のエンティティを取得する際にスタッフのスケジュールが大丈夫かどうかの制約を課すことで、注文の成立というコアドメインには、合意書の作成、課金という重要な事柄だけを記述することができます。
スポット約定.png

コントローラの実装

コントローラはDDDでいうUI層に当たると考え、コントローラは適切なアプリケーションサービスを呼び出しその結果を返すだけの存在。
テストを書くのがアプリケーションサービスに対して書けばいいので簡単になりました。

アプリケーションサービスの実装

ルートエンティティをレポジトリから取得して、そのルートエンティティにメッセージを送ったり、ドメインサービスを呼び出した後、エンティをレポジトリに保存します。
アプリケーションサービスは出来るだけ薄くして、ビジネスロジックは入れないようにします。

レポジトリ、ファクトリの実装

エンティティはActiveRecordのオブジェクトと同じとは限りません。レポジトリを経由することで、テーブル定義にひきずられずに、コアドメインの処理を実行する際に理想となるルートエンティティを作ることができます。
レポジトリ内でActiveRecordをつかって取り出したデータをファクトリを使って、ルートエンティティやそれを構成するエンティティや値オブジェクトを作成します。

エンティティやサービスの実装

コンテキストマッピングに合わせたエンティティやサービスを作成することで、どのモデルがどういうことをするのかが把握できます。
DDDの集約を意識して、ルートエンティティ配下のエンティティや値オブジェクトはルートエンティティ経由しか呼び出せないようにします。内部的にはルートエンティティはエンティティに処理を委譲します。

ドメインイベント

ドメイン処理終了時にを識別できる識別子とデータを組み合わせたイベントを発行します。メール送信やpush通知などのサブドメインはそのイベントをsuscribeしており、該当のイベントが来た時に処理することで、コアドメインはすっきりさせたままにすることができます。

まとめ

DDDの本を読むと"染み出さない"という言葉がしょっちゅう出てきます。
それはアプリケーション、ドメインモデル、インフラといったレイヤの切り離すことだったり、ドメインのコンテキスト境界をはっきりさせることを指しています。染み出さないことで、影響範囲が限定され、どこになにがあるかわかりやすくなります。重要なコアドメインが抽象度の高い状態で保たれることで、競争力の高いアプリを維持できます。

ただそれは、そのためのコストを払う必要があります。
今までは無駄がなく、バグがないプログラムであればOKであったところを、実装の前にこの機能において、何がコアドメインか、各処理はどこのドメインにいるべきか等をじっくり考え、チーム内で議論する必要があります。
せっかく設計に時間をかけるので、やはり型をもった言語を使いたくなります。

DDDで出来上がったものを見ると、APIサーバなのでViewもないし、ActiveRecordをそんなに活用しないので、Railsじゃなくてもいいのでは?と感じますが。チームの共通項がRailsな上、既存のAPIの実装やテストが活かせる点が大きいです。
DDDによりフレームワークへの依存が減り、コンテキスト境界で実装も疎結合になっているので、処理速度やスケールが必要になった際はMicroServiceとして切り出すことも検討する価値があると思います。