書籍「ドメイン駆動設計入門」をKotlin+TDDで試してみた感想

Posted by user on Tuesday, May 12, 2020

TOC

はじめに

成瀬さんの書籍「ドメイン駆動設計入門」を読んで気づいた点、ポイントをまとめました。 自分用メモなので新たに気づいた点や忘れかけていた点を洗い出しています。 実装に関してはKotlinとTDDを初めて使ってみました。普段はJavaがメインですが、Kotlinを使ってみての感想も後ろにまとめてあります。

書籍を読んで気づいた点・ポイント

ドメイン駆動設計を選ぶ理由

ドメイン駆動設計は初期開発速度は劣るが、サービスの変化、開発途中における要件の変化などへの対応が柔軟。長期目線で見たときにドメイン駆動設計の優位性がある。

値オブジェクト

値オブジェクトの分割基準として、ドメイン内で分ける必要がある単位で分割する。ルールを作成する必要がある単位など、取り扱ううえでまとめる必要があるかがポイント。 細かくしすぎるとメンテナンス性が低くなる。 ある程度、不変性を保つことができる範囲で作成する。

値オブジェジェクトを使用することで使用法に強制力を持たせるため、バグ混入を防ぐことに繋がる。また、ロジックを1箇所に集めることになるため、メンテナンス性も高い。

エンティティ

DB設計時にER図でエンティティという名が用いられるがドメイン駆動では別の意味を表している。 エンティティには値オブジェクトにはないライフサイクルの概念が存在する。すなわちエンティティはシステム利用中に値が変化したり削除されたりする。

ドメインサービス

ドメイン駆動設計で表されるサービスはドメインサービスとアプリケーションサービスの2つ。明確に分ける必要がある。(エヴァンス本ではサービスは1括りである) ドメインサービスには値オブジェクトやエンティティに書くと不自然になるものを記述する。 値オブジェクトやエンティティと異なり自身のふるまいを変更する記述は書かれず、複数のドメインオブジェクト(エンティティ及び値オブジェクト)を横断して操作する処理を記述する。 エンティティに書くことができる処理をドメインサービスに書いてしまうとドメイン欠乏症に陥る。エンティティの責務をドメインサービスが奪わないようにする。 命名規則としては「ドメイン名 + Service」が良さそう。自分の知っているプラクティスではパッケージによってdomainやserviceを分けているが、本書ではXxxDomain.Services.XxxServiceのようにドメイン単位でサービスを分けてることを例に挙げている。

リポジトリ

リポジトリはドメイン駆動設計以外でも頻繁に目にする考え方。オブジェクトの永続化、再構築を行う。 重複確認existsメソッドはリポジトリではなくドメインサービスに実装すべき。何をもって重複とするかはドメインの責務。 永続化する単位で処理を行う。一部しかupdateしない場合でも永続化単位を引数として受け取る。 リポジトリクラスを切り替えることでインメモリのDBを使用し、テストを行う記述があったがコーディング量が多くなるため接続先DBを切り替えるだけの方が実用的な気がした。

アプリケーションサービス

ユースケースを実現するオブジェクト。メソッド1つがふるまい1つを表すイメージ。 アプリケーションの返り値がドメインの場合、プレゼンテーション層でドメインを操作される可能性がある。返り値は画面表示用のクラスにするなど、境界を保つことも検討する(クリーンアーキテクチャのイメージ)。開発コストや規模との兼ね合い。 updateのように何かしら命令を送る場合、引数を修正することを避けるためにcommandクラスというDTOを引数とすると疎結合になる。 凝集度を測る指標としてLCOMがある。インスタンス変数が全てのメソッドで使用されているかを基に測る指標。 インスタンス変数を使用しているメソッドが偏っている場合はクラス分割を検討する一因となる。 サービスとはクライアントのために何かを行うもの。自身のふるまいを持たない。 ドメインにおける活動をドメインサービス、アプリケーションとして成り立たせるためのサービスをアプリケーション・サービスといった形で領域を明確に分けする。

ファクトリ

IDの採番はどこの責務か。DBをの自動採番、ファクトリ、リポジトリの3つが挙がっているが、自動採番することを開発者内の暗黙の了解とするのが最適としている。 著者はリポジトリの責務は永続化と再生成が主と考えているためリポジトリ内の採番を推奨していない。。 ドメインオブジェクトがふるまいとして定義できるようであればあるドメインオブジェクトが他のドメインオブジェクトを生成するようにファクトリをメソッドとして定義することができる。

データの整合性を保つ

整合性を保つための手段として特定の技術基盤に依存することは避けるべき。DBに頼った場合、整合性の条件変更の対応が漏れる可能性がある(例えばテーブル内のユニーク条件が変化した場合、プログラム上の記載がなくDBのユニークキー設定変更は漏れやすい) アプリケーションサービスでトランザクション管理をすると、RDBに依存していることになる。JavaのSpring上の実現方法としては、AOPを実現する@Transactionalアノテーションを付与することでメソッド内のトランザクション整合性を担保することができる。

アプリケーションを1から組み立てる

集約

DomainのクラスをRepositoryで永続化に使用するDataClassに変換する際、Domainのフィールドを直接取得するのではなく通知オブジェクトを経由してDataClassを生成するパターンがある。Domainのフィールドに直接アクセスさせないための強制力がかなり強い。 Scalaだとフィールドのアクセス権限をInterface単位で設定でき、Interfaceを実装するクラスからのアクセスのみを許可することができる。 リポジトリは集約の単位で作成する。永続化の単位が集約であるため。 集約の単位でデータをロックしてしまうため、大きすぎる集約は厳禁。

