ドメイン駆動設計とオニオンアーキテクチャ、Go言語での実現について

devops チームの認知負荷を下げるためには


Posted on Thu, Jan 12, 2023
Tags golang, ddd, architecture

背景

システム開発ならびに運用する現場において、最近、ドメイン駆動設計を取り入れている。 一方で、システムの devops は絶対と言えるような法則は少なく、メリデメが存在する(= 銀の弾丸はない)ことがほとんどである。

この記事では、ドメイン駆動設計(Domain Driven Design)とオニオンアーキテクチャについて、自分の理解を整理してまとめる。

まとめ(TL;DR)

  1. 業務に関わるシステムの devops においては、「ソースコードからシステム動作が想像しやすい」ことが大切な要素の一つである
  2. ドメイン駆動設計は特定のアーキテクチャに縛られたものではないが、オニオンアーキテクチャを採用することで、チームの中で「どのコードがどういった役割なのか」理解しやすくなる
  3. 銀の弾丸ではないものの、「ドメイン駆動設計 + オニオンアーキテクチャ」をうまく開発に用いれば、devops チームの認知負荷の低減が期待できる

ドメイン駆動設計とオニオンアーキテクチャの採用について

システムの devops において大切なこと

ドメイン駆動設計の話に入る前に、まず業務に関わるシステム開発において重要なことの一つに「ソースコードからシステム動作が想像しやすい」ことが挙げられる。 もちろん、それ以外にも重要なことが複数あるが、本稿では設計手法やアーキテクチャに関連してこちらを取り上げる。

なぜ、「ソースコードからシステム動作が想像しやすい」ことが大切なのかといえば、システムは往々にしてビジネス要件に応じた変更を加える必要があり、変更を加えた場合の動作が想像しづらいシステムは運用で苦しむことになるためだ。 逆にハードウェアに組み込まれているような変更を加えることがないソースコードはどんなに複雑であっても、期待した振る舞いをしていれば修正することはないのであまり気にすることはないだろう。

では、どうすれば「ソースコードからシステム動作が想像しやす」くなるのだろうか? 自分は、「システム内の依存関係を明確にし、ソースコードからシステムの構造を理解できる」ことにあると考えている。

例えば、システム構造が下記のような木構造になっているとする。

システムとは、「入力に対して、状態を持ち、出力を返すステートマシン」であるため、システム構造が上図のようになっていることがソースコードから理解できれば、動作を想像するのは簡単になる。

一方で、システム構造が下記のような構造(いわゆる Big Ball of Mud)になっているとする。

この場合、一部のプログラムに修正を加えた場合、依存関係が複雑なため、どこに影響が波及するか想像することは難しい(システムの規模や開発者の熟練度やチームの規模によるが)。

ドメイン駆動設計

ドメイン駆動設計は、ドメイン知識(ソフトウェア化の対象となる業務知識やルールなど)を核としたソフトウェアを設計する手法である。

Domain Driven Design(ドメイン駆動設計) Quickly 日本語版 から引用すると、

では、どうすればドメインに円滑に適合するソフトウエアを作成できるのでしょうか。もっともよい方法はドメインの反映としてのソフトウエアを作ることです。そのためには、ソフトウエアはドメインの核となる概念や要素を取り入れ、それらの関係を正確に再現する必要があります。

我々がなぜビジネスにおいてソフトウェアを開発運用するかというと、多くの場合、業務を効率化するためである(注1)。

ドメイン駆動設計は、現実のドメイン知識を核としてソフトウェア化する設計手法のため、整理されたドメイン層のコードを読めば、何を行おうとしてどう動作するのかを想像しやすい(注2)。 「ソースコードからシステム動作が想像しやす」ければ、新たな業務フローが追加された場合のコード修正や、システムが想定していない振る舞いをした場合の問題の理解がしやすくなる。

