内なる秩序の探求〜テスト駆動開発をやめて、なお残すべき習慣とは(7)

「何を作るか」の外側のループの次は、「どう作るか」の内側のループの秩序を探求しなければなりません。それには何が必要でしょうか?

Eiji Ienaga
時を超えたプログラミングの道

--

はじめに

前回は、外側のテストループについて何を作ればよいかの探求について解説しました。今回は、実際に「どう作ればよいか」について、コードや設計の内側の秩序の探求について解説します。

TDDの肝は、動作するきれいなコードを目標に、テスト書いて、 実装して、学んで、リファクタリングして… を小さく繰り返し、内なるコードや設計の秩序化するステップを踏み続けることにあります。では、「動作するきれいなコード」と呼ばれる目指すべき場所は何を頼りに向かえば良いのでしょうか?

プログラミングにはコードや設計の秩序化を図るための定石が幾つか知られています。例えば、UNIXの設計判断(例:一つのプログラムには一つのことをうまくやらせる)、メタファ、名前重要、DRY原則、SOLID原則、KISSの法則、コマンドクエリの分離原則、契約による設計オブジェクト指向のイディオム関数型のイディオム各種の言語やフレームワークのイディオムリーダブルコードで紹介されるイディオム、デザインパターン、etc…

本記事では、対象ドメインの理解を深掘りし動作するきれいなコードを探索するため、ドメイン駆動設計(DDD)のパターンの一部を駆け足で紹介します。

副作用のない関数(Side-Effect-Free Functions)

副作用のないコードは入出力結果が推測しやすくテストもしやすいです。また、副作用のない関数を組み合わせても動作が推測しやすく、見通しが良くなります。

副作用のない関数は、関数を状態変更するものと参照するを分離する(コマンドとクエリの分離原則)ほかに、次に紹介するバリューオブジェクトと合わせて利用します。

バリューオブジェクト(Value Object)

{message:string} => Message
{x:number, y:number}=>Point
{red:number, green:number, blue:number}=>Color
{street:string, zip_code:number}=>Address
{currency:string, amount:number} => Money

string, int, mapなどプログラミング言語がデフォルトで提供する属性や型をそのまま使うのではなく、対象ドメインの語彙で名前をつけてバリューオブジェクトとして括りだします。後述のエンティティからバリューオブジェクトを切り出すことができれば、エンティティの複雑さを軽減することができます。バリューオブジェクトは状態不変で副作用なしにして、メソッドを組み合わせて使っても動作が推測しやすくしておきます。バリューオブジェクトは後述のエンティティと違って、一意性の保証は不要です。

書籍『テスト駆動開発』の1部では、Moneyクラスの計算を、副作用ありのコードから、 バリューオブジェクトの副作用なしのコードへとリファクタリングし、お金の計算ロジックの見通しを良くする様子が描かれています。

書籍『ドメイン駆動設計』の3部の“より深い洞察へ向かうリファクタリング”の中で、ドメインエキスパートとの対話を通じてビジネス上の計算ロジックの基本骨格となるシェアパイの発見が描かれていますが、シェアパイはバリューオブジェクトで表現されています。

エンティティ(Entity)

エンティティはオブジェクトの一意性を保証します。小粒のエンティティ単位のままでは、ビジネス上の不変条件の整理できない場合がありますが、そのときは後述のアグリゲートでエンティティ郡を纏めます。エンティティを永続化するや問い合わせるには後述のリポジトリを利用します。

アグリゲート(Aggregate)、アグリゲートルート

オブジェクトライフサイクルに着目し、小粒のエンティティではなく纏まりで整合性を保ちたい場合に、アグリゲート(集約)を活用します。アグリゲートの取りまとめ役のエンティティはアグリゲートルートと呼ばれます。ビジネス上の満たすべき不変条件を明瞭にしたい場合、エンティティの他にアグリゲート単位で整理すると良いでしょう。アグリゲートを永続化するやアクセスするには、後述のリポジトリを活用します。

リポジトリ(Repository)

エンティティの永続化や問い合わせのためにリポジトリを用意します。アグリゲートを用意した場合は、アグリゲートルートに対してリポジトリを用意し、纏めて整合性を保って永続化やアクセスできるようにします。

Railsなどのアクティブレコードパターンを使っているフレームワークの場合、リポジトリを別途用意してしまうは、煩雑になってフレームワークの良さが得られないので、活用しないでしょう。

