私でもわかるDDD④ 値オブジェクトとは

値オブジェクトについてまとめていきたいと思います。

・ドメイン駆動設計とは
・モデル
・値オブジェクト ←ここ
・エンティティ
・リポジトリ
・DIP


このあたりから実際の実装に沿った内容になっていきます✊

私はPHPとかReactとかやってるのでそんな感じで書いていきますが、他の言語でもだいたい同じように考えていけるでしょう。

☝「値」とは(プリミティブ型、オブジェクト型)

プログラミングとかやってると一番見るもの(?)、それが値です。


$name = "ちひろ";
$height = 153;
$isMan = false;
$child = { name: "ちひろ子", age: 1 };

とかの右辺が値ですね。

値には種類があり、↑の例だと順番に、string, number, boolean, Objectになります。

DDDにおいては、値とは以下のような特徴を持つものだとされています。

1.不変である(イミュータブル)

値は変更されることがない!ということです。

あれ?普通に変更できない?と思った方、それは代入のことを思っているはずです。


$name = "ちひろ";
$name = "花子";

これは代入です。代入は値の置き換えのためOK。取り替えるのはアリ。

だめなのは、以下のようなパターンです。(実際にはエラーになるコードの書き方ですよ)


$name = "ちひろ";
$name[0] = "や";

一文字目を「や」にして、「やひろ」にしたろ!と部分的に変更する行為、これはNGです。

あくまで値は宣言された時点で確定しており、変更したければ全体を置き換えよ、という考えです。

値って自由にいじれるイメージがあるのに(私だけ?)、意外と制約があるんですね〜。

2.交換できる

これは「1.不変である」を叶えるために必要な考えですね。値は変更したければ置き換えることが必要なため、交換できるという特徴を持ちます。



3.等価性

2つの値は「中身が同じ」ことにより、同じだとみなされるということです。

例えば、


$a = 5;
$b = 5;

違う変数名で「5」という値を宣言したとき、aとbは同じになります。a = b はtrue。

変数名を見ると違うものだけど、中身をみるとどっちも5だから同じでいいよね、ということです。

これはプリミティブ型の値であればすべてこのように中身を見て同じかどうかを確認できます。

注意したいのはオブジェクト型の時です。

オブジェクト型は、一般的には等価性ではなく同一性というもので判断されます。


$a = {count: 5};
$b = {count: 5};

このように2つのオブジェクトを作ったとき、中身は同じだけど a = b はfalseとなります。

これは、オブジェクト型は作られたときのメモリ上の住所で比較する、という同一性の特徴があるためです。

DDDの考えでは、ややこしいことに、オブジェクト型も等価性で判断されるべきとされています。

でも、オブジェクト型ではa = b はfalseになるんでしょう?どうやって中身が同じなら同じという判断をするのでしょうね?

ということで、「値オブジェクト」の説明に自然に入っていきますよ。

メモリ上の住所がどうのこうのについて

個人的に、コードを書いているときにメモリがどうのこうのを気にしたくない人です(気にしろ)

が、素通りはできないので、渋々理解しやすいように書いた備忘録が以下です。


オブジェクト型に入るもの:メモリ上の住所

それはまるで、内容が同じgoogleスプレッドシートを2つ(ファイル名がdog1, dog2とか)作るような感じ。内容は「ポチ」だけど、それぞれのURLが違うので違うファイルだよね。

→ファイル名dog1の中身をいじると、そのURLを共有している人すべてに変更が影響する(同じファイル見てるからね)

→つまりオブジェクト型は、データの格納場所を参照しているだけ。


プリミティブ型:データそのもの

それはまるで、ポチと書かれた紙をプリンターで印刷するようなもの。ポチと書かれた紙が2枚あったら、それは同じものと判定する。

印刷された内容を変更したかったら、また印刷し直すしかない。変更しても、他の人が持っているコピーに影響はない(みんな手元に自分用のコピーを持っているからね)





