読者です 読者をやめる 読者になる 読者になる

Shin x Blog

PHPをメインにWebシステムを開発してます。Webシステム開発チームの技術サポートも行っています。

Amazon Dash Button にみるドメイン特化クラスのヒント

この投稿は、PHP Advent Calendar 2016 - Qiita の 7 日目です。

DDD のようなドメインを意識した開発手法でなくても、ドメインコンテキストで必要な操作のみを実装したクラスを作ってみましょう話です。

Amazon Dash Button

Amazon Dash Button は、ボタンが一つだけあるデバイスです。このボタンを押すと、あらかじめ決められた商品の注文が Amazon に送信され、商品が配送されるというシンプルなものです。

サントリー天然水 Dash Button

このデバイスは、「商品を届けて欲しい」というユースケースに対して、ボタンを押すというシンプルなインターフェイスを実装しています。単にボタンを押すだけなので、その裏側でどのようにして実現されているかを知らずとも、誰もが利用できます。違う言い方をすれば、ボタンを押す(商品を注文する)ことしかできないので、操作方法を覚える必要もなく、想定外の使い方もやりようがありません。*1

このように、ドメインやそのユースケース、関心事に特化したインターフェイスを用意すれば、そのコンテキストの中では迷うことなく、自然に理解でき、扱い方を間違えることもありません。

ドメイン特化クラス

「ユーザに現在の年齢を尋ねる」というユースケースの実装を考えてみます。

User クラスとして実装したのが、下記です。

現在の年齢を算出するには、生年月日が必要となるので、コンストラクタで渡しています。生年月日は、cakephp/chronos パッケージの Date クラスで表現しています。

年齢を尋ねるには、age メソッドを利用します。

<?php
namespace Acme\Generic;

use Cake\Chronos\Date;

class User
{
    /**
     * @var Date
     */
    private $dateOfBirth;

    /**
     * @param Date $dateOfBirth
     */
    public function __construct(Date $dateOfBirth)
    {
        $this->dateOfBirth = $dateOfBirth;
    }

    /**
     * @return int
     */
    public function age(): int
    {
        return $this->dateOfBirth->age;
    }
}

このコードを実行するのが、下記です。生年月日をコンストラクタを渡して、age メソッドを呼ぶだけなのでシンプルです。

<?php
// 現在日を設定
// Chronos::setTestNow(Chronos::create(2016, 12, 8, 12, 34, 56));

$user = new User(Date::create(2000, 1, 1));
echo $user->age(), PHP_EOL; // 16

このままでも動作は問題は無いのですが、気になるのは、Date クラスです。これは、日付を扱うクラスなので、挙動としては問題ありません。ですが、このクラスは汎用クラスなので、ドメインのコンテキストで扱う生年月日ではありません。プロパティ名を見れば、生年月日であることは想像できますが、より明示するために生年月日を示すクラスを実装します。

生年月日を示したのが、下記の DateOfBirth クラスです。内部表現としては、Date クラスを利用しているので、ただのラッパーのようにも見えます。ここで大事なのは、Date クラスのインスタンスを内包して、DateOfBirth クラスでは、年齢を算出する age メソッドのみを実装しているという点です。これにより、DateOfBirth クラスが担うのは、年齢を算出するだけということが分かります。

<?php
namespace Acme\Domain;

use Cake\Chronos\Date;

class DateOfBirth
{
    /**
     * @var Date
     */
    private $date;

    /**
     * @param Date $date
     */
    public function __construct(Date $date)
    {
        $this->date = $date;
    }

    /**
     * @return int
     */
    public function age(): int
    {
        return $this->date->age;
    }
}

DateOfBirth クラスを使った User クラスは下記です。コンストラクタの型宣言が変わっているだけなのですが、Date クラスに比べると生年月日であることが読み取れます。もちろん、User クラス内で生年月日を操作する際も age メソッドしかできないので、間違いようがありません。

もし、配送日などの別の日付クラスが存在したとしても、誤って User クラスに与えることができません。要求されている仕様が分かりやすいだけでなく、誤った引数に対するガードとしても有効です。

<?php
namespace Acme\Domain;

class User
{
    /**
     * @var DateOfBirth
     */
    private $dateOfBirth;

    /**
     * @param DateOfBirth $dateOfBirth
     */
    public function __construct(DateOfBirth $dateOfBirth)
    {
        $this->dateOfBirth = $dateOfBirth;
    }

    /**
     * @return int
     */
    public function age(): int
    {
        return $this->dateOfBirth->age();
    }
}

ドメイン特化クラス実装のポイント

このようなドメインに特化したクラスを実装するには色々な方法がありますが、いくつかポイントを書きます。

操作を必要最低限に絞る

PHP には、数多くの組み込みクラスやライブラリ、フレームワークがあります。一般的なデータ型を示すためのクラスは探せば見つかると思うので、多くの場合、それをベースとして利用するでしょう。

その場合、継承ではなく上記の DateOfBirth クラスのように委譲を使って実装します。

