フレームワークへの依存度を下げるFLUDパターン (5)
フレームワークへの依存度を下げるFLUDパターン (4.5) の続きです。
FLUDパターンのおさらい
FLUDパターンは、以下の3階層のパターンです。
黒い矢印は依存(使うということ)を表します。白い矢印は汎化、つまりインターフェイスにして使うということを表します。
フレームワーク/ライブラリ層
- フレームワークやライブラリを直接使うコード
- フレームワークを使ったMVCコードは通常全てこの層になる
ユースケース層
- ユーザーがソフトウェアで行えるアクション
- ソフトウェアがなくなったら存在しなくなる
- ドメイン層のオブジェクトを使いユースケースを組み立てる
ドメイン層
- ドメイン知識
- ソフトウェアを適用して問題解決しようとする領域(ドメイン)に存在するルールや制約
- ソフトウェアがなくても存在する
- ユースケースによって変わることがない
FLUDパターンの基本ルール
- MVCコードから、ドメイン層とユースケース層のコードをできる限り分離する
- 下の層のコードが上の層のコードを取り扱う場合は、抽象型(インターフェース)に依存する
ガイドライン
ユースケースクラス
- 動作を表すため動詞で始める
UseCase
を最後に付ける(例、CreateNewsUseCase
)- 1クラス1パブリックメソッドとする(例、
run()
メソッド)
ディレクトリ構成
ディレクトリ構成は以下のようになります。
app/
├── Config
│ ├── Autoload.php
├── Controllers
│ ├── News.php
├── Models
│ └── NewsRepository.php
└── Views
└── news
├── create.php
├── overview.php
├── success.php
└── view.php
packages/
├── news ... Newsパッケージ
│ └── src
│ ├── Domain ... ドメイン層
│ │ └── News
│ │ ├── News.php
│ │ └── NewsRepositoryInterface.php
│ └── UseCase ... ユースケース層
│ └── News
│ ├── AbstractNewsUserCase.php
│ ├── CreateNewsUseCase.php
│ ├── GetNewsItemUseCase.php
│ ├── GetNewsListUseCase.php
│ └── NewsDto.php
└── shared ... Sharedパッケージ
└── src
└── GetterTrait.php
app/
以下のファイルはモデルのファイル名を変更しただけで配置はそのままにしています。
packages/
以下にドメイン層とユースケース層のコードが追加されています。
サンプルコード
ユースケース層
ユースケース
packages/news/src/UseCase/News/GetNewsListUseCase.php
<?php
declare(strict_types=1);
namespace Acme\News\UseCase\News;
use Acme\News\Domain\News\News;
class GetNewsListUseCase extends AbstractNewsUserCase
{
/**
* @return NewsDto[]
*/
public function run(): array
{
$newsList = $this->newsRepository->getNewsList();
$newsDtoList = array_map(function ($news) {
return new News(
$news->id,
$news->title,
$news->slug,
$news->body
);
}, $newsList);
return $newsDtoList;
}
}
DTO
packages/news/src/UseCase/News/NewsDto.php
<?php
declare(strict_types=1);
namespace Acme\News\UseCase\News;
use Acme\Shared\GetterTrait;
class NewsDto
{
use GetterTrait;
private $id;
private $title;
private $slug;
private $body;
public function __construct(
int $id,
string $title,
string $slug,
string $body
) {
$this->id = $id;
$this->title = $title;
$this->slug = $slug;
$this->body = $body;
}
}
ドメイン層
リポジトリ
packages/news/src/Domain/News/NewsRepositoryInterface.php
<?php
declare(strict_types=1);
namespace Acme\News\Domain\News;
interface NewsRepositoryInterface
{
/**
* @return News[]
*/
public function getNewsList(): array;
public function getNewsBySlug(string $slug): ?News;
public function addNews(News $news): void;
}
エンティティ
packages/news/src/Domain/News/News.php
<?php
declare(strict_types=1);
namespace Acme\News\Domain\News;
use Acme\Shared\GetterTrait;
class News
{
use GetterTrait;
private $id;
private $title;
private $slug;
private $body;
public function __construct(
?int $id,
string $title,
string $slug,
string $body
) {
$this->id = $id;
$this->title = $title;
$this->slug = $slug;
$this->body = $body;
}
}
何故こんなことをするのか?
もともとのコードは app/
以下のMVCのコードだけで動作していました。
FLUDパターンにしたことで、packages/
以下のドメイン層とユースケース層のコードが増えました。
これは、コードを書くことが好きすぎて、少しでも書くコードを増やそうとして増やしているわけではありません。
目的は、ドメイン層とユースケース層のコードを長期間運用可能にすることです。フレームワークやライブラリと密結合しないようにし、たとえフレームワークやライブラリが変更されても動作し続けるようにするためです。
また、本来ドメイン層にあるコードがフレームワークを利用することで、余計な制約を受けないようにするためです。OOPによるモデルの表現力を損なわないようにするためです。
例えば、ドメイン層のクラスがフレームワークのModelクラスを継承しているとしたら、本来必要ない継承関係がコードに含まれることになります。モデルとコードが解離しています。複雑なドメインモデルをOOPでコードにより表現することが妨げられます。
何故このサンプルは無駄に感じるのか?
CodeIgniter4のチュートリアルのコードをFLUDパターンに変更しました。このコードを見れば、何故、こんな無駄なことをするのだろうと疑問に思う人がいるかも知れません。
また、チュートリアルがこうなっていたら、なんだか同じようなコードをいくつも書かないといけなくて面倒だなと思うかも知れません。
これは、このサンプルのドメイン層のコードが薄いからです。要するに、複雑ではなくロジックもほぼありません。
本来、FLUDパターンは複雑なドメイン層をきれいにモデリングして、長期間運用可能にするためのものです。仮に、このチュートリアルのNewsサイトがこれでほぼ完成するとしたら(実際にNewsサイトが本当に単純なままかはわかりませんが)、FLUDパターンはオーバーヘッドが大きすぎます。
本来は、ドメイン層のクラスがもっともっと増えて、複雑になるようなケースで使うべきものです。
まとめ
- フレームワークへの依存度を下げる方法として「FLUDパターン」があります。
- FLUDパターンは3階層のシンプルなパターンです。
- FLUDパターンは長期間運用される複雑なドメインで利用することが適したパターンです。
- CodeIgniter4でもFLUDパターンを適用して、フレームワークへの依存度を下げることが可能です。
参考
Date: 2021/12/24