☝値オブジェクトとは

DDDでドメインの仕様を実現するためのモデルのひとつ値オブジェクトが出てきました!👏

値オブジェクトとは、上記に書いた値の特徴そのままを持った、オブジェクトです。は?

値をオブジェクトにするとは、値をクラスとして扱うということです。は?

例えば値にはstring型というものがありますが、同じように自分たちでドメインの仕様を表現できる値の型を作っちゃおうということです。

よくわかりませんね。実際に見ていきましょう。

さきほど、[ $name = "ちひろ"; ] がありました。この値はstring型ですね。

DDDでは、これを例えば以下のようなクラスにして、Name型として扱います。


class Name {
    public function __construct(
        public readonly string $value
    ) {}
}

// 使い方の例
$name = new Name("ちひろ");

Nameというclass名で、コンストラクタがあって、そこに値を渡すとvalueという名のプロパティ値になる仕様です。

「ちひろ」というstring値を引数にしてnewすると、Nameオブジェクトになります。おやおや、ただのstring値がName型というオリジナルの型になってしまいました。


なんでこんなことするのか?string型だとだめなのか?

まあ、普通に考えればstringのままでええやんってなりますね。私もしばらくそう思っていました。

しかし思い出していただきたい(見ていれば)。DDD③「モデル」にて記載しましたが、DDDにおいて各モデルは責任を持っているのです。

今回の例では「名前」というモデルになりますが、モデルに責任を持たせるためにはクラス化することが必要です。

なぜか?それは、メインプログラムが「これやって!」「あの情報ちょうだい!」とモデルに処理を丸投げするからです。

例えばドメインのルールとして「名前は3文字以上」というのがあるとします。

メインプログラムは「名前の入力があった、これは使える名前か?!」とNameに聞きます。

Nameは使えるかどうかを判断して返事する必要があります。

ただのstring型だとこの動きができないんですね〜。

なのでモデルはクラスとして定義して、名前が妥当かどうかを判定するロジック(ふるまい)を持つ必要があるのです。


class Name {
    public function __construct(
        public readonly string $value
    ) {
        $trimmedValue = trim($this->value); // 値の文字数を判定するふるまいを追加!
        if (mb_strlen($trimmedValue) < 3) {
            throw new InvalidArgumentException("名前は3文字以上!");
        }
  }
}

// 使い方の例
$name = new Name("ちひ"); // Nameくんが「これはだめ!」とエラーを返す
$name = new Name("ちひろ"); // Nameくんは「これはOK!」とNameオブジェクトを返す


この他にも、モデルとして持つべきふるまいであればなんでも入れられます。

名前に「佐藤」という文字が入っていれば「加藤」に変更して値を保持する、みたいなわけのわからんルールでも、それがドメインのルールなら入れればいいですね。

ちなみに忘れがちですが、string型にもふるまいはあります。lengthとかtrimとか。

そう考えると、この値に◯◯のふるまいを持たせたいけど、stringにはないふるまいだから、オリジナルの型を作っちゃおうという発想は妥当な気がします。


値オブジェクト、ちゃんと値の特徴3つ持ってる〜?

DDDでは値=値オブジェクトは最初に書いた3つの特徴を必ず持っていないといけません。

特徴を満たすためにどのように扱っていけばいいかを見ていきましょう。

不変である、交換できる

値オブジェクトにおける「値」はプロパティ値のことですが、この値を変更することはできません。

プロパティ値なんて自由に変更できそうなものですが、DDDで実装していくなら変更できてはいけないので、値をreadonlyやprivateにしたりして書き換えられないようにします。

その代わり、値と同じように「置き換え」で対応します。つまり、値オブジェクトを新しい値で作り直すというようにします。


