2020-10-30に更新

C# の DateTime で説明する ValueObject

ValueObject イズ 何?

ValueObject とは「何らかの概念についての状態と振る舞いを持ち、その概念の責務を果たすオブジェクト」です。
ValueObject はそれが定義される上で満たすべきいくつかの要件があります。
IDDD 本で示されている ValueObject が満たすべき要件を確認してみたいと思います。

No. 要件 説明
1 計測・定量化・説明 そのドメイン内の何かを計測したり定量化したり、あるいは説明したりする。
2 不変 状態を不変に保つことができる。
3 概念的な統一体 関連する属性を不可欠な単位として組み合わせることで、概念的な統一体を形成する。
4 交換可能性 計測値や説明が変わったときには、全体を完全に置き換えられる。
5 値の等価性 値が等しいかどうかを、他と比較できる。
6 副作用のない振る舞い 協力関係にあるその他の概念に、副作用のない振る舞いを提供する。

引用 : ヴァーン・ヴァーノン. 実践ドメイン駆動設計

どうでしょう、初見で理解出来ましたか?
「全然わからん」となったそこのあなた。もしあなたが C#er なら ValueObject は怖くありません。
あなたは既に ValueObject を知っているからです。

人間誰しも自分の理解していない事柄に対しては拒絶感を抱きやすいものですが「既に知っている」となればどうでしょうか。
この記事は「まだ ValueObject をあまり知らない方」に向けた「ValueObject に対する拒絶感を和らげる」ために書かれた記事です。

DateTime で説明する ValueObject

C# には日時を取り扱うための構造体として DateTime があります。C#に触れたことがある人ならば一度は見たことがあるのではないでしょうか。
この DateTime ですが実はある種の ValueObject です。
嘘ではありません。
それでは DateTime で ValueObject の要件を説明していきましょう。

計測・定量化・説明

言わずもがな DateTime は日時を示す為の構造体です。
日時に関しておおよそ一般的な定義や振る舞いを提供してくれます。

不変

DateTime は読み取り専用構造体として定義されておりインスタンス生成後に状態を変更することは出来ません。
「状態を不変に保つことができる」と言うより「状態が不変であることを強制されている」と言う方が正しいかもしれません。

    public readonly partial struct DateTime : IComparable, IFormattable, IConvertible, IComparable<DateTime>, IEquatable<DateTime>, ISerializable, ISpanFormattable

引用 : .NET Core Source Browser DateTime.cs

既に存在する DateTime インスタンスとは別の日時が欲しい場合は常に新しいインスタンスとして生成されます。
一例として AddDays メソッドの定義を見てみます。

        public DateTime AddDays (double value);

引用 : DateTime.AddDays(Double) メソッド | Microsoft Docs

戻り値が DateTime になっていますが、この戻り値について MS Docs の説明にはこう書いてあります。

このインスタンスの値に、指定された日数を加算した新しい DateTime を返します。

引用 : DateTime.AddDays(Double) メソッド | Microsoft Docs

上記の通り新しいインスタンスを返却します。自身の状態を変更して自身の参照を返す訳ではありません。
AddMonths メソッドや AddYears メソッドも同様です。

概念的な統一体

ここに「2000」という数字があるとします。この数字は、これだけでは殆ど価値のある意味を持ちません。
ですが「年」という言葉と組み合わせると「2000 年」になり、只の「2000」よりも具体的な意味を示すようになります。

DateTime には「年」以外にも構成要素があります。
月、日、時刻、現地時間なのか UTC なのか、などです。
これらの属性が組み合わせることで「現地時間 2000 年 4 月 1 日 12:00」の様な日時として更に意味のある概念を示せるようになります。

この様に複数の属性で何かを説明したり意味のある概念を示すものが「概念的な統一体」です。

交換可能性

DateTime は「不変」であるため、交換が不可能であると値を変更することが出来なくなってしまいます。
その為 DateTime は交換可能でなければなりません。

            // dateA に「2000 年 1 月 1 日」のインスタンスをセットしたあと 「2000 年 1 月 2 日」のインスタンスに「交換」する。
            var dateA = new DateTime(2000, 1, 1);
            dateA = new DateTime(2000, 1, 2);

C# 的には当たり前のことかもしれませんが ValueObject として必要な要件です。

値の等価性