ドメイン駆動設計については、参考となる様々な書籍や Webの記事があるため、詳細は省略する。

  • 注1:すべてのソフトウェア開発が当てはまるわけではない。例えば、コンパイラや Linux のカーネルなど業務効率化を目的としないソフトウェアではドメイン駆動設計を採用する必要はない
  • 注2:例外として、受注開発しているなどの関係で、解決したいドメインに関する知識を全く持たずに開発しなければならない場合、ドメイン層のコードを読んでもドメインに対する知識がないため、動作を想像することは難しい可能性がある。そうした場合も、ドメイン駆動設計が適合しないかもしれない

オニオンアーキテクチャ

ドメイン駆動設計はあくまでドメインを中心に据えた設計方法であり、システムのアーキテクチャを指定するものではない。 DDD Reference のなかでは Layered Architecture が取り上げられているが、要点として下記が記載されている。

Isolate the expression of the domain model and the business logic, and eliminate any dependency on infrastructure, user interface, or even application logic that is not business logic.

訳すと「ドメインモデルとビジネスロジックは隔離させ、インフラストラクチャ、ユーザーインターフェース、ビジネスロジックではないアプリケーションロジックすらへの依存を排除させる」とある。 つまり、ドメインモデルとビジネスロジックを独立させることができれば、アーキテクチャは何でも良いとも言える。

実際に、ドメイン駆動設計について、クリーンアーキテクチャやヘキサゴナルアーキテクチャ、あるいは関数型言語で実装するような例が存在する。

そのうちの一つのアーキテクチャとして、自分がよく利用するものに Onion Architecture(オニオンアーキテクチャ) がある(注3)。 厳密には、オリジナルのオニオンアーキテクチャの名称を変更した下記の図のようなアーキテクチャである。

なぜ上記のアーキテクチャを利用するかといえば、下記のメリットがあると感じるからである。

  1. DDD Reference で記載されている Layered Architecture と比べて、ドメイン層がインフラ層に依存せず、独立できている
  2. Clean Architecture と比べてシンプルであり、複数のコンポーネントで適切に活用できれば、チームの学習コストや認知負荷を減らせる(Rails の MVC のフレームワークのように)

特にチームで devops しているケースだと、図示されたアーキテクチャに従った実装を心がけることは、新規に参加したメンバーも「ソースコードからシステム動作が想像しやすく」なる。

Go言語での構成

ここまで、ドメイン駆動設計とオニオンアーキテクチャについて説明したため、具体的に Go言語で開発する場合はどうしているのかについてまとめる。

自分がよく利用するディレクトリの基本的な構造は下記のようになる。扱うドメインの複雑さに応じて、下記に手を加えることもある。

repo-root
├── cmd/      : コマンドラインツールの場合、 main.go の置き場となる。基本的に、usecase/ を呼び出してコマンドを実行するだけとなる
├── domain/   : ドメイン層。ドメインモデルとビジネスロジックの隔離のため、他の infra/ usecase/ へ参照(依存)してはならない。
├── go.mod
├── go.sum
├── infra/    : インフラストラクチャ層。ドメイン層で定義された interface を実装する
├── internal/ : ログ、トレース、メトリクス、DI などレイヤーを横断して求められる機能を集約する
├── presentation/ : プレゼンテーション層。UI があるアプリケーションの場合に利用する。usecase/ のみに参照させる
└── usecase/  : ユースケース層。アプリケーションに求められているユースケースを domain/ を呼び出して実現する

ポイント

  • ディレクトリの分離によるレイヤーの明確化。domain 内のパッケージから infra や usecase が呼び出されないようにする
  • internal ディレクトリにログ、メトリクスなど外部に公開しないシステム横断的な機能を入れる。Go言語では internal ディレクトリ配下は他のパッケージから参照できないようになる
    • domain パッケージから internal を参照する可能性はあるが、domain の独立性を高めるために最小限に留める。最悪のケースのため、internal にドメインロジックの流出や、ドメインに関わらないロジックに依存してしまうため、利用は慎重でなければならない

最後に

繰り返しになるが、ドメイン駆動設計はシステムの設計方法の一つであり、全てのシステム開発にうまく当てはまるわけではない。 実現したいこととメリデメを理解し、ドメイン駆動設計が当てはまりそうな場面では利用を検討するということが、ソフトウェアエンジニアには求められる。

参考