以前職場で行っていたドメイン駆動開発のValueオブジェクトについて、です。
ドメイン駆動開発
ドメイン駆動開発の主だった狙いとしては、デスクトップだろうとWebだろうと、クラウドだろうとオンプレだろうと、C#だろうとJavaだろうと、ビジネスロジックは変わらない。
そのビジネスロジック部分を作り込んでおきさえすれば、技術が変わっても対応できると私は思って取り組んでました。
Valueオブジェクト
ドメイン駆動開発におけるValueObjectの考えは、ドメイン駆動開発をしてなくても取り入れた方がいい概念です。
詳しいことは以下の説明が参考になりますね。
私も当時のリーダーにお札で説明されたのを思い出します。
同じ人間でも人によって違うので、これはEntity。ただし、お札の場合は同じ1000円なら、どれも同じです。
概念的な話よりも実装サイドで話をしたいので、実装する形で説明していきます。
例えば、以下のようなオブジェクトがあったとします。
class Person { // 自動車免許書番号 public string CarLicenseNumber {get; set;} // マイナンバー public string MyNumber {get; set;} // 名前とか性別とか他にもあるけど中略 }
自動車免許書番号もマイナンバーもstringで文字列型ですが、stringではなくValueObject化して見ます。
public class CarLicenseNumber:StringValueObjectBase { public CarLicenseNumber(string rawValue):base(rawValue) {} } public class MyNumber : StringValueObjectBase { public MyNumber(string rawValue):base(rawValue) {} } /// <summary> /// 単一の値を持つValueObjectを示すクラスです。 /// </summary> public abstract class StringValueObjectBase { /// <summary> /// コンストラクタ /// </summary> /// <param name="rawValue"></param> protected StringValueObjectBase(string rawValue) { RawValue = rawValue; } /// <summary> /// 本来の生の値 /// </summary> public string RawValue { get; } /// <inheritdoc/> public override bool Equals(object obj) { if (null == obj || GetType() != obj.GetType()) { return false; } return this == obj as StringValueObjectBase; } /// <inheritdoc/> public override int GetHashCode() { return RawValue.GetHashCode(); } /// <summary> /// 等号の拡張 /// </summary> /// <param name="a"></param> /// <param name="b"></param> /// <returns></returns> public static bool operator ==(StringValueObjectBase a, StringValueObjectBase b) { if (a is null || b is null) { if (a is null && b is null) { return true; } return false; } return a.RawValue == b.RawValue; } /// <summary> /// 不等号の拡張 /// </summary> /// <param name="a"></param> /// <param name="b"></param> /// <returns></returns> public static bool operator !=(StringValueObjectBase a, StringValueObjectBase b) { if (a is null || b is null) { if (a is null && b is null) { return false; } return true; } return a.RawValue != b.RawValue; } }
StringValueObjectBaseは、Stringの値を内蔵し、この値を比較して、オブジェクトの等価性を判断するクラスです。
これを継承して、MyNumberとCarLicenseNumberを定義しています。
これらのクラスを適用すると以下のようになります。
class Person { // 自動車免許書番号 public CarLicenseNumber CarLicenseNumber {get; set;} // マイナンバー public MyNumber MyNumber {get; set;} // 名前とか性別とか他にもあるけど中略 }
こんなことをしていて意味あるのと当時は思っていました。わざわざValueObjectの基底になるクラスを定義してまで、と。
ただし、これにはメリットはあります。
利点1:型安全性による実装ミスを防げる。
しょうもない失敗ですが、
// マイナンバーに自動車免許書番号を代入してしまう
person.MyNumber = carLicenseNumber;
ということは起き得ますし、これはコードとしては通ってしまいます。
こんな失敗するわけないじゃないかと思うものですが、コードが複雑になるにつれ、こういうミスは起き得ます。
逆に、ValueObjectをきちんと定義しておけば、MyNumberクラスにCarLicenseNumberクラスを代入することはコンパイルエラーになりますので、こうしたしょうもないミスが減らせます。
利点2:拡張性
本来はこちらだと思います。
システムがより複雑になるにつれ、より細かい挙動を求めたくなっていきます。
マイナンバーが何文字までとか、記号は使われないなど、そうしたValidation処理を適用するなどして、モデルの表現力を高めたい時に対応しやすくなって行きます、
以下のサイトでは免許書番号で、免許の交付年や失効回数を取得することができるとあります。
こうしたことを実装するのにもValueObjectと定義していれば楽ですよね。
public class CarLicenseNumber:StringValueObjectBase { public CarLicenseNumber(string rawValue):base(rawValue) {} public int GetLostCount() { // 免許書の最後の番号は紛失回数 } public int GetGrantYear() { // 免許書の左から3・4番目のふた桁が交付年(西暦) } public bool Validate() { // 免許書番号は12桁 // 免許書番号は全て数字 } }
それだけではなく、何かしらの大きな変化が起きることがあるかもしれません。
本来、数字で扱われていたものが文字列で扱う必要が出た、など。
こうした時もValueObjectとして定義していれば柔軟に対応できます。
欠点もある
欠点もあります。冗長な実装の印象を持たれるかと思いますが、その分、時間がかかります。
このような実装を全てに施したら大変な時間がかかります。
なので最初から完璧に実装するよりは、
- 実装の手軽さがなくなるのでプロトタイピングでは後回し
- ここだというところにValueObjectに定義
- システム改修のタイミングでValueObjectを適用
というのが現実的かなというのが私の印象です。
ドメイン駆動開発していなくても ValueObjectを適用した方がいいと感じるシーンはあります。
実装のテクニックとして使っていきたい考えだと捉えています。