🐖

DDDについて喋りたい ~Clean Architectureの実装例を添えて

2023/05/07に公開

これはなに

いままで実務でドメイン駆動設計(DDD)に触れてきて、意識してきたことや違和感などをつらつらまとめます。

Clean Architectureの具体的な実装例も絡めて、こうしたらうまく回りましたよというところをお伝えできれば幸いです。

ちなみにアカデミックな内容ではなく、実務でどのように使ってきたかということに傾倒するため、一般的なDDDやClean Architectureの手法からは乖離がある場合があります。

アウトラインをなぞったふりをしている、DDDやClean Architectureのばったものと考えていただいたほうが適切かもしれません。

どうすればDDDになるかではなくなぜDDDを使うのか

DDDの参考書である エリック・エヴァンスのドメイン駆動設計に DDDの根本的な原則として以下のように書かれています。

  1. コアドメインに集中すること
  2. ドメインの実施者とソフトウェアの実施者による創造的な共同作業を通じてモデルを探求すること
  3. 明示的な境界づけられたコンテキストの内部でユビキタス言語を語ること

上記はDDDの重要性を表しているのですが、その理解が揃わない状態で、DDDを取り入れることに注力してしまい、なぜDDDが重要なのかをという目的意識をすっ飛ばして、いかにDDDの手法を正しく使っているかに意識が傾倒してしまっている組織がいままで多かった印象です。

ぶっちゃけ言い回しが遠回しすぎて、いまだに言わんとしてることがわかっているわけではないですが、
ドメイン駆動設計は総じて、非開発者を含めたプロダクト開発に関わる人全員が共通認識を持つためのコミュニケーション手法だと思ってます。

どうすればDDDになるかではなく、なぜ使うかでアプローチすべきです。

筆者がドメインをどう捉えているか

あとの文章を書きやすくするため、使う用語を定義します。
あくまでこの記事の範囲の用語で、一般的な意味合いとは異なる可能性があります。

  • ”ドメイン”とはサービス、プロダクトを構成する「要素」と捉えます(データベースはドメインではない)
  • ”ドメインが持つ状態”は「属性」と捉えます
  • ”ドメインの状態が変化する行動”は「振る舞い」と捉え、ユーザーまたはシステムがとる行動のまとまりと捉えます。

上記の「要素」、「属性」、「振る舞い」の3つでドメイン会話できると思ってます。

また、「開発者」はシステム開発におけるプログラミングするメンバーを指し、「非開発者」はプロダクトマネージャーやデザイナー、営業、CS等のビジネスサイド等のプログラミングしないメンバーを指します。

構成している要素を関係者で認識合わせする

ドメインについて共通認識を関係者と持つためにドメインの関連図を書くことをおすすめします。

実装のための設計ではなく、共通認識を関係者全員で持つことが目的です。
関係者にはプロダクトマネージャーやデザイナー、営業、CS等のビジネスサイドなど、実際に開発に関わらない非開発者も含みます。

よくクラス図っぽい図を書く方がいますが、すり合わせしながらその場で書き直すために、荒く各ドメインの繋がりを表すだけにすると良いと思います。(ドメイン同士の1対0、1対N、N対N等の関連性をまとめる程度)

  1. ドメインの実施者とソフトウェアの実施者による創造的な共同作業を通じてモデルを探求すること
    のドメイン実施者とはソフトウェアの実施者の対義語であるため、非開発者と解釈できます。

ドメイン関連図の作成にはシステム的な用語は廃し、開発者と非開発者が共通認識の持てる粒度で会話するべきです。

以下は私が個人的に妻と買い物管理アプリを開発した際に書いた実際のドメイン設計図です。(かなり汚く読みづらいですが)

妻はセールスエンジニア、またはカスタマーサポート相当の仕事に従事しており、システム会社に勤務はしていますが、開発するエンジニアではないため、システムの設計業務に携わったことはありません。

アプリ開発に当たり、要件を詰めるために設計にはいる前のタイミングで、3時間ぐらいこちらの図を使って会話しました。

その上で実装したAPI周りのディレクトリ構成がこちらです。

アプリを開発してから1~2年、妻からの要望を聞きながらカスタマイズし続けていますが、大きくこの関連図が崩れたことはありません。何より、ドメイン関連図で共通認識を妻と私でもっているため、妻からちゃぶ台返しのような要望がでて来ないことが大きいと思いっています。(途中でNoSQLからElasticSearchに乗せ換えるような派手なアーキテクチャ変更はやっています)

