より良いエンジニアを目指して

1日1つ。良くなる!上手くなる!

ドメイン駆動開発のValueオブジェクトは実装のテクニック足り得る

以前職場で行っていたドメイン駆動開発のValueオブジェクトについて、です。

ドメイン駆動開発

ドメイン駆動開発の主だった狙いとしては、デスクトップだろうとWebだろうと、クラウドだろうとオンプレだろうと、C#だろうとJavaだろうと、ビジネスロジックは変わらない。

そのビジネスロジック部分を作り込んでおきさえすれば、技術が変わっても対応できると私は思って取り組んでました。

Valueオブジェクト

ドメイン駆動開発におけるValueObjectの考えは、ドメイン駆動開発をしてなくても取り入れた方がいい概念です。

詳しいことは以下の説明が参考になりますね。

little-hands.hatenablog.com

私も当時のリーダーにお札で説明されたのを思い出します。

同じ人間でも人によって違うので、これは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と定義していれば楽ですよね。

web.motormagazine.co.jp

    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を適用した方がいいと感じるシーンはあります。

実装のテクニックとして使っていきたい考えだと捉えています。