ポリシー(Policy)

ポリシーはデザインパターンでいうところのストラテジーに相当するものです。ある条件でのビジネス上の計算ロジックをエンティティやサービス内に埋もれさせるのではなく、ポリシーとして明示的に括りだし対象ドメインの語彙を使って名前をつけて理解しやすく、計算ロジックの変更の際も修正しやすくしておきます。

サービス(Service)

先述のエンティティやバリューオブジェクトやポリシーなどにビジネスロジックを配置するのが収まりが悪いものはサービスに配置します。サービスには状態を保持しないようにしておきます。

境界づけられたコンテキスト(Bounded Context)

多人数が参加するプロジェクトで一枚岩の大統一のモデルを整理して開発するには、理解するコストも調整コストも高くついてしまいます。同音異義語があるとさらに混乱してしまいます。対策としてドメインの境界を区切ってコンテキストを明らかにし個別に整理すれば、理解しやすく、独立して作業が進められます。

マイクロサービス化を積極的に行っているのであれば、技術的な理由の単位の分離のほか、境界づくれらたコンテキストの単位でデプロイ要素もチームも分離し整理することになるでしょう。

CQRS

CRUDの統一的なモデルでは複雑になってしまう場合があります。そのときは、更新系と参照系のメソッドを分けるだけでなく、モデル自体もWriteモデルとReadモデルを分離して整理する方法があります。大量の参照のリクエストを捌ける仕組みにしたいの理由で、参照系のコンポーネントを分離する場合こともあるでしょう。

ドメインイベント(Domain Event )

対象ドメインのビジネスの整理方法として、CRUDでアグリゲートの状態変更に着目する代わりに、ビジネス上で起きている主要イベントに着目して記録するアプローチがあります。会計系や在庫管理系のシステムに携わっているのであれば、既に似たアプローチを実施しているかもしれません。ドメインイベントのログが記録できれば、ログから最新状態のアグリゲートもある過去の時点の状態のアグリゲートも組み立てることが可能です。ドメインイベントはイベントソーシングやCQRSと合わせて利用されます。

分散システム前提で、パフォーマンスを劣化させずに、分散に配置されたアグリゲートの整合性を保つ仕組みを作りたい、けれど分散トランザクションではパフォーマンスが期待できないの理由からドメインイベント&イベントソーシングの結果整合性の仕組みを入れる場合もあるかもしれません。(イベントソーシング採用による副作用の即座にデータが反映されないや仕組みの複雑さは許容する。)

まとめ

TDDの目標となる「動作するきれいなコード」と呼ばれる目指すべき場所のヒントになるDDDのパターンを駆け足で紹介しました。

ただし、初めからDDDパターンに沿って事前設計で対象ドメインが構造化されるわけでもありません。イテレーションのサイクルを繰り返すなかで、または、TDDのサイクルを繰り返すなかで、学びを得てコードや設計の秩序化をすすめていきます。

アグリゲートは見落としがちです。アグリゲート単位で、オブジェクトの不変条件の整合性を保つことに注目して、対象ドメインのコードや設計の秩序化を図るのはおすすめです。

紹介したパターンはすべて使う必要はありません。対象の複雑さがCRUDモデルで十分立ち向かえ、パフォーマンス要件も満たせるのであれば、CQRSもドメインイベントもイベントソーシングも使うことはないでしょう。対象が十分小さいのであれば境界づけられたコンテキストも区切りません。

パターンはいつでも使うのではなく、現状のコードや設計に不安や居心地の悪さや痛みを感じたときに、適切なもの選びます。感情に向き合うリファクタリングは、TDDのテーマの1つです。例えば、開発をすすめていく上で、CRUDの統一エンティティのモデルでは複雑過ぎるとチームメンバーが実感したときに、CQRSでWriteモデルとReadモデルの分離して課題が解消するか試してみます。効果があれば続け、効果がないなら戻します。

たとえTDDをやめたとしてもパターンやイディオムを参考に、だれでも読めて自信を持って修正し続けられるようにコードや設計の内なる秩序を探求し続けることは欠かせません。内なる秩序の探求には、DDD以外のパターンやイディオムも参考になります。

次回以降は、新訳の書籍テスト駆動開発を参考にTDD自体に触れていきます。

--

--