エクスキューズ
これは社内向けの発表なので、フェーズやメンバーのスキル想定が大幅に異なる可能性があります。
Evans本、IDDD、ボトムアップ本ぐらいしか読んでないので、あくまで現時点での感想として捉えてください。
想定読者
- 1チーム10人程度のRails
- 全員がスキルフルというわけではなく、インフラ・フロントの分野でも課題が残っている
- 規模感としてテーブルは200~300ぐらいあってまぁまぁ複雑
- レビューでServiceクラスにドメインの知識を流出してしまっています、と言われたがドメインの定義って何?
- QiitaなどでDDDについて書かれた記事を見かけることはあるが、謎の用語が多すぎてよくわからない
結論
そんなに優先度高くない
理由
- 他に勉強しないといけないことがいっぱいある
- クラウドインフラやIaC
- フロントエンド周りのあれこれ
- Ruby, Railsのバージョンアップ
- 前提としている知識が多い
- SOLID原則などのソフトウェア工学
- オブジェクト指向についての理解
- デザインパターン(GoFやPoEAA)
- モデリング、分析の技法
- DDDやIDDDはJava(Spring, Struts), C#(.NET)を前提にしていて、サンプルコードに実感が湧かない
- もはや古典DDDとかって呼ばれていて、今だと関数型DDDとかが話題になったり
- まだ色々読んでる途中なのでこれだけ読んどけばいいかな~ってもの見つけたら紹介します
それでもこの文書がある理由
DDDを知らなくても十分開発できるとはいえ、オブジェクト指向、関数型と同じようにパラダイムとして染み出してきているので、備えよう
Rubyでブロックを使ってeachを書いて関数型の恩恵を受けているのと同じように、ドメインって大事だよねみたいな考え方も何となく定着している。
たとえば下記はどっちが良いだろう?
# CaseA:
post.set_status(Post::STATES[:published])
post.save!
# CaseB:
post.publish!
具体的に活かせそうなポイント
- クラス図、アクションなどからドキュメントを作り、チーム全員に共有する
- Serviceクラスに対する解像度を高める
- 組み込みのValueObjectを使う
1, 2さえきちんとおさえてくれればOK! 3はちょっと賛否あると思う。
そもそもDDDとは何か
対象とする事業領域の複雑さに耐え、変化に対して素早く対応できるシステムを構築できれば、自ずと競争優位性が高まるので、ソフトウェア開発で一番大事なのは、ドメインへの理解をどれだけソフトウェアに反映させられるか
という哲学のもと構築された理論
単語の説明
- ドメインとは
- ソフトウェアを使って問題解決しようとする領域
- ドメインモデルとは
- ドメインの問題を解決するために、物事の特定の側面を抽象化したもの
- これは何ではないか
- 「対象をそのままうつしとったもの」ではない
- ドメインモデルは何なのか
- 「こういうの作ったらドメインの問題解決するんじゃない?」
という解決案- 実際に試してみないと本当に役立つかはわからない
- こまめにフィードバックサイクルを回し、改善していくことが重要
引用元: https://little-hand-s.notion.site/b97c1a305aa24040a63a61b3cd863e55
動画: https://www.youtube.com/watch?v=Upeg6cNOirc
そしてそのためにEvans本では大体以下のようなことを言っている
- ドメインモデルについてエンジニアとエキスパートが同じ言語で話せるようにせよ
- コミュニケーションの障壁をなくしてイテレーションを素早く回す
- ドメインモデルを純粋に書ける設計にせよ
- レイヤーを分けて、ドメイン由来の複雑さとFWやInfraに依存する複雑さを別々に管理できるようにする
ユビキタス言語とか境界づけられたコンテキストの話は1
リポジトリとかエンティティとかの話は2
DDDは設計とプロセスの両面を扱うはずなのだが、DDDというと2の話が取り上げられがち。
なぜならエンジニアサイドで完結できるし、サンプルコードも用意できて話がしやすい。
巷でいう軽量DDDは2のみをやっているという認識でOK。
逆に1の方はケースバイケースだし、書籍で取り上げられてるパターンはあまり直感的な名前じゃない。
例えば進化する秩序
というパターンがあるが、これはアーキテクチャに銀の弾丸を求めて最初から全てそれで対応させるのではなく、局所では最適な実装パターンを選ぼうねっていう話なんだけど、字面からはあんまり伝わってこない。
両方を回していくことが大事なので、Railsらしさを損なわないようバランスよく少しずつ取り入れていきたい
具体的に活かせそうなポイント
クラス図、アクションなどからドキュメント(図)を作り、チーム全員に共有する
これが一番大事
現状の運用は下記のようになっている
- 大きめのIssueでも議論の履歴だけあって、ドキュメントは作ったり作らなかったり
- あったとしてもエンジニアが読むことを前提としている
ドメインモデルについてエンジニアとエキスパートが同じ言語で話せるようにせよ
これを達成するために技術と切り離したモデルを共有する、っていうのがDDDのキモだが、小規模チーム(弊社)ならそこまでする必要はないのでは
- ビジネスサイドへの実装のフィードバックがあればよい
- テーブル構造や正規化に対する理解があるので、違和感があれば指摘してもらえる
- なぜ変更に時間がかかるのかということや、次依頼する修正の見積もりができるようになる
- ビジネスサイドもSQLを駆使して自分でデータを取れるようになっておきたい
- 無理にデータ構造などを隠す必要がない
- テーブルとモデルを1:1で対応させていること(制約)はむしろメリットになりうる
- Actionもほぼそのままユースケースになっているはずなので、routesの変更を並べるだけで大まかな機能がつかめる
アクションの一覧があれば、何ができるかはだいたいわかるし
クラス図があればデータ構造と何ができなさそうか、がわかるというRailsの良さを活かしたい
ポイント
- ビジネスサイドでわかるぐらいの詳細にとどめておくこと
- 結局それが初めて見るエンジニアにとっても丁度いい粒度になっているはず
- すべての情報を1つの図に収めようとしない
- 自分なりの何となくの型は用意しておくが、フォーマットは定めずすべてを使い捨てにする
- 全部終わってから書くと大変なので、実装しながらメモを書いておく
- 詳細への質問がしたくなったり、実装が終わったタイミングで共有
Serviceクラスに対する解像度を高める
基本下記の記事を読んでおけばOK!
Railsでサービスクラスを書く時に知っておきたいこと #Rails - Qiita
巷で使われるServiceクラス、に対して下記の解釈がありうる
- DDD:Application Service
- DDD:Domain Service
- DDD: Infrastructure Service
- トランザクションスクリプト
- PofEAA:サービス
Rails上でServiceを作ろうとするとき、自分が書こうとしているのはどの層なのかを意識すると依存関係の方針が見えてきたり、大きいServiceクラスが誕生するのを防げる
DDDのレイヤ分けの一例
- Application Service: 入力チェック、ドメイン層への指示出しとトランザクションの管理
- Infrastructure Service: メールやDBへのアクセスなどを簡易に操作できるように提供
- Domain Service: Entity, Value Objectとなじまない、操作というべきもの
色々細かいところにツッコミどころ(同心円がどうのこうの)はあるかもしれないが、大体こんな感じのイメージでいい
ポイントはDomain層を再下層において、FWやInfraの影響を受けないようにしているということ
RailsでServiceクラスを作るか…ってなったとき初期段階ではApplication Serviceだったのに、いつの間にかDomain Serviceっぽいものができていたり、Infrastructure Serviceができていて、Service同士の依存とかが発生したりmodelsからドメインに関する知識が流出してしまっていたりする。
ポイント
-
Railsにおいても、FWやCSに由来する複雑さとドメインの問題に関する複雑さを分けておいたほうがよい
- なので、ドメインに関する知識はなるべくmodelsディレクトリに突っ込めるように(namespaceも使えるといいね)
-
ControllerとModelの間に挟まるのは、大体Application Serviceなので、もうServiceという名前を使うならこれにする
- つまり入力チェック、ドメイン層への指示出しとトランザクションの管理が責務
- (ActiveRecord内のメソッドでtransactionブロック貼るとかもやめる)
- 共通化したければDomainServiceとして切り出すか、
services/modules
に何か作る- Concernはやめておく
- つまり入力チェック、ドメイン層への指示出しとトランザクションの管理が責務
-
DomainServiceは無理やり名前をつけるとか、イベントモデリングで対応
- texta.fmで触れられているので聞くと良い
-
InfrastructureServiceも意識できるとよい
- Railsはモデルとインフラが癒着しているといわれているが、あくまでDBとだけなので、例えば他のサービスとの通信やプッシュ通知をモデルとなるべく分ける
- とはいえUserNotificationみたいな、明らかに何らかのインフラと結びつきがあるだろ、みたいなクラスまで分ける必要はない(気がする)
- ActionMailerの呼び出しもActiveRecordに書かない
- 結局真面目に書こうとすると、失敗の頻度もロールバックへの影響もDBの更新処理とは違うしActiveRecord内のメソッドが長くなる原因になるのでApplicationServiceに置く
- Railsはモデルとインフラが癒着しているといわれているが、あくまでDBとだけなので、例えば他のサービスとの通信やプッシュ通知をモデルとなるべく分ける
-
もし次新しいプロジェクトを始めるなら、Serviceという曖昧な単語はやめてフルネームで呼ぶか言い換える
- ApplicationService => UseCase
- InfrastructureService => ~Client
組み込みのValueObjectを使う
ファットモデルを分割する方法として、has_oneのレコードを積極的に使っていってもいいんじゃないかみたいな話があるが、その手前にValueObjectの組み込みがあっていい
ValueObjectってなに?
DDDのモデルを表現するパターンは主に3つ
- Entity(≒モノ): 同一性がある
- ValueObject(≒概念): 同一性がない
- Domain Service(≒操作): EntityにもValueObjectにもなじまないもの
- 例: 注文
Order: Entity, DeliveryAddress: Value Object - 例: 口座振替
BankAccount: Entity, Money: Value Object, BankAccountTransfer: Domain Service
同一性がキーワード。
Railsだとfind(id)
で探すようなやつがEntity。
それがそのものであることに意味があるので追跡できるようにしていなければならない
なぜEntityとValueObjectを分ける必要があるのか
Entityの追跡をやめることによって楽になる部分がある
- has_manyのレコードを更新するときに1つずつupdateとかじゃなくて全置換してもよい、とか
- オブジェクトが不変性を持てるようになる
エンティティは所有するオブジェクトの操作を調整することによって責務を果たすようになる
要するにモデルの中でも扱いに注意が必要なEntityとある程度雑に扱えるValue Objectを分けることで複雑さを減らせと言っている。
RailsではすべてがEntity扱いになってしまうので、クラスにコメントをつけたりすることで本当に
エンティティから余計な属性やふるまいを他のオブジェクトに移動するなどで削ぎ落とし、その概念にとって本質的なふるまいと、そのふるまいが必要とする属性だけにすること
つまり、Entityは属性に依存しない同一性を持つ存在なのだから、多くの属性が交換可能
EntityがValueオブジェクトに命じることで処理が行われるようにしたい
ValueObjectが備えるべき性質
[must]
- 不変性: 一度作成されたオブジェクトの属性が変更されない
- 完全性: 不正なオブジェクトが生成されない
[better]
- 閉じた操作: 戻り値の型が引数の型と同じにできる場合はそのように操作を定義すること
- 独立したクラス:
- メソッドにその値オブジェクト以外のクラスのオブジェクトを渡すと依存関係が発生するので避ける
- 仕方ないときはプリミティブ型を直接渡すとかする
ValueObjectを使ったときのメリット・デメリット
メリット
- 複数の属性がまとまって1つのクラスになるので、見通しがよくなる
- 不変条件を強制しやすい
- テーブル的には1つなのでパフォーマンスに影響を与えにくい
- スペシャルケース(Null Userみたいなやつ)を定義して使うこともできる
デメリット
- トランザクションの競合に気をつける必要がある
- あんまり活用されておらず知見が少ない
- validationどうするの?とかまだ調べられていない
具体的な実装は下記の記事とかを参考に
Rails: Value Objectで「基本データ型への執着」と戦う(翻訳)|TechRacho by BPS株式会社
PoEAAのValue ObjectとDDDのValue Objectは違う、みたいな話もあるが、一旦あんまり気にしなくていい。
eql?を実装していないといけないとか、まぁ定義からいうとそうなんだろうけど、比較使わないこともあるし、とりあえず不変性、完全性のほうが便利に使う上では大事かなというスタンス
おわりに
Rails way大好きなので、ほぼ崩さないような形でDDDを取り入れる方法を考えてみました。
別にDDDからじゃなくても、チーム開発、ソフトウェア工学的なところからも導き出せるようなことではあるが、何かに活かそうと思って読んでないと耐えられなかったので…
DDDの前にデータモデリングの技法を学んだほうがいいかも。