😋

PHPでバリューオブジェクト

2024/03/29に公開

話のはじめに

今回はバリューオブジェクトのお話です。
メリットなどを主にお伝えできればと思ってます。

PHPにおける型とドメイン

phpであるかどうかに関わらず、プログラムを書くときにデータを使って演算します。
演算とは判断・加工・計算です。
この時演算の対象になるものの多くは基本型と呼ばれるものに分類されるでしょう。
https://www.php.net/manual/ja/language.types.type-system.php
この基本型の中でも皆さんが特によく使われるのは整数、文字列、配列、オブジェクトなどでしょうか。
これらの基本型はドメインの約束事に従って判断、加工、計算されるということになります。

例えば、

  • 金額に関することであれば、100円単位に丸めたり、割引処理をしたりする
  • 名前に関することであれば、姓と名を繋げてフルネームにしたり、最後に様を付ける

といった調子ですね。

処理と型付け

ドメインでの約束事に従って書かれるプログラム処理はもちろん間違いが入り込まないことを望まれます。
そのためにも型というものは存在します。
例えばメソッドの引数にタイプヒントを指定するなどです。

ここでドメインオブジェクトがなぜ必要かというお話をするために、少しタイプヒントに関する小噺をさせてください。

小噺

あなたはある会社でエンジニアをやっています。
フロント、バックエンド両方を触っています。
ドメインのとある処理、従業員情報をフロントに表示するため、情報を羅列した文字列の生成に関するメソッド処理を書いていました。
実装自体はほぼ完了しましたが、メソッド引数へのタイプヒントをつけ忘れていたので、厳密性を高めるためにタイプヒントの実装を追加しました。
出来上がったコードはこちらです。

function getPersonData(string $name, int $age, int $departmentNum) :string
{
    return "氏名: {$name}, 年齢: {$age}, 部署番号: {$departmentNum}";
}

$personName = "kaziki";
$personAge = 25;
$personDepartmentNum = 1;
echo getPersonData($personName, $personAge, $personDepartmentNum);
// 氏名: kaziki, 年齢: 25, 部署番号: 1

特に問題なく動きそうですね。
それであなたはgetPersonData()を使用してフロント画面の表示に関する実装を引き続き行い、リリースまで漕ぎ着けました。

その後しばらくして、別のフロント画面で同じように表示したいという要求があったとしましよう。
あなたは忙しかったので新人のエンジニアさんにこの仕事を託しました。
新人さんはこのメソッドを使い回して修正をしましたがコードに誤りがあります。
コードはこちらです。

$personName = "kazikazi";
$personDepartmentNum = 2;
$personAge = 30;
echo getPersonData($personName, $personDepartmentNum, $personAge);

// 氏名: kazikazi, 年齢: 2, 部署番号: 30

誤りに気づきましたか。

年齢と登録番号の表示が入れ替わっていますね。
原因は関数に対して引数を入れ込む作業で、引数の順番を間違えてしまったからです。
新人さんは間違いに気づかず実装を続けてしまいました。
あなたはコードレビューの段階で気づいたのでその事を指摘しました。
レビューの段階で気づいたので良かったのですが、もし気づかなったら間違った表示が本番に入ってしまうところでした。

小噺の振り返り

ここまでの話の中で改善すべきところはどこだったのでしょうか?
新人さんの不注意を是正すべきだったのでしょうか?
もしくはコード自体がわかりにくかったのでしょうか?

ここでは特に悪い人探しをすることはしませんが、バリューオブジェクト(値オブジェクト)を導入するとこのメソッドの引数に対する割当順番のミスも防げたかもしれません。

バリューオブジェクトって何?

バリューオブジェクトは値を扱うためのクラスを用意する手法です。

コード例

先ほどのコードに適用してみます。
年齢、部署番号の値をバリューオブジェクトで表現してみます。

class Age
{
    private int $value;
    
    public function __construct($v) {
        $this->value = $v;
    }
    
    public function displayAge() {
    return "年齢: " . $this->value;
    }
}

class DepartmentNum
{
    public const MIN = 1;
    public const MAX = 5;
    private int $value;
    
    public function __construct($v){
        if ($v < self::MIN || $v > self::MAX) {
            throw new Exception();
        }
        $this->value = $v;
    }
    
    public function displayDepartmentNum()
    {
        return "部署番号: " . $this->value;
    }
}

年齢、部署番号をそれぞれAge、DepartmentNumとしてバリューオブジェクトで表しています。
このバリューオブジェクトを使用して、先ほどの関数を書き直すと以下のようになります。

function getPersonData(string $name, Age $age, DepartmentNum $departmentNum) :string
{
    return "氏名: {$name}, {$age->displayAge()}, {$departmentNum->displayDepartmentNum()}";
}

$age = new Age(25);
$departmentNum = new DepartmentNum(1);
echo getPersonData("kaziki", $age, $departmentNum);
// 氏名: kaziki, 年齢: 25, 部署番号: 1

何がいいの?

型が厳格になる

先ほど新人さんのエピソードを思い出して下さい。
関数に対して引数を割り当てる順番を間違えてしまったことが原因で表示の間違いがありました。
それをバリューオブジェクト適用後のコードで再現させてみましょう。

$age = new Age(30);
$departmentNum = new DepartmentNum(2);
echo getPersonData("kazikazi", $departmentNum, $age);

この処理を実行すると引数の割当が間違っていることになるので型エラーが発生します。
きっと新人さんも自分の間違いに気づいてくれることでしょう。
このように最初に基本型のintとして指定されいた型がより厳格な型になりました。

値の許容範囲を制限できる

基本型と呼ばれる型はドメイン内では決して扱わないであろう値も許容します。
例えば整数、integerを使用する時指定できる数字の範囲は -2147483648 ~ 2147483647 まであります。
一方で部署番号であれば、何万何億という部署が存在するような組織は現実に存在するでしょうか。
先ほどのドメインオブジェクトのコードでは、そういったドメイン上では考えられないような値を弾く処理がありました。

if ($v < self::MIN || $v > self::MAX) {
    throw new Exception();
}

データとして許容される範囲を限定的にできるだけでなく、ドメインで扱いたい値を可視化できます。

気をつけること

バリューオブジェクトは不変にしましょう

オブジェクトが状態遷移をするようになると、そのオブジェクトが現在どのような値であるか気にしなければなりません。
オブジェクトの値が参照のタイミングで思わぬ値に遷移していることはコードの可読性を下げ、心配事を増やします。
ですので、値が別の値を取る時には新たなオブジェクトを作り出すようにして、setterメソッドを作らないようにしましょう。

class Age
{
    private int $value;
    
    public function __construct($v) {
        $this->value = $v;
    }
    
    public function displayAge() {
    return "年齢: " . $this->value;
    }

    public function increment() {
        return new Age($this->value++);
    }
}

$oldAge = new Age(35);
$newAge = $oldAge->increment();

このようにオブジェクトの全てのプロパティはコンストラクタの処理で決定され、その後の処理によって変化しない実装を完全コンストラクタと表現します。

まとめ

バリューオブジェクトの導入のメリットを感じて頂けたましたか?
型の厳密性が高まり、ドメインで扱いたい値の範囲をコードにドキュメントのように残せるので堅牢性やドメインに対する理解を向上できます。
いささかの注意点に加えてチームメンバーにこれらのメリットに対する理解を求める必要性という導入コストは存在しますが、正しい使い方を認識して、使いこないしてみて下さい。

ソーシャルデータバンク テックブログ

Discussion