要約 / inb4 tl;dr
- 成瀬允宣さん(Twitter , nrslib )の 『ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本』 を読んでサンプルコードを Laravel で実装してみた。
- DDD 入門として、とても良い書籍だったのでオススメ。
- 実装の前提条件やポイントをまとめた。
- ※注:この記事は、あくまでサンプルコードの書き起こしと整理のみです。DDD 本来の設計作業についての記載はほとんどありません。
- コードはこちら
はじめに
2020年2月9-11日に開催された PHPerKaigi2020 に参加してきました。
こういうイベントごとには滅多に参加してこなかったのですが、今回は少し足を伸ばして練馬まで行ってきました。
沢山学びがあったのですが、その中でも以下のセッションがキッカケとなって DDD 入門するに至りました。
セッションでお話しされてた成瀬さんの書籍に興味を持って購入してみたのですが、これがまた本当に良い本でした。
ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本 - Amazon
一通り通読したあと、 Laravel / PHP で実装してみたので、忘れないうちに構築のステップなどをメモしておこうというのがこの記事の内容です。
概要
ローカル開発環境
実装時に使用したローカル開発環境のソフトウェア・スタックはコチラ
以前、いくつか記事を書いたことがあるのですが、ローカル開発環境については以下の構成で作業しました。
Software Version
GuestOS のソフトウェア・バージョンは下表の通りです。 Application用の Docker Container のソフトウェア・バージョンは下表の通りです。実装時に使用したローカル開発環境のソフトウェア・バージョン情報はコチラ
HostOS のソフトウェア・バージョンは下表の通りです。
項目
内容
備考
HostOS
Windows 10
仮想化
VirtualBox
Vagrant 2.1.2
Editor
Visual Studio Code 1.26.1
WebBrowser
Chrome 80.0.3987.122
項目
内容
備考
GuestOS
CentOS Linux release 7.6.1810 (Core)
git
git version 2.16.5
heroku
heroku/7.35.1 linux-x64 node-v12.13.0
OpenSSH
OpenSSH_7.4p1, OpenSSL 1.0.2k-fips 26 Jan 2017
docker
Docker version 19.03.5, build 633a0ea
docker-compose
docker-compose version 1.17.1, build 6d101fb
項目
内容
備考
PHP
PHP 7.2.26 (cli) (built: Dec 28 2019 22:17:07) ( NTS )
Xdebug
Xdebug 2.9.2
composer
Composer version 1.9.3 2020-02-04 12:58:49
Laravel
Laravel Framework 6.18.0
PHPUnit
PHPUnit 8.5.2
一般的なWebシステムの構成
環境定義
Application Architecture
最終的に出来上がったアプリケーション内の Package ( Module ) / Class の構成は下図のようになりました。
Set up
ホストOS 上で以下のコマンドを実行することで、 Docker と docker-compose が入った CentOS 7 を構築します。 起動した仮想環境内で、 Docker と docker-compose をコマンド操作できるか確認します。 HostOS の VSCode で Remote Development を使って、 GuestOS 上の Docker Container にアクセスするための手順は下記を参照ください。 仮想環境内で、 Laravel + MySQL + nginx を dockerコンテナ にインストールする為のスクリプトを取得し、ビルド、実行することで、 Laravel を導入します。 Laravel から MySQL へ接続する為に、 .env を更新します。 マイグレーションを行います。 以下の URL から接続を確認します。 この画面を確認することが出来たら、初期構築は完了です。 VSCode に導入した拡張機能は次の通りです。 【dokcer環境 on VirtualBox by Vagrant】 を構築して Laravel と VSCode の初期設定まで。
1. CentOS7 の起動
// GitHub から Vagrantfile と provision.sh を取得する
$ git clone https://github.com/anfangd/vagrantfile-centos7-and-docker centos-7-docker && cd $(basename $_ .git)
// HostOS に Vagrant のプラグインをインストールする
$ vagrant plugin install vagrant-vbguest
$ vagrant plugin install vagrant-proxyconf
// Vagrant の起動する
$ vagrant up
// 仮想環境へ ssh でアクセスする
$ vagrant ssh
// docker の動作確認をする
$ docker run hello-world
// docker のバージョン確認をする
$ docker --version
Docker version 18.06.1-ce, build e68fc7a
// docker-compose のバージョン確認をする
$ docker-compose --version
docker-compose version 1.17.1, build 6d101fb
2. Laravel の導入
// GitHub から docker-compose 用の スクリプトを取得する
git clone https://github.com/anfangd/docker-compose-php-laravel.git dc-laravel && cd $(basename $_ .git)
// 取得したスクリプトをビルドして、docker-compose で コンテナを起動する
$ docker-compose build
$ docker-compose up -d
// laravel でプロジェクトを作成する
// nginx で `laravel-src` のフォルダ指定があるため、ディレクトリ名はこれで固定
$ docker-compose exec php composer create-project laravel/laravel laravel-src --prefer-dist "6.*"
// 作成したプロジェクトの storage に書き込み権限をつける
$ docker-compose exec php chmod -R 777 ./laravel-src/storage
// MySQL への接続情報を変更する
$ vim src/laravel-src/.env
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=root
DB_USERNAME=root
DB_PASSWORD=root
// マイグレーションを行う
$ docker-compose exec php php ./laravel-src/artisan migrate
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated: 2014_10_12_000000_create_users_table
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated: 2014_10_12_100000_create_password_resets_table
3. 接続確認
4. composer で関連ツールの導入
composer
で導入するツールは次の通りです。// IDの採番に使用する
$ composer require robinvdvleuten/ulid
// AOPの実現に使用する
$ composer require ytake/laravel-aspect:4.0
// Debug Tools
$ composer require --dev "squizlabs/php_codesniffer=*"
$ composer require --dev friendsofphp/php-cs-fixer
$ composer require --dev phpmetrics/phpmetrics
$ composer require --dev fzaninotto/faker
$ composer require --dev mockery/mockery
$ composer require --dev php-coveralls/php-coveralls
5. VSCode に 拡張機能を導入
# PHP 関連
bmewburn.vscode-intelephense-client
felixfbecker.php-intellisense
ecodes.vscode-phpmd
ikappas.phpcs
junstyle.php-cs-fixer
MehediDracula.php-namespace-resolver
mikestead.dotenv
neilbrayfield.php-docblocker
# js 関連
dbaeumer.vscode-eslint
eg2.tslint
octref.vetur
# 便利ツール
eamodio.gitlens
donjayamanne.githistory
znck.grammarly
Gruntfuggly.todo-tree
wayou.vscode-todo-highlight
HookyQR.beautify
emilast.LogFileHighlighter
eriklynd.json-tools
redhat.vscode-yaml
IBM.output-colorizer
wraith13.background-phi-colors
# VSCode 関連
johnpapa.vscode-peacock
MS-CEINTL.vscode-language-pack-ja
ms-vscode-remote.remote-containers
ms-vscode-remote.remote-ssh
ms-vscode-remote.remote-ssh-edit
ms-vscode-remote.remote-wsl
ms-vscode-remote.vscode-remote-extensionpack
# その他
yzhang.markdown-all-in-one
shd101wyy.markdown-preview-enhanced
jebbs.plantuml
zhouronghui.propertylist
vscode-icons-team.vscode-icons
実装してみると、機能がほとんど無い割にコード量が多く、クラスもどんどん増殖してゆくので、VSCode の拡張機能や PHPUnit、 Xdebug を使い倒さないと実装の抜け漏れに気づくのが大変だということを痛感しました。
設計
理想としては以下の記事のように丁寧に設計を行ってみたいのですが、今回は書籍で紹介されていたコードの実装に注目しているので、簡単な情報の整理のみを行います。
ユビキタス言語
ユビキタス言語 | 英名 | 説明 |
---|---|---|
ユーザ | User | # 意味 サービスの利用者のこと。ユースケース図のアクターとなる。 # 用例 ユーザを作成する。 |
ユーザID | User ID | # 意味 ユーザを一意に識別するコードのこと。 # 用例 山田さんのユーザIDは XXXXX です。 |
ユーザ名 | User Name | # 意味 ユーザの名前のこと。姓名の分割は行わない。 |
プレミアム会員 | Premium Member | # 意味 ユーザの属性のうち、特殊な属性を持つユーザであることを示す。 # 用例 山田さんはプレミアム会員です。 |
サークル | Circle | # 意味 ユーザが所属する任意の組織のこと。 # 用例 佐藤さんは「野球」サークルのメンバーです。 |
サークルID | Circle ID | # 意味 サークルを一意に識別するコードのこと。 |
オーナ | Owner | # 意味 サービスの所有者のこと。 # 用例 「野球」サークルのオーナは山田さんです。 |
メンバー | Member | # 意味 サークルに所属するユーザのこと。 # 用例 岡村さんは「野球」サークルのメンバーです。 |
ユーザ登録 | Register User | # 意味 ユーザがサービスに利用登録する操作のこと。 # 用例 サービスの利用にあたってはユーザ登録をしてください。 |
ユーザ情報の更新 | Update User | # 意味 ユーザ名の変更操作を行うこと。 # 用例 本当にユーザ情報を更新しますか? |
退会 | Delete | # 意味 ユーザがサービスを利用停止すること。 # 用例 山田さんが退会しました。 |
サークル作成 | Create Circle | # 意味 サークルを新しく作成すること。 # 用例 山田さんがサークルを作成します。 |
参加 | Join | # 意味 ユーザがサービスのメンバーとして登録すること。 # 用例 林さんが「野球」サークルに参加しました。 |
勧誘 | Invite | # 意味 オーナがサークルにユーザを招待すること。 # 用例 山田さんが「野球」サークルに佐藤さんを勧誘する。 |
コンテキストモデル(?)
実装したシステムの全体像はこんな感じ。
ドメインモデル(?)
ドメインはシンプルに下図の通り。
ユースケース図
ユースケースは下図の通り。
ロバストネス分析
- ロバストネス図の概要
- ロバストネス分析の目的 - Qiita
- 実践ロバストネス分析 第1回 ロバストネス分析の基礎 | オブジェクトの広場
- ロバストネス図を活用したシステム設計 | Think IT(シンクイット)
UseCase: ユーザを登録する
UseCase: ユーザ情報を取得する
UseCase: ユーザ情報を更新する
UseCase: 退会する
UseCase: サークルを作成する
UseCase: サークルに参加する
UseCase: サークルに勧誘する
クラス図
省略。
ER図
ER図は下図の通り。
ディレクトリ構成
作成したアプリケーションのディレクトリ構成は以下の通り。
├── app
│ ├── Aspect # AOP Modules
│ │ ├── Annotation
│ │ ├── Interceptor
│ │ ├── Modules
│ │ │ ├── LogExceptionsModule.php
│ │ │ ├── LoggableModule.php
│ │ │ ├── QueryLogModule.php
│ │ │ └── TransactionalModule.php
│ │ └── PointCut
│ ├── Console
│ │ └── Kernel.php
│ ├── Database # Eloquent Models
│ │ └── Eloquent
│ │ ├── CircleDataModel.php
│ │ └── UserDataModel.php
│ ├── Exceptions
│ │ └── Handler.php
│ ├── Http # Http Controllers
│ │ ├── Controllers
│ │ │ ├── Controller.php
│ │ │ └── UserController.php # Controller
│ │ ├── Kernel.php
│ │ ├── Middleware
│ │ └── Models
│ │ └── User
│ ├── Providers
│ │ └── AppServiceProvider.php
│ └── User.php
│
├── bootstrap
│ └── app.php
├── composer.json
├── config
│ ├── app.php
│ └── ytake-laravel-aop.php # conf about Laravel-Aspect
├── database
│ └── migrations
│
├── packages # 作成したコードの大部分はこの下に配置
│ └── Techno # Domain Name
│ └── Sns # Sub Domain Name
│ ├── Application # Application Layer
│ │ ├── Circle
│ │ └── User
│ ├── Domain # Domain Layer
│ │ ├── Circle
│ │ ├── Exceptions # Exceptions
│ │ ├── Service
│ │ └── User
│ ├── Infrastructure # Infrastructure
│ │ ├── Eloquent
│ │ ├── InMemory
│ │ └── Notification
│ └── UseCase # UseCase
│ ├── Circle
│ └── User
│
├── routes
│ ├── api.php
│ └── web.php
└── tests
├── Feature # Feature Test Code
│ └── Http
│ └── Controllers
└── Unit # Unit Test Code
└── packages
└── Techno
└── Sns
実装
やっとこさ、この記事の本題です。
実装した順に、コードと対象スコープを記載します。
コードは、機能単位で GitHub の Pull Request にまとめているので、詳細はそちらを参照ください。
0. 関連ソフトウェア
基本的には 生のPHP + Laravel の一部機能( Collection ) で実現をしますが、 AOP の実現 や ユーザIDの発行 のために以下のソフトウェアを使用しました。
- AOP の実現
- ID の発行
1. Value Object の作成
2. Entity の作成
Service の作成
Repository の作成
Application Service の作成
Application Service の分割
Controller の作成 と IoC の実装
ファクトリ( Factory )の実装
AOP の実装
ここを始めた段階で、 Laravel の 7.x 系を使っていたことに気づいて 6.x 系にダウングレードした。。
サークル 関連機能の作成
集約( Aggregation )の実装
仕様( Specification )の実装
Notification の実装
実装したコードの残課題
- 最後の方は疲れてしまって、サークル機能で一部実装漏れがあります。
-
packages
ディレクトリ配下の構成がもっと良くなる気がしています。- ex.) Domain に関連するテストコードはコチラにまとめた方が良いかも。
- => Laravel の
tests/
は Laravel のスコープだけテストする。 - Laravel 側にまとめると、せっかく切り離したドメイン領域と繋がってしまう為。
- => Laravel の
- ex.)
tests/
配下のテストコードの見直し。-
tests/Unit
配下で DB接続までしてしまっているので、モックで対応する。 -
tests/Feature
でクラス間の結合をテストする。
-
- ex.) Domain Model の配置構成を改善する。
-
Model/
ディレクトリがあった方がService
と並列に捉えやすいかもしれない。
-
- ex.)
Infrastructure/
の位置付けに悩み。- いまの構成だとサブドメインごとに別々のものが出来上がりそう。
- ★ 以下の "Implementing Domain-Driven Design" のサンプルが参考になりそう。
- ex.) Domain に関連するテストコードはコチラにまとめた方が良いかも。
- Domain Object は 抽象クラス を継承するとより分かりやすくなりそう。
- ex.)
Value Object
の抽象クラスを作り継承するなど。
- ex.)
- 拡張Exception の配置場所に悩んでいる。
- =>
Domain/
と並列よりは、直下の方がスッキリした。
- =>
- DB 接続以外の Infrastructure も実装してみたい。
- ex.) SaaS 接続、メール送信、外部サービス認証、etc.
- CI との接続
- ex.) CircleCI との連動など。
- 共通クラスをどこに配置するか悩み。
感想
- 機能が全然ない割にコード量が膨大で驚き。
- DDD 以前に習熟しておくべき技術要素が盛りだくさん。
- TDD
- AOP
- Repository Pattern
- 設計手法
- git / ブランチ戦略(GitFlow) / GitHub
- ...etc.
- 成瀬さんがお話しされてた通り、チームに適用するときは何かしらのコード生成ツールを自作して導入しないと、メンバーはとっかかりづらい。
- UnitTest 作りながら実装しないと盛大にデグレが発生する。
- 事前の設計が非常に大事。
今後
繰り返しになりますが、『ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本』は非常に面白かったですし、入門書としてとても読みやすかったです。
本格的に DDD の道に入るべく、積読中の以下の本もちゃんと読もうと思います。