はじめに
記事を書く時間が取れないため、単発のTipsを書くことにしました。
ドメイン駆動設計の現場で気づいたことや、自分が理解に苦しんだポイントなどを簡易的に取り上げていきたいと思います。
この記事では、ドメイン駆動設計を始めようとしている方を対象に書いています。
「DDDの書籍は沢山読んだ!理解した気がする!」という段階の方に向いていると思います。
概念を理解したつもりでも、いざコードを書こうとすると手が止まってしまう。
今回は、アプリケーション層(ユースケース)とドメイン層の実装で判断に迷いそうな箇所を取り上げていきたいと思います。
※ 当記事はLaravelを例に書いております。
例題1
- 記事の投稿機能を持つアプリケーション
- 投稿された記事をユーザーに対して表示するユースケース
BAD Practice
<?php
// ユースケースのどこか
final class ShowPostUseCase
{
private PostRepositoryInterface $postRepository;
public function __construct(PostRepositoryInterface $postRepository)
{
$this->postRepository = $postRepository;
}
public function __invoke(Request $request): Post
{
$post = $this->getPostByRequest($request);
abort_if(!$post, 404);
$this->validatePost($post);
return $post;
}
private function getPostByRequest(Request $request): ?Post
{
return $this->postRepository->find(
PostId::of($request->input('id'))
);
}
private function validatePost(Post $post)
{
// 記事をユーザーに表示しても良いかの判断
// 公開意外のステータスや、公開期間外だったら見せない
if (!$post->hasPublicStatus() || !$post->hasActivePeriod())
abort(404);
}
}
<?php
// ドメインエンティティー
final class Post extends Entity
{
protected PostId $id;
protected string $title;
protected string $content;
protected PostStatus $status;
protected PostPublishPeriod $period;
public function hasPublicStatus(): bool
{
return $this->status->equals(PostStatus::PUBLIC);
}
public function hasActivePeriod(): bool
{
return $this->period->isActive();
}
}
手抜き満載のコードですが、とりあえず伝われば良しとします。
上記例のvalidatePost
メソッドにて、記事を一般ユーザーに見せても良いかどうかの確認を行っています。
何も問題ないように見えますが、実は必要以上のドメイン知識がユースケース層に含まれています。
NGの理由1
ずばり、ユースケース側で投稿記事のルールを知ることなるからNGです。
記事がステータスを持っていること、公開でない場合はユーザーに見せることができないということ、
公開期間が定められていること、期間外はユーザーに見せられないということなど、
ユースケースに必要以上の知識が求められます。
NGの理由2
1つの理由にあったルールは、いわゆるビジネスルールに該当するかと思います。
場合によっては、特定の権限を持つユーザーには非公開記事を見せる仕様にするかもしれないし、
記事が持つ属性によって、掲載期間が変動するかもしれない。
これらの条件(ビジネスルール)をユースケースで取り扱うとなれば、ドメインそのものの意味が薄れてしまうので、
ユースケースでは判断をせずに、ドメイン側で解決するようにしましょう。
Good Practice
<?php
final class ShowPostUseCase
{
private PostRepositoryInterface $postRepository;
public function __construct(PostRepositoryInterface $postRepository)
{
$this->postRepository = $postRepository;
}
public function __invoke(Request $request): ?Post
{
$post = $this->getPostByRequest($request);
abort_if(!$post, 404);
$this->validatePost($post);
return $post;
}
private function getPostByRequest(Request $request): ?Post
{
return $this->postRepository->find(
PostId::of($request->input('id'))
);
}
private function validatePost(Post $post)
{
// 記事をユーザーに表示しても良いかの判断
// 判断の中身はユースケースで知る必要がないので、ドメインに任せる
abort_if(!$post->isVisible(), 404);
}
}
<?php
final class Post extends Entity
{
protected PostId $id;
protected string $title;
protected string $content;
protected PostStatus $status;
protected PostPublishPeriod $period;
private function hasPublicStatus(): bool
{
return $this->status->equals(PostStatus::PUBLIC);
}
private function hasActivePeriod(): bool
{
return $this->period->isActive();
}
public function isVisible(): bool
{
return $this->hasPublicStatus() && $this->hasActivePeriod();
}
}
改善内容
- ユースケースでは投稿記事の閲覧条件(判断基準)を知る必要がないので、ドメイン層に判断を任せる
- ビジネスルールをドメインに集約したことで、ビジネスルールが増えても管理がしやすい。ユースケースによるバグが減らせる。
- コードの再利用性が自然と上がる
- テストが書きやすくなる(投稿エンティティーをモックして、ユースケースの振る舞いをテスト、ビジネスルールは単独でテストできるなど)
おわりに
ルールがシンプル過ぎる場合に、判断ミスがしやすい箇所だと感じましたので、
簡単なTipsとして投稿させて頂きました。ルールがシンプル過ぎるゆえに、
ビジネスロジックであることを見落としてしまうケースではないかと思います。
単なるオブジェクト指向とDDDの決定的な違いだと思ったりもします。
今回は、参考になる内容かどうかも知りたかったので、ニーズが合う場合は
また時間ができたタイミングで単発記事を書きたいと思います。