【デジタルアライアンス ハタケヤマ】
木附さん、バトンパス有難うございます。
娘の成長の速さに驚いて、近頃私も時の流れの速さを実感しています。
漫画といえば私が見ているのはアニメだけですが、ベタですが最近は私も鬼滅の刃にはまっています。
もしかすると漫画にも手を出すかも・・・現在検討中です。
さて、今回はPHPのLaravelフレームワークで、ドメイン駆動開発(以下ddd)を検討した話についてご紹介したいと思います。検討するに当たり、書籍「エリック・エヴァンスのドメイン駆動設計」(以下dddの書籍と呼称)だったり、Laravelでdddを検討した方がGitHubにアップされたサンプルプロジェクト等を参考に致しました。
LaravelはMVCのWebアプリケーションフレームワークです。言語はPHPとなります。そのまま使っても便利で簡単にWebアプリケーションの構築ができるのですが、Webアプリケーションの規模が大きくなっていくと重複コードが増えていって、類似の処理があちこちに点在してしまう自体にもなり得ます。適切にクラス設計をしながら開発しなければ、保守の効率が下がったり、品質が安定しなくなるリスクもあります。そうした問題を解決すべく、dddの導入を検討しました。
まずは一般的なLaravelのディレクトリ構成を以下に記します。
<Laravelのディレクトリ構成>
laravelapp
┗app
┃ ┣Console
┃ ┣Exceptions
┃ ┣Http
┃ ┃ ┣Controllers(※1)
┃ ┃ ┗Middleware
┃ ┣Model(※2)
┃ ┗Providers
┗resources
┗views(※3)
(※1)HTTPリクエストの入り口であり、MVCアーキテクチャのC(コントローラー)に当たる責務を司る
(※2)データベースへアクセスするビジネスロジックのクラス郡。MVCアーキテクチャのM(モデル)に当たる責務を司る。
(※3)ユーザに表示するHTML。動的な処理も実装可能。MVCアーキテクチャのV(ビュー)に当たる責務を司る。
上記はデフォルトのLaravelのディレクトリ構成の一部です。このディレクトリ構成のまま開発していくと、よく陥る事態としてはControllersが肥大化したり、別のリクエストのControllerの関数なのに類似の処理が書かれてってコードが重複したり、再利用性が低下します。コード全体の見通しも悪くなり、保守性の低下や品質の低下のリスクも高まります。
また、Modelについても注意が必要です。様々な処理をModelへ継ぎ足して行くこともまたModelの肥大化につながったり、類似コードの重複や再利用性の低下等、上記のControllersと同じような問題に繋がります。
それではLaravelにdddを導入したディレクトリ構成のサンプルを以下に記します。
<Laravelにdddを導入したディレクトリ構成のサンプル>
laravelapp
┗app
┃ ┣Console
┃ ┣Exceptions
┃ ┣Http
┃ ┃ ┣Controllers
┃ ┃ ┗Middleware
┃ ┣Model
┃ ┗Providers
┣resources
┃ ┗views
┗packages
┗[Product名]
┣UI(※4)
┣Application
┃ ┗UseCase(※5)
┣Domain(※6)
┗Infrastructure
┣Notifications(※7)
Laravelのプラットフォームからは分離したディレクトリ「packages」配下にdddのクラス郡を配備します。以下に主要なディレクトリの説明を記します。レイヤ化アーキテクチャを導入し、UI、Application、Domain、Infrastructureと4つのレイヤーに責務を分割します。そして、それぞれのレイヤーの責務にあうようにモジュール分割して実装していきます。それによって、ソフトウェアの関心事が適切に分割されて整理されます。そのことでドメイン層がドメインモデルを表現するという責務に専念できる様になるとのことです。ドメイン層にDBの処理が入ったり、Viewの表示に関する処理が入ってしまうと、ドメイン層へドメイン層以外の責務や関心事の処理が混在して複雑化したり、ドメインモデルの本来の責務が曖昧になったり分かりづらくなってしまいます。
(※4)UIに関するロジックをこのレイヤーに実装。
(※5)ドメイン層のクラス郡を使用して一連の処理を行うといった、スクリプトのように振る舞う処理を司る。書籍「エリック・エヴァンスのドメイン駆動設計」にはサービス層と記載されていたのですが、LaravelにはServiceProviderという全くdddのいうサービスとは異なるが名称が似ている機構があるため、混乱を避けるためにあえてUseCaseという名称を採用。
(※6)ソフトウェアが取り扱うビジネスの概念・情報・ルールを司る。ドメイン駆動開発の心臓部。
(※7)通知系の処理を司る。
(※8)DBやファイル等、永続化機構のR/Wの処理を司る。
レイヤ化アーキテクチャを導入することで、モジュール分割がソフトウェアの関心事毎に適切に行われ、見通しがよくなりました。また、UI→Application→Domain→Infrastructureの順でレイヤには上下関係があり、UIが最上位でInfrastructureが最下位となります。原則下のレイヤか、同階層のレイヤに対してしか依存しないというルールとなっています。どうしても上のレイヤとコミュニケーションが必要な場合は、オブザーバーパターンを使用する場合もあるとの事です。
ただ、dddの目的はこのレイヤ化アーキテクチャそのものではなく、あくまでもドメイン層のドメインモデルがその責務に専念する為とされています。ドメインモデルが自らの責務に専念出来さえすれば、レイヤ化アーキテクチャは大真面目に全て取り入れる事は必須ではなく、必要に応じて取り入れればいいとdddの書籍に記載されておりました。
個人的な意見としては、規模にもよるでしょうがWebアプリケーションでdddを完全な形で導入するのはオーバースペックな印象があります。これは私の設計力の問題かもしれないですが・・・ただ、レイヤ化アーキテクチャは優れた設計思想であり、dddもオブジェクト指向でソフトウェアを開発する上で、非常に優れた設計思想・概念だと思います。Webアプリケーション開発においてもそれは例外ではなく、その設計思想を全部とは言わなくても部分的にでも取り入れることで、保守性や品質の高いソフトウェアを開発出来ると思います。
例えばDBから値をとってくるRepositoryの処理を上の層で使う際、データ型をDBから取ってきたままではなくプリミティブな値に変換することで上位層との結合が疎結合となり、RepositoryのDBがPostgreSQLからMySQLへ変更されても上の層の処理は変更しなくていいという実装テクニックがあります。でも、それをやろうとすると、テーブルやマスター毎にValueObjectクラスを実装する必要が出てきて、冗長な実装が大量に発生します。また、Laravelには使用するDBを.envで設定できる機構が備わっており、こうした疎結合の実装を無理して導入するメリットはあまりありません。有効に働く概念・設計思想を無理なく導入するのが良いと考えています。
上記シーケンス図で表している挙動としては、ViewからUseCaseをコマンドのように何個も実行出来るよう構成しています。バリデーションチェックをさせたり、メール送信をさせたり、Viewに必要なデータを取得したりという細かい単位でUseCaseを構成し、それをコマンドのように必要なものを必要な文実行する構成です。こうすることで、一連の処理をするUseCaseも部品化し、類似の処理は共通のUseCaseとして部品化出来ます。Repositoryから取得した情報をDomainのEntityへ復元し、Entityに対して追加で必要な情報取得を行います。このシーケンス図では、ProductRepositoryでProductというDomainのEntityを取得し、Productに対して色違いの取得や、サイズ違いの取得を行っています。
上記クラス図で表している構造として、UseCaseクラスは全てUseCaseBaseを継承し、共通の処理をそこへ記載することでコードの重複を避けています。また、実装の仕方も一定のルール化をされて、クラスやモデルの責務に逸脱する実装を避けます。Repositoryに関してもInterfaceや継承を活用し、実装の仕方のルール化や、Repositoryの利用の仕方の明示化がなされます。Interfaceで上位層との疎結合をし、UseCaseやDomainと、Repositoryの分離をする事も出来ますしdddではそれを推奨しているようですが、先述で述べたようにそれをするにはプリミティブな型へデータ変換するという冗長な実装が増えるので敢えてしておりません。苦労しても得られるものが少ない箇所は省略して楽した方がメリットです。
最後に、書籍「エリック・エヴァンスのドメイン駆動設計」ではモデルは常にリファクタリングを通して進化させる事を推奨しており、小さな進化や、進化をきっかけに大きな改善のきっかけを掴んでブレイクスルーといった大きな進化を繰り返すことで、より高い保守性・高い品質のソフトウェアに進化していく。ドメインモデルの更なる進化の洞察を得て、モデルの蒸留をする。そして、改変、機能追加を柔軟に行えるしなやかな設計を目指す。その機会を逃さない事を推奨していました。1つの解があるわけではなく、開発するアプリケーション毎に最適な解を見出し、プロダクトを成熟させていく。そうした所に開発の醍醐味があり、設計技術を鍛える経験値にもなっていくと思います。一方でリファクタリングにはリスクがつきものなので、自動テストを導入してリスク低減を図ったり、改造規模によっては実施するか否かの事業判断が必要な場合もあると思います。影響範囲をすり合わせた上で必要とあらば実行する。そうした影響範囲の考慮も必要だと感じています。