プライベートだけでなく、仕事でも複数のドメインを追加および廃止する、システムへの大きなカスタマイズするプロジェクトに取り組む際に、現状とカスタマイズ後のドメイン関連図を作って認識合わせしましたが、企画、開発サイドに大きな混乱なくプロジェクトを終え、かつ以後のシステムに関する認識合わせもスムーズに進むようになりました。

図の範囲の解像度で全員が会話できるようになるため、あるドメインに対して変更を加える際に、関連ドメインの影響を非開発者から確認してくれる例もありました。

ドメイン関連図を使っての会話はシステム開発において、非開発者が踏み込んで来れる範囲を広げ、かつ、建設的で無茶振りが起きにくい関係性を構築する効果があります。

作っているものの価値感を揃える

1.コアドメインに集中すること
を突き詰めると、関係者がサービスやプロダクトの価値観を揃えることではないかと思っています。

関係者で価値となるドメインはなにか、認識合わせすることは重要です。

システム開発の方向性はプロダクトマネージャーやデザイナー、ビジネスサイドなど、実際に開発に関わらない非開発者がプロダクトやサービスの方向性の決定権を持っていることも多いと思います。
非開発者の判断基準は開発者と異なり、追加開発や新規開発などに傾倒し、ユーザーに直接的に価値を産まない例えばセキュリティー対策やリファクタリング等の判断が先送りになることなどは往々にして起こります。

価値が生まれないため、対応範囲が広いと実施に踏み切る判断はしづらいと思われます。(そして開発が申請するこの手の対策は大抵大規模です)
そのため、何かが起こってから対策することが多いと思います。

また、開発者も何が価値か十分に理解してないことで、ちょっとした変更でユーザーの体験が向上する事があったとしても、ユーザーに近い非開発者から具体的な要望が出てこないと対応ができないということも起こります。
非開発者から見ると、開発者が何ができるかわからないため、結果対応されないままお見合いしてしまいます。

重要なドメインと、ユーザーに対する理想的な状態を定義し、それを共有することで、阻害する防止策や能動的な提案が生まれ上記問題の解決の足がかりとなります。
また、セキュリティー対策やリファクタリングを小規模で実施する際の優先順位を考える材料にもなります。

専門用語と立場を超えた共通の用語を住み分ける

  1. 明示的な境界づけられたコンテキストの内部でユビキタス言語を語ること
    についてユビキタス言語はよく聞くようになってきましたが、開発者だけ、または非開発者だけで定義しているところを散見します。

開発者同士、非開発者同士で言語統一することはそれはそれで必要で否定はしませんが、それはあくまで同一領域内の言語統一であって、ユビキタス言語とは呼ばないと思われます。

エヴァンズ本においてユビキタス言語は「開発者とユーザーの共通の厳格な意味を持つ用語」と定義されています。言い換えれば開発者と非開発者の会話に使う共通言語ともいえ、逆に言えば、開発者と非開発者の会話に使わない用語はユビキタス言語ではないと考えられます。

開発サイド、及びビジネスサイドには共に専門用語があり、同じ役職同士であれば会話ができると思いますが、他領域の関係者と話すときに使うと意味が通じず思考が停止する原因になり、本来話したいことから筋がそれていく原因になります。

ユビキタス言語は共通言語を定義すること以上に、プログラマの専門用語や、サービス・プロダクトに関連しないビジネス用語は徹底的に排除することが重要です。開発者と非開発者がわかること、言い回しがシンプルで、直感的であることが大切です。

役割が異なるため、すべてを理解する必要はないし、理解するにはものすごくコストがかかります。
サービスを作る上で共通認識をとる領域をきめて、その部分は理解し合えるようにできると良いと思います。

なぜClean Architectureを使うのか

DDDを使う意義はつらつら書いたため、この節以降はDDDに則ったアーキテクチャであるClean Architectureについて書いていきたいと思います。

Clean Architectureとはよくみるこの図のやつです。

個人的に、Clean Architectureはここまでに書いてきた他領域とのコミュニケーションありきのアーキテクチャであり、実装手法だけ取り入れてもコストばかり高く、割と簡単に崩壊してしまい、かえって混乱の種になると思っています。(軽量DDDっていうんですかね?しらんけど)