例えば、DateOfBirth クラスを Date クラスを継承して実装したとします。この時、DateOfBirth クラスは、Date クラスの public / protected メソッドを受け継ぐので、Date クラスと同様の操作が可能です。Date クラスには、日付の加算、減算といったメソッドがありますが、このコンテキストの生年月日に関してはそういった操作は不要です。不要なだけでなく、うっかり使ってしまうと想定外の動作を招く場合もあります。

こうした違いは、IDE で開発すると明確です。委譲版 DateOfBirth は、age メソッドのみ補完候補となります。一方、継承版では、age メソッドに加えて、Date クラスのメソッドが多数出現します。一つしかボタンが無いのと、たくさんのボタンがあるのと、どちらが利用者として分かりやすいかは一目瞭然です。

  • 委譲版 DateOfBirth

f:id:shin1x1:20161207135012p:plain

  • 継承版 DateOfBirth

f:id:shin1x1:20161207135019p:plain

対象に特化する(汎用性を求めない)

上記、継承が使われる理由として、DRY を意識しすぎるため、継承で実装の再利用を行いたいというものがあります。

例えば、このシステムに配送日といったモデルがあったとしましょう。生年月日も配送日も日付を示すものなので、Date クラスを基底クラスとし、それぞれ継承したとします。しかし、生年月日と配送日を同種のものとして扱う場面というのは、現実世界ではあまり思い当たりません。

ドメインに特化したクラスの実装では、汎用的に実装する必要はなく、対象にのみフォーカスして実装します。

これは、一般的な汎用クラスとは指向している方向が異なります。あくまでドメインに特化したクラスなので、汎用性を考える必要はありません。*2

もし、実装を共有したいという目的であれば、共通処理を行う別クラスを実装して、そこに委譲するか、トレイトにまとめるという手法を取ると良いでしょう。

一つの概念を表す小さなクラスを作る

今回の生年月日のように、一つの概念のみを示すクラスを作るようにします。凝集度が高い小さなクラスを作ることで、実装しやすく、使いやすく、テストしやすいクラスになります。

こうしたクラスは取り回しが楽なので、どのようなアーキテクチャであっても、取り入れることができます。特に、DDD のようなドメインを意識したアーキテクチャでなくても、一部だけでも、一つの概念だけでも、簡単に取り込むことができます。

実際の開発では、ValueObject(値オブジェクト)として実装するところから始めると良いです。

コンテキストによって異なる

DDD でも「境界付けられたコンテキスト」があるように、コンテキストというのは重要な概念です。

同じ「生年月日」でも、本エントリで扱う「生年月日」と別のシステムで扱う「生年月日」では、あるべき振る舞いが異なるかもしれません。今回は年齢の算出のみを実装しましたが、「今日が誕生日かどうか」「今月が誕生月か」といった振る舞いが必要となる場合もあるでしょう。むしろ、この2つのみが関心事であれば、「年齢を算出する」は不要かもしれません。

このように、同じ用語、概念であっても、コンテキストによって求められるものは異なります。つまり、生年月日とはこうあるべきといった汎用の生年月日クラスを実装したとしても、それがそのまま適用できるかどうかは、コンテキストによって異なるということです。

ユースケース

こうしたドメインに特化したクラスを少しづつ導入したい場合は、ValueObject として導入するのが実装するのが良いでしょう。ValueObject にすれば、操作が限定できるだけでなく、オブジェクトの不変条件を内包したり、内部表現をカプセル化することができます。

例えば、下記のようなものが考えられます。

  • 日付(日付ライブラリは高機能なわり、ドメインで要求されるものは一部。)
  • 数値、特に演算が行われるもの(金額、ポイントなど。演算の限定、制約条件の付与。)
  • 要求仕様が明確なデータ(商品コードなど。)

それ以外でも、サービスのように状態を保持しない処理もドメイン特化クラスで実装しやすい箇所です。

例えば、暗号処理なら、実際の暗号化や複合処理は別クラスで実装し、後はユースケース別のクラスからこれらのクラスを委譲で利用して、暗号化や複合を行うという場合に使えます。暗号処理は、暗号化と複合をペアで行う場合が多いので、ベアごとにクラスを分けると、ここで処理で暗号化したものは、あそこで複合しているというのが分かりやすくなります。

さいごに

こうしたドメインに特化したクラスを作るというのは、一見面倒なように見えますが、やってみると面白いものです。

生年月日も、汎用日付オブジェクトで表現すれば、それで終わりです。一方、生年月日クラスを実装すると、生年月日にどのような振る舞いが求められているかを考えることになります。このような視点で仕様を見ていけば、一つ一つの用語にも関心がいき、より深くドメインを理解して、コードに表現するようになります。

これは、教条的なものではなく、むしろ感覚に近いもので、こうしたプラクティスを重ねていくことで、身についていくもののように思います。

ドメインに必要な部品を少しづつ作っていき、それを組み合わせて構築していくという作業は、安心感もあり、とても楽しい作業です。身近なところから、取り組んでみてはどうでしょうか。

blog.shin1x1.com

*1:誤って押して、想定外の注文が飛ぶことはありますが

*2:ドメインコンテキスト内で汎用性を持たせるということは有り得ます。