C# において等価性について何も考慮されていないクラス/構造体では、内部で保持している値が同じだとしても異なるインスタンス同士は等価と判断されません。

        // 参照型のクラス
        public sealed class User
        {
            public User(string id)
            {
                this.Id = id;
            }

            public string Id { get; }
        }

        public void Main()
        {
            var userA001 = new User("1234");
            var userA002 = new User("1234");

            // 同じ ID なので等価として判断して欲しい。
            if(userA001 == userA002)
            {
                Console.WriteLine("同一ユーザー"); // しかし出力されない。
                return;
            }

            Console.WriteLine("別ユーザー"); // 出力される。
        }

ですが同じ値を保持しているのであればそれらのインスタンス同士を等価として判断して欲しい場面があります。
C# にはそんな時の為に異なるインスタンス同士の等価性判断を提供する IEquatable インターフェースや == 演算子が用意されており、DateTime でもこれらが実装されています。

        public static bool operator ==(DateTime d1, DateTime d2) => d1.InternalTicks == d2.InternalTicks;

引用 : .NET Core Source Browser DateTime.cs

これにより DateTime では異なるインスタンス同士を違和感なく等価かどうか判断出来るようになっています。

            var dateA = new DateTime(2000, 1, 1);
            var dateB = new DateTime(2000, 1, 1);
            Assert.IsTrue(dateA == dateB);

この様に等価かどうかを判断出来ることが「値の等価性」です。

副作用のない振る舞い

「副作用のない振る舞い」とは何かを出力するけれども自身の状態は変更しないものです。
引用 : ヴァーン・ヴァーノン. 実践ドメイン駆動設計

「不変」の要件と異なるのは「何かを出力するけれども」という点です。
DateTime がただ「不変」であるだけで良いのであれば AddDays メソッドや AddMonths メソッドは不要な筈です。
異なる日時のインスタンスは全てコンストラクタで生成すれば済むからです。

しかし新しいインスタンスが欲しくなる度にコンストラクタを呼び出していてはコードが冗長になる上にバグを埋め込む可能性も高くなります。

            var dateA = new DateTime(2000, 1, 31);
            // dateA の翌日が欲しかったが 31 日であることに気付かず日付に 1 を足してしまった。
            var dateB = new DateTime(dateA.Year, dateA.Month, dateA.Day + 1);

こういったことを防ぐ為に DateTime では AddDays などのメソッドを提供しています。
この様に『その概念に関する振る舞いをカプセル化して「不変」であることを保ちつつ何らかの値を返却する』ことが「副作用のない振る舞い」です。

DateTime は ValueObject

無事に DateTime で ValueObject の要件を全て説明することが出来ました。
同時に DateTime は ValueObject としての要件を満たしていることが分かりました。
「ValueObject 全然わからん」という方でも「つまり DateTime みたいなヤツだな」という理解が出来るようになったのではないでしょうか。

おまけ : ValueObject のポイント

ValueObject 6 個の要件は以下の様に 3 つのグループに分けることが出来ます。

  • 概念
    • 計測・定量化・説明
    • 概念的な統一体
  • 不変
    • 不変
    • 交換可能性
    • 副作用のない振る舞い
  • 等価性
    • 値の等価性

概念グループの 2 つの要件は両方とも ValueObject で何らかの概念を定義する為の要件なので同じグループです。

不変グループは ValueObject が「不変」であることが前提にあり、残りの 2 つは「不変」であるが故に生じる要件なので同じグループです。
「不変」であろうとすると自然に「交換可能性」と「副作用のない振る舞い」を満たす実装になります。

等価性グループは「値の等価性」だけのグループです。

自分で ValueObject を定義する際は「概念(のカプセル化)」、「不変」、「等価性」の 3 点を意識しておけば良いと思います。

Originally published at qiita.com
ツイッターでシェア
みんなに共有、忘れないようにメモ

kono 16

しがない C#er です。

Crieitは誰でも投稿できるサービスです。 是非記事の投稿をお願いします。どんな軽い内容でも投稿できます。

また、「こんな記事が読みたいけど見つからない!」という方は是非記事投稿リクエストボードへ!

有料記事を販売できるようになりました!

こじんまりと作業ログやメモ、進捗を書き残しておきたい方はボード機能をご利用ください。
ボードとは?

コメント