また、ツールのような小規模の実装や、全く変化しない作りきりのシステムには不向きです。べらぼうにファイルを作るので、なれてないとびっくりするぐらい時間を持ってかれます。
一定つくったら次フェーズのカスタマイズは担当者総入れ替えするような業態やプロジェクトにも向かいないと思います。

コミュニケーションを取ることが前提のアーキテクチャではありますが、使いこなせれば強力です。
Clean Architectureを使うことの強みとして、会話で展開すればコードのキャッチアップは比較的かんたんであることと、変化に強いことの2つが挙げられます。

実装方法とコードの例ともになぜ使うのか、どういうところを気をつけるべきかを説明します。

ちなみにコード例はClean Architectureと銘打ったものの、自己流のところは多分にあり、
ばったものである可能性は否定できませんのであしからず。

サンプルコード

以下がClean Architectureで組んだサンプルコードです 
https://github.com/pt-suzuki/grpc_template

こちらのDevelopersIOの記事をもとにdomainを整理して実装しています
https://dev.classmethod.jp/articles/golang-grpc-sample-project/

ざっと実装ルールは以下のように定義しています。

  • リクエストのハンドリングはcontrollers、実装はdomainsに集約する
  • domains配下に要素ごとにディレクトリを切る
    • 関連が強い要素は同一の要素ディレクトリに集約するする
      • サンプルコードでいうとレポートはじゃんけんの結果を返すものであり、関連度が強いため、reportはrock_paper_scissorと同一のディレクトリに置く
      • 要素はファイル名、クラス名で識別する
  • controllers配下にdomains配下と同一のディレクトリを切る。
  • 要素の実装はusecase、repository、translator、model(値オブジェクト)に層分けする
    • usecaseには計算、判断、状態変更などのビジネスロジックを集約する
      • 単体のmodelのみで計算してプリミティブ型、または定数で値を出力する場合、modelにビジネスロジックを実装する
    • repositoryには外部サービスの接続を集約する
    • translatorには型変更ロジックを集約する(状態変更は実装しない)
    • modelは基本的に型オブジェクトとして使用する
  • 他要素のロジックはusecase→usecaseで呼び出す。
  • usecase→repositoryは同一要素で呼び出す。
  • controllersのファイルはリクエストの種類ごとに分割します。またリクエストをaction、レスポンスをresponderに分割します。

普段の会話とコードを揃える

Clean Architectureを使うことで、普段の会話をそのままコードに落とし込むことができるようになります。
具体的にコードに落とし込める会話とは、「要素」、「属性」、「振る舞い」を指します。

軽く触れましたが、ドメイン関連図を使って定義した「ドメイン」はそのままdomainディレクトリにそのまま落とし込みます。

検索する、登録する、削除する、計算するなどの「振る舞い」はメソッドにそのまま落とし込みます。

「属性」は値オブジェクトのフィールドです。

上記についてこちらのDevelopersIOのじゃんけんサービスのコードを例にして定義します。
https://dev.classmethod.jp/articles/golang-grpc-sample-project/

元のコードでは1ファイルにまとまっていますが、このじゃんけんサービスを構成する「要素」として、ゲームであるじゃんけん(rock_paper_scissors)とその勝敗の積み上げを返すレポート(report)があります。

じゃんけんのゲームの「振る舞い」はユーザーが、グー、パー、チョキのいずれかの値を選ぶと、ランダムでシステムがグー、パー、チョキのいずれかを出し、ユーザーが選択した値と比較して勝ち、負け、あいこを結果として返します。
元記事ではメモリに保存していますが、私の実装ではDBに結果を保存します。

レポートの取得の「振る舞い」はユーザーの今までのじゃんけんのゲーム数と勝った数、およびすべてのじゃんけんの詳細(自分と相手が出した手と結果)を返します。
元記事ではメモリに都度結果を積み上げていますが、私の実装ではDBに保存した結果を検索して返します。

元コードにはありませんが、ユーザーをDBに保存してじゃんけんの結果を関連付けます。レポートはユーザーのIDをもとに呼び出します。

上記を元にClean Architectureに則って整理したのが以下です。

https://github.com/pt-suzuki/grpc_template

このように整理すると会話の内容をある程度はコードにそのまま落とし込めるようになります。
リクエストや、じゃんけんをランダムに出力する方法、どのようにDBに保存するかなどの仕組みは会話では表現していませんが、データの流れは会話のとおりに追うことが可能です。

上記粒度であれば、非開発者も仕様の検討・提案が可能で、開発者のみで考える領域をへらす事ができます。

変化に強いとは捨てやすいということ