class Name { public function __construct( public readonly string $value // valueをreadonlyにすることで、変更できないようにする ) {} } // 使い方の例 $name = new Name("ちひろ"); // 値オブジェクトを作った $name->value = "花子"; // これはNG!valueはreadonlyなので変更できない $name = new Name("花子"); // 新しい値でnewすればOK


等価性

値は等価性があるべき、しかしオブジェクト型は等価性ではなく同一性の判断になってしまうのでした。

困ったことに、いま、stringというプリミティブな値をNameクラスにしたことで、値はオブジェクトになりました。

プリミティブなstringの"ちひろ" と stringの"ちひろ" はイコールだったのに、

オブジェクトのnew Name("ちひろ") と new Name("ちひろ)はイコールではなくなってしまった!

等価性の特徴を持てない!値オブジェクトなんかにしなきゃよかった!😭



そんなこと言わないで(自答)!!

そういえば値オブジェクトにはふるまいを持たせられるのでしたね。

じゃあ、「値が同じかどうか判断する」というふるまいを持たせてはいかがでしょう。


class Name {
    public function __construct(
        public readonly string $value
    ) {}

    public function equals(Name $other): bool { // 中身が同じであればtrueを返すふるまいを追加!
        return $this->value === $other->value;
    }
}

// 使い方の例
$name1 = new Name("ちひろ");
$name2 = new Name("ちひろ");
// $name1 === $name2 の比較をしたいけど、オブジェクト型なのでfalseになってしまう
var_dump($name1->equals($name2)); // Nameオブジェクトに比較処理を依頼する!→trueになる

equalというメソッドを追加することで、無事に、オブジェクトだけど、「値が同じなら同じとみなす」という特徴をもたせることができました!

値がオブジェクトになっちゃってどうなることやらと思ったけど、よかったよかった。


☝どんなモデルが値オブジェクトになるのか

実はモデルには、値オブジェクトのほかにエンティティドメインサービスといったものがあります。

こいつらの説明はあとに回すとして、じゃあモデルのなかのどれを値オブジェクトとして扱えばいいのでしょうか。

以下3つは考え方の例です。実際は、ドメインとよ〜〜くにらめっこして決めていくものかと思います。

①値そのものがアイデンティティかどうか

「Price」なら「500」のように、その値を持つことがアイデンティティになっているものは値オブジェクトです

逆に、たとえば「Human」モデルがあったとすると、「住所」という値は引っ越しなどにより変わる可能性があります。

住所という値が変わってもHumanのアイデンティティには一般的には影響はないため、Humanは値オブジェクトではないでしょう。

②不変(Immutable)にできるかどうか

値オブジェクトは値の変更ができないので、もしプロパティ値を変える必要があるものなら値オブジェクトではありません。

ある「Price」が「500」という値をもつとき、その値が変わることはありません。もし「600」にしたいなら、新しいPriceオブジェクトを作るからです。

「Human」モデルの値として「体重」を持つとき、一般的に体重は日々変わるものですが、体重が変動しても「Human」が別人になることはありません。

オブジェクトを作り直さずに体重は変更したい、この場合は値オブジェクトにすべきではないでしょう。

③中身が同じなら同じにしてよいか

たとえば名前の重複チェックをしたいとき、nameAとnameBのオブジェクトの値がそれぞれ「ちひろ」だったら、その値(中身)で比較・判定したいはずです。これは値オブジェクトです。

逆に、Humanモデルを例に取ると、体重が50kgのオブジェクトが2つあるとき、体重が同じだからと同じ人間だと言っていいでしょうか。当然別の人かと思います。

この場合、中身が同じでも別のものとして扱いたいので、値オブジェクトにすべきではありません。

ということで、「値オブジェクト」についてでした。

個人的に、値とはなんなのか、プリミティブ型やオブジェクト型への基本的な理解が深まり、すごく楽しいところでした。

次は値オブジェクトとは異なるモデルである「エンティティ」についてまとめていきます!

0 件のコメント:

コメントを投稿

注目の投稿

blogger格闘記② コードブロックを使いたい