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

考える場所

ココロとカラダ、思考する全部

CQRSの小さな演習(番外) バリューオブジェクト

たとえば、エンティティがIDをもっていて、これをどうやって表現するかを考えてみます。MySQLでauto incrementの数値だからintかな?とか、UUIDだからStringかな?とかって考えますよね。だけど、その前に、エンティティにとってはそれが数値であっても文字列であっても、IDをもつ、ということに変わりはないはずです。

f:id:fukuchiharuki:20160416121345p:plain

そこでIdというオブジェクトを考えます。今回の演習ではIdのほかに、DoingとDoneAtを個別のクラスにしていて、それぞれString、String、Dateをラップしています。

class Id {
  String value;
}
class Doing {
  String value;
}
class DoneAt {
  Date value;
}

これはちょっと面倒なんじゃないの?と最初こそ思うのですが、実際に書いてみると分かります。引き寄せられるように部品が組み立っていきます。一度やってみてください。

値を確実に識別する

メソッドの引数や戻り値に型を指定しますね。引数でString idとすれば、文字列の値を受け取れますが、これが本当にIDなのかどうかは分かりません。Stringは文字列として間違いないですが、IDとして間違いないことを意味しません。変数名でidと言うことに拘束力がないからです。

この拘束力のなさはメソッドの呼び出しに不安定さを与えるような気がします。String idでIDの値を考えた場合、どこからがIDとして妥当な値なのかが見えなくなります。約束事で取り決めることはできるかもしれませんが、コード上の制約にすればそれがもっと強力になります。

イベントでもコマンドでも属性をバリューオブジェクトにしました。エンティティがもっているIDとイベントやコマンドがもっているIDは同じIdである、ということがコードとして表現することができます。すると、間違ってコードを組み立ててしまうことがグッと減ると思います。

値を変更しない

バリューオブジェクトは値そのものですから、途中で値を変えるようなことをしません。できないようにします。immutableです。

ドメインとして意味のあるオブジェクトである、という型をもっていても、その内容が書き換わっていくようでは困ります。3+2をやったときに、次回の35のはたらきをしたら大変ですよね。プリミティブな変数なら少なく(広く?)ともメソッドでスコープが切れますが、インスタンスはあちこちで参照されて保持される可能性がありますから、知らない間に破壊される(値として妥当でなくなる)と目も当てられません。バリューオブジェクトは完全コンストラクタか相当のもので生成することで、安心して利用することができます。

なお、Stringもimmutableです。が、それでもStringをラップしてドメインとして意味のあるバリューオブジェクトにするべきです。

そのデータの処理はそのオブジェクトでする

オブジェクト指向の、データと振る舞いを一体にする、という基本的な考え方に従えば、プリミティブな値をラップするのは自然なことです。Idの内部的な値がStringであっても、その値をIDとして処理することがあればIdにメソッドをもたせたいのです。

たとえば、IDをハッシュ化することがあったときに、次のようにはしたくなくて、

String str = id.toString();
MessageDigest md = MessageDigest.getInstance("SHA-1");
byte[] digest = md.digest(str.getBytes());

こうしたいのです。

byte[] digest = id.getDigest();

もっと言うとbyte[]を直接操作するのは不安定なので、こうした方がいいかもしれません。

DigestedId digestedId = id.digest();

データコンテナとそれを操作する処理、という構図は積極的に回避したいのです。ので、参照している側が内部の値をgetして直接処理するべきではないです。エンティティがそうならバリューオブジェクトも同じで、ドメインとしての意味をもつ個別のクラスにできるわけです。

本編

blog.fukuchiharuki.me