WHITEPLUS TechBlog

株式会社ホワイトプラスのエンジニアによる開発ブログです。

DDD入門者が学んだ、クラスにロジックを持たせるときの考え方の基礎

これはWHITEPLUS Advent Calendar 2019の18日目の記事です。

はじめに

最近入社した私は業務知識を短期間で習得するため、オンボーディングとしてベテランエンジニアとペアプロで開発を進めています。 その時の様子はこちらの記事にまとめましたので、読んでいただくとペアプロの雰囲気を感じられると思います。

ホワイトプラスが運営するリネットではドメイン駆動設計(DDD)を取り入れた開発をしており、今回私は初めてDDDを実践しました。 開発を進める中でクラスにロジックをどのように持たせるのかが肝だと感じており、その基本的な考え方の一部をTodoアプリを例にまとめました。

クラスに仕事を与える

Todoの登録時にタイトルと期限日を指定して登録する場合、それぞれを表現するクラスを用意します。 (DueDateのプロパティであるDateクラスは日付を表現するクラス)

f:id:atsukg:20191216121620p:plain

期限日を指定しないで登録する際、DBにはnullではなく未定の日付形式を入れておきたいとします。

// NG
$todo = new Todo(
    $id,
    $title,
    new DueDate(Date::distantFuture())
);

このときの未定の日付をクライアント側で生成すると、生成時にどんな値を入れるかを毎回意識しなければならないし、日付未定の値が変わった時に呼び出し元をすべて直さないといけなくなります。

なので、生成はDueDateクラスに任せます。

// OK
$todo = new Todo(
    $id,
    $title,
    DueDate::undefined()
);

class DueDate
{
    private $value;

    public function __construct(Date $value)
    {
        $this->value = $value;
    }
 
    public static function undefined(): self  // 未定の日付を生成する処理を定義
    {
        return new self(Date::distantFuture());
    }
}

f:id:atsukg:20191216105800p:plain

クラスの仕事を考える

今度はTodoの完了処理をする時、期限内に終えられたかどうかを判定する処理を書きます。 完了日クラス(CompletedDate)を新たに作成し、与えられた期限日をもとに期限内かどうかを判定すると良さそうです。

// NG
class CompletedDate
{
    public function __construct(Date $date)
    {
        $this->value = $date;
    }

    public function isPastDue(DueDate $dueDate): bool
    {
        return $this->value->isAfter($dueDate->value());  // DueDateクラスが仕事をしていない
    }
}

これだと「期限内かどうか」のロジックがCompletedDateクラスに書かれていて、DueDateクラスが仕事をしていません。DueDateクラスの仕事を考えた時に、期限日より前か後かを判定するのがメインになるので、「与えられた日付が期限内かどうか」はDueDateクラスに持たせ、CompletedDateクラスはそれを使うようにしたいです。

f:id:atsukg:20191216121815p:plain

// OK
class CompletedDate
{
    public function __construct(Date $date)
    {
        $this->value = $date;
    }

    public function isPastDue(DueDate $dueDate): bool
    {
        return $dueDate->isPast($this->value);
    }
}

class DueDate
{
    public function __construct(Date $value)
    {
        $this->value = $value;
    }

    public function isPast(Date $date): bool
    {
        return $this->value->before($date);  // 期限日を過ぎているかどうかチェック
    }
}

クラスを用意した時にどんな仕事を持たせていくのかある程度想像しておくことが大事です。

クラスの仕事を内部に閉じ込める

Todoの完了処理をサービスクラスに書きます。 完了処理時点で期限日を過ぎていた場合は、過ぎた日数を記録しておきたいとします。

// NG
class TodoCompletionService
{
    public function completeTodo($id, $completedDate): void
    {
        $todo = $this->todoRepository->findTodo($id);
        if ($completedDate->isPastDue($todo->dueDate())) {
            $this->warningRepository->record($id, $this->diffDate());
        }
    }

    // ロジックがサービスクラスに流出
    private function diffDate(DueDate $dueDate, CompletedDate $completedDate): DateRange
    {
        return new DateRange($dueDate->value(), $completedDate->value());
    }
}

差分を表すDateRangeクラスの生成ロジックがサービス側に流出してしまっています。DueDateとCompletedDateのgetterで必要な情報を抜き出して生成するのではなく、情報を持っているクラスに任せます。ここでは、完了した時に完了日と期限日の差分を取得するので、差分の生成はCompletedDateクラスで行うのが良さそうです。

f:id:atsukg:20191216121943p:plain

// OK
class TodoCompletionService
{
    public function completeTodo($id, $completedDate): void
    {
        $todo = $this->todoRepository->findTodo($id);
        if ($completedDate->isPastDue($todo->dueDate())) {
            $this->warningRepository->record($id, $completedDate->diff($todo->dueDate));
        }
    }
}

class CompletedDate
{
    public function __construct(Date $value)
    {
        $this->value = $value;
    }

    public function diff(DueDate $dueDate): DateRange
    {
        return new DateRange($dueDate->value(), $this->date);
    }
}

最後に

DDDを実践すると概念をクラスで表現するため多くのクラスを作成しますが、仕事の分担や組み合わせ方を上手く整理していくことで 実際の業務をコードに落とし込むことができ、理解しやすいものになると感じています。

ホワイトプラスではDDDを実践したいエンジニアを募集していますので、興味があればご応募してみてください。

open.talentio.com