Clean Architectureは変化に強いと言うことがよく挙げられます。
変化に強いというとアバウトなのでもう少し突っ込んでいうと、個人的にコードを捨てやすいというところがあると思っています。

事業会社に所属していると一つのプロダクトを長く運用し続けることになると思いますが、長く運用することで要望からカスタマイズが発生したり、時間が経つにつれてプロダクトを新規開発で選定した時よりも、よりマッチした言語やサービスが発表されるなんてこともあると思います。

その際に現行で使っている技術を切り離して新しい技術に差し替える作業が発生しますが、ガッチリ絡んでいて実装をすることはもちろん、影響範囲を調べることも難しいということは多いと思います。

Clean Architectureでは、意識して実装すれば既存のコードを切り離す(捨てる)ことが、普通にコードを書くよりも、やりやすいと思っています。

Clean Architectureではdomainとして要素ごとにレイヤーを切り、要素ごとの実装から更にcontroller(action、responder)、repository、usecase、translator、model(値オブジェクト)のレイヤーに区切ります。
この2次元のレイヤーに区切ることで、捨てやすいコードにすることが可能です。

サービスを差し替えるときはrepository、またはcontrollerを捨て、言語ごと入れ替える際はdomain単位でストラングラーパターンを使用することで、一度に大規模な変更をせず、影響範囲を限定しながら徐々に移行できます。また、ユーザーにとっての価値が高さは移行の順序をきめる軸にもなります。

上記を実現するために気をつけることとして、
二度書きをしないことと、usecase層にサービス由来の値オブジェクトを持ち込まないことの2点です。

二度書きしないことは言うまでもない事かもしれませんが、
やりがちなのはmodel(値オブジェクト)にDBの呼び出しロジックを含めることです。

言語仕様によってmodelをただの値オブジェクトとして利用するか、関数を含めてドメインロジックを含めるかはチーム内での決めの問題ではありますが、個人的に少なくともmodelにDB呼び出しロジックを含めるのは避けてusecaseを介してrepositoryで呼び出したほうが同一ロジックを書かずにすむのでそうしています。

また、usecaseから他のドメインのrepositoryを呼び出すこともビジネスロジックの重複につながるので避けたほうが良いです。自ドメイン以外の呼び出しはすべてusecaseを通すと棲み分けが明確になります。

次にusecase層にサービス由来の値オブジェクトを持ち込まないことですが、これはusecase層にオブジェクトを入れる前に、自分で作成したmodel(値オブジェクト)に詰め替えてからusecaseに渡します。

usecaseに内製のコードをすべて集約すること、inputとoutputを担保することで、外部からの接続をcontrollerとrepositoryに完全に切り離せるため、サービスやミドルウェア等のアーキテクチャ変更がやりやすくなります。

input、outputの入力を担保するために、私はtranslatorを使用しています。
controllerやrepositoryで呼び出し、型変更してからusecaseに渡す、またはusecaseから受け取ってから外部アクセス用オブジェクトに詰め替えます。

余談でmodelへ直接型変更を実装すると、一方通行の場合は問題は出ませんが、A→B、B→Aの両方の変換するケースがあると循環参照ができて崩壊します。

テストについて

テストはサンプルに実装していないため、整備してから改めて追記しますが、ドメイン別層別に実装している分、クラス単体でのテストはやりやすいです。

また、ドメインの価値・重要度については前述の通り濃淡があるため、価値・重要度の高いドメインのテストを優先的に実装する事が可能です。ディレクトリごとにカバレッジを算出し、特定のディレクトリのカバレッジ率を上げるなどすると、全体のカバレッジ率で考えるよりも効果が高い目標値になると思います。

節目が層ではっきりしているのでモックも実装しやすいです。また、DIを徹底すれば、言語別のモックツールに依存することなく、テストクラスをInjectionして単体テストを実装する事が可能です(コード量は多くなると思いますが)

層で考えると、肌感ではありますがバグはほとんどtranslaterでおきます。
translatorのテストをしっかり書くと、バグの何割かは潰せます。

おわりに

DDD、Clean Architectureについてまとめました。
最初に書いたとおり、自己流の考え方、組み方を大いに含んでおり、決して正しいDDDやClean Architectureとは言えないかもしれません。

まだまだやりながら少しづつマイナーチェンジして改良している最中で、ある時そっくりころっとやり方を帰る可能性もあるのでそういうものと思って見ていただけると幸いです。

Discussion