仕様

仕様を全てDomainクラスに盛り込んでしまうと肥大化してしまい変化が容易ではなくなる。 仕様クラス(〜Specificationクラス)として括りだすことで扱いやすくなる。(リポジトリに仕様クラスの処理を移譲するとパフォーマンス上の問題もある) 検索に関わる複雑・特殊な条件の場合はリポジトリを使わない方法もとり得る。ドメインの防衛を第一に考えず、アプリケーションの猟奇はプレゼンテーション(利用者の利便性)を第一に考える。例えばドメイン内の操作でN+1問題が発生しそうだったりページング処理などがあれば1回のSQLクエリで取得できるようにするなど、例外的な対応も検討する。

アーキテクチャ

アーキテクチャはドメイン知識が流出しないように記述すべき箇所を示す方針。 レイヤードアーキテクチャはオブジェクトを4つの層(プレゼンテーション・アプリケーション・インフラストラクチャ・ドメイン)に分け、それぞれの層が責務を持つ。また、記載した4つの層の順に上位層から下位層へ依存が存在し、逆の依存は許されない。 ヘキサゴナルアーキテクチャはアプリケーションに対するその他のインターフェース・保存媒体を取り外しできるようにすることがコンセプト。インターフェースを利用して依存性逆転の原則を実現するため、レイヤードアーキテクチャとは依存関係が異なる。 クリーンアーキテクチャのコンセプトはヘキサゴナルアーキテクチャと同じ。ビジネスルールをカプセル化したモジュールを中心に据え、依存関係を制御する。加えて、具体的な実装方法を明示している。 アーキテクチャを適用することで開発方針が決まるため、システム開発中に考える問題を少なくすることができるため有用。

ドメイン駆動設計の扉を開こう

ユビキタス言語を使う。updateなのかchangeなのか、システマチックな言葉ではなくドメインエキスパートが使う言葉に合わせる。 同じ言葉でも視点が変わることにより必要な属性は異なる。境界づけられたコンテキストを適用し、別のエンティティを作る方が賢明。大規模になればなるほど同一のエンティティで全ての動きを賄うことは困難。

Kotlinで試し書きした感想

総じて書きやすかったです。Javaよりコード記述量が少なく、nullSafeな書き方ができる点がDDDに向いているなと思います。 nullSafeだとコンパイルエラーとしてnull混入を防ぐことができる点は本当に助かります。

  • nullSafeな言語は記述量がかなり少ない。DDDに向いている
  • 値オブジェクト書きやすい。プライマリコンストラクタが便利
  • safe call (?.)が便利。JavaScript(ES2020)ではOptionalChainingと呼ぶがKotlinは呼び方が違う。動きは同じ
  • ElvisOperator (?:)も便利
  • 今回使用していないが、SpringBootが使えるのは強み
  • ValueObjectやEntityObjectではsuperClassを作った方が良いかも

TDDプラクティスの感想

Kotlinと合わせてTDDも取り入れてみました。使用したライブラリはjUnit5です。 ちょうどDDD界隈で有名な松岡さんがTDDのライブコーディングをやっており、DDDと相性が良いとのことで取り入れてみました。
https://www.youtube.com/watch?v=UhHdnLTxOjE

使用感としてはDDDとTDDの相性がかなり良いように思いました。 DDD実装中に頻繁にコード修正(引数の型を変えたり、クラスを分割したり)したのですが、修正漏れがないかの確認に使えました。 これで大丈夫だろうという自分の感覚があっても、テスト全実行後にコケていて修正漏れに気づいたり、想像以上にTDDの恩恵を受けました。 単体テストの実装コストは多少かかるが、規模が大きくなればなるほど後々の恩恵はかなり大きいものになりそうな予感です。 想像以上に便利だったので今後もTDDは取り入れてみようと思いました。

最後に

今参画している現場でもDDDを取り入れているが、書籍による気付きは多かったです。 DDDにおける細かい箇所においては現場によって使う言葉が異なるのかなとも思いました。 例えばServiceの細分類やEntityという言葉の使用有無が今の現場と書籍で異なったので読んでいて違和感がありました。 今の現場ではDomainServiceという名前はなく、Domainsのような言葉を使い、Entityという言葉は使わずDomainという言葉を使っています。 DDD界隈でも色々な派閥があるかと思いますが、今の現場ではValueObjectを使わない、DB依存を許容するというスタイルで取り組んでいるためです。 現場によって様々な背景があり、DDDプラクティスの取り入れ可能な範囲や取り入れ方も異なるかと思います。 その中で、DDDの本来の目的、各プラクティスの目的や導入時のメリットを正確に理解し、取捨選択しているモノは何かに気づくために本書籍は有益でした。 パッケージ構成についての説明・理由も細かく書いてあったのは特に有り難かったです。

書籍もKotlinもTDDも初めて試しましたが残念な結果にならなくて良かったです。 作りたいところだけ作って整理してないけど、実装サンプルとしてGitHubに公開しました。
https://github.com/somurieengieer/kotlinDDDTraining

ちなみに本を読みながら調べていたらIDDDをまとめた以下サイトが気になった。少し時間を空けてから読んでみよう。
https://codezine.jp/article/corner/655