この記事は、int や string の代わりに使用する値はプリミティブ型だけど値が特定の意味を持つため型にして区別したい時に使用する immutable(不変性:一度作ったら以降に内容が変化しないよう) なオブジェクトを実現ための C# での実装例の紹介です。
この ValueObject の使い道ですが、例えば、主に単一フィールドを対象にして、中身は string なんだけど名前を表すから Name クラス、JSON 文字列だから string 型ではなく JSON クラス、double 型だが単位はミリだから Millimeter クラスのように型を作成して(昔あった、LPSTR型のようなエイリアスとして代用する)場合や、設計モデル上のドメイン内の(immutable な性質を持つ)データをクラスとして表現する際の実装に便利に使用できる実装の紹介になります。
ちなみにドメイン駆動設計における ValueObject の意義・意図および一般的な実装方法論は他の技術系サイトや書籍で繰り返し説明されているためこの記事中ではこれ以上詳しく説明しません。
実装環境
この記事は以下の環境で実装、動作確認しています。
- .NET Core 3.1、C# 8.0
- Visual Studio 2019
実装コード
通常のValueObjectの実装
単一型
単一の値を表す ValueObject の実装例は以下の通りです。readonly struct と immutable化 で値を表します。必要に応じて演算子の operator を足したりしますがそこにドメイン固有の処理が入らないように注意しましょう。
// SinleValueObjectSample.cs // 単一型のValueObjectのテンプレ public readonly struct SinleValueObjectSample : IEquatable<SinleValueObjectSample> { readonly string value; public SinleValueObjectSample(string value) => this.value = value; public string Value => this.value; public static implicit operator string(SinleValueObjectSample value) { return value.value; } public static implicit operator SinleValueObjectSample(string value) { return new SinleValueObjectSample(value); } public bool Equals(SinleValueObjectSample other) => this.value.Equals(other.value); public override bool Equals(object obj) { return obj is SinleValueObjectSample _obj && this.Equals(_obj); } public override int GetHashCode() => this.value.GetHashCode(); public override string ToString() => this.value; public static bool operator ==(in SinleValueObjectSample x, in SinleValueObjectSample y) => x.value.Equals(y.value); public static bool operator !=(in SinleValueObjectSample x, in SinleValueObjectSample y) => !x.value.Equals(y.value); }
複合型
基本型 & immutable なオブジェクトを複数組み合わせる複合型の ValueObject の実装は以下の通りです。あまり意味を持たせすぎたり、ドメイン固有の処理を入れるとすぐ負債化するのでそういったものは極力方に入れないようにします。
// 複合型のValueObjectのテンプレ public readonly struct MultiValueObject : IEquatable<MultiValueObject> { // immutable public readonly int A; public readonly SinleValueObjectSample B; public readonly SinleValueObjectSample C; // immutable constructor public MultiValueObject(int a, SinleValueObjectSample b, SinleValueObjectSample c) { this.A = a; this.B = b; this.C = c; } // 演算子のオーバーライド public static bool operator ==(MultiValueObject a, MultiValueObject b) => Equals(a, b); public static bool operator !=(MultiValueObject a, MultiValueObject b) => !Equals(a, b); // 等値比較演算子の実装 public override bool Equals(object obj) => (obj is MultiValueObject _obj) && this.Equals(_obj); // IEquatable<T> の implement public bool Equals(MultiValueObject other) { if (other is null) { return false; } // 個別に記述する return ReferenceEquals(this, other) || this.A == other.A && this.B == other.B && this.C == other.C; } public override int GetHashCode() { unchecked { var hashCode = this.A; hashCode = (hashCode * 397) ^ this.B.GetHashCode(); hashCode = (hashCode * 397) ^ this.C.GetHashCode(); return hashCode; } } }
共通基底クラス版(非推奨)
以降かなりの分量を書いておいて申し訳ありませんが、以下内容はパフォーマンスが非常に悪いので全然オススメではありません。前述の内容で十分なので以下興味があればお読みください。
MSのサイトに値オブジェクトを実装するというページがあるのですがこのページに共通部分は基底クラスに切り分けるみたいな事が書いてあったので、その実装例も紹介したいと思います。MSサイト内のコードから少しブラッシュアップしつています。
先述の基本実装例だと実装量が多いので基底クラス側で「immutable に値を保持できること」、「内容で等値判定したい」、「保持する値はnullを許可しない」などの共通の性質を基底クラスに抽出し、ValueObject はこれを継承することで実現していきます。
単一型
まずは単一のフィールドを対象とした値オブジェクの実装です。
// ValueObject.cs /// <summary> /// 値オブジェクトを表します。 /// </summary> public abstract class ValueObject<T> : IEquatable<ValueObject<T>> { // // Fields // - - - - - - - - - - - - - - - - - - - - private readonly T Value; // immutable // // Operators // - - - - - - - - - - - - - - - - - - - - public static bool operator ==(ValueObject<T> a, ValueObject<T> b) { if (!(a is null)) return a.Equals(b); else if (!(b is null)) return b.Equals(a); return true; } public static bool operator !=(ValueObject<T> a, ValueObject<T> b) => !(a == b); //public static implicit operator T(ValueObject<T> v) => v.Value.Clone(); // // Constructors // - - - - - - - - - - - - - - - - - - - - public ValueObject(T value) => this.Value = value ?? throw new ArgumentNullException(); // // Public Methods // - - - - - - - - - - - - - - - - - - - - /// <summary> /// <see cref="object.Equals(object)"/> をオーバーライドします。 /// </summary> public override bool Equals(object obj) { return obj is ValueObject<T> other && this.Value.Equals(other.Value); } /// <summary> /// 他の <see cref="ValueObject{T}"/> と比較します。 /// </summary> public bool Equals(ValueObject<T> other) { if (other is null) { return false; } return EqualityComparer<T>.Default.Equals(this.Value, other.Value); } /// <summary> /// <see cref="object.GetHashCode"/> をオーバーライドします。 /// </summary> public override int GetHashCode() => this.Value.GetHashCode(); // // Non-Public Methods // - - - - - - - - - - - - - - - - - - - - protected virtual bool Validate(T v) => !(v is null); }
使い方は以下の通りです。例えば string の値を何らかのキー要素として扱いた場合は以下のように実装します。
// Key.cs // 実装例(1) // T が string 型の ValueObject public class Key : ValueObject<string> { // 必要があれば定義 public static readonly Key Key1 = new Key("type1"); public static readonly Key Key2 = new Key("type2"); // 完全コンストラクターパターンで最低限これだけ宣言すればよい public Key(string value) : base(value) { } } // 使用方法 string _a = "a"; Key a = new Key(_a); string _b = "b"; Key b = new Key(_b); // 中身の値ベースで等値判定 if (a == b) { Console.WriteLine("a == b"); }
比較演算子を実装しているので実際は中身の値で比較を行なっています。
また、自作のクラスを ValueObject 化することもできます。指定するクラス内でも immutable を保証しないといけないのでひと手間必要です。
// 実装例(2) // 自作のクラス public class Sample { public string Name { get; set; } public object Clone() => new Sample() { Name = this.Name }; // DeepCopy public override bool Equals(object obj) { if (obj == null) return false; if (obj is Sample s) return s.Name == this.Name; return false; } public override int GetHashCode() { // ... } } // T が 自作型の値オブジェクト public class SampleValue : ValueObject<Sample> { public SampleValue(Sample value) : base(value) { } } // 宣言方法 var s1 = new Sample() { Name = "Tako"; } a = new SampleValue(s1); c = new SampeValue(null); //null はエラーになる
自作の型を ValueObject にする場合そのオブジェクトで Equals をオーバーライドして中身ベースの比較を実装する必要があります。
また以下のように、 既存APIと互換させる事もできます。
// 実装例(3) // JSONを表すクラス public class Json : ValueObject<string> { public Json(string value) : base(value) { } } // こういった既存APIへの値の受け渡しが変換コンストラクタによってシームレスに操作できる Json json = new Json("{ \"root:\" [\"ABC\", \"DEF\", \"GHI\"] }"); var s = JsonUtility.FromJson<Sample>(json);
この実装はUnity上で動作させた場合の GCAlloc は生成と比較時は基本の実装と同等、動作速度は1.3-1.5倍程度です(微妙…w
複合型
複合型の ValueObject の場合、複数のフィールドで1つの ValueObject を構成します。2つの string があり1つめが名前、2つ目が住所など複数の値のセットが一つのクラスを表します。MSのサイトに掲載されているのはこっちですね。ただ、パフォーマンスが向上するようにいくらか書き直しています。
/// <summary> /// 複合型の値オブジェクトの基底クラスです。 /// </summary> public abstract class CompositeValueObject<T> : IEquatable<CompositeValueObject<T>> { // // Fields // - - - - - - - - - - - - - - - - - - - - private readonly object[] cache; // // Props // - - - - - - - - - - - - - - - - - - - - /// <summary> /// オブジェクトが持つフィールドの数を定義します。 /// </summary> protected abstract int FieldCount { get; } // // Operators // - - - - - - - - - - - - - - - - - - - - // 同じ型どうし比較 public static bool operator ==(CompositeValueObject<T> a, CompositeValueObject<T> b) { if (!(a is null)) return a.Equals(b); else if (!(b is null)) return b.Equals(a); return true; } public static bool operator !=(CompositeValueObject<T> a, CompositeValueObject<T> b) => !(a == b); // // Constructors // - - - - - - - - - - - - - - - - - - - - public CompositeValueObject() => this.cache = new object[this.FieldCount]; // // Pulbic Methods // - - - - - - - - - - - - - - - - - - - - /// <summary> /// <see cref="object.Equals(object)"/> をオーバーライドし、等値判定を行います。 /// </summary> public override bool Equals(object obj) { return (obj is CompositeValueObject<T> b) && this.Equals(b); } /// <summary> /// <see cref="object.GetHashCode"/> をオーバーライドし、オブジェクトのハッシュ計算を行います。 /// </summary> public override int GetHashCode() { unchecked { int sum = 0; foreach (var item in this.__GetEqualityComponents()) { if (item is null) { sum ^= 0; } else { sum ^= item.GetHashCode(); } } return sum; } } /// <summary> /// <see cref="IEquatable.Equals(T)"/> を実装します。 /// </summary> public bool Equals(CompositeValueObject<T> other) { if (other is null) { return false; } var self = this.__GetEqualityComponents(); var bb = other.__GetEqualityComponents(); if (self.Length != bb.Length) { return false; } for (int i = 0; i < self.Length; i++) { if (self[i] != bb[i]) { return false; } } return true; } // // Non-Public Methods // - - - - - - - - - - - - - - - - - - - - /// <summary> /// 派生クラスで定義される immutable なフィールドを配列に列挙します /// </summary> protected abstract void GetEqualityComponents(object[] array); bool isInit; // 指定した値の中に null が含まれているかを確認する // true : 含まれている / false : 含まれていない protected bool ContainsNull(params object[] args) { foreach (object o in args) { if (o is null) return true; } return false; } // 各フィールドを列挙 private object[] __GetEqualityComponents() { if (!this.isInit) { this.GetEqualityComponents(this.cache); this.isInit = true; } return this.cache; } }
基底クラスでは複数のフィールドを等値判定する機能と、派生クラスに対し比較対象にするメンバー変数群の登録の要求する機能を実装しています。
具体的な使い方は以下の通りです。
/// <summary> /// 複合型の ValueObject /// </summary> public class Foo : CompositeValueObject { // 複合型値オブジェクトで管理する値 public string Name { get; private set; } public int ID { get; private set; } protected override int FieldCount => 2; public Foo(string name, int id) { // 自分のコンストラクタ内でチェックして代入する if (this.ContainsNull(name, id)) throw new ArgumentException(); this.Name = name; this.ID = id; } // 比較するメンバーの設定 protected override void GetEqualityComponents(object[] array) { array[0] = this.Name; array[1] = this.ID; } }
ちょっと単一型と中身の変数値へのアクセス方法が異なるのが気になるところではありますが、もし本当に気になるようであれば各々で改良してもよいと思います。
派生クラス側はこれを継承すれば割とすっきり実装できると思います。MSDN のサイト内の実装例ではメンバーの列挙に毎回 Linq を用いるなどで比較やハッシュ計算時の動作速度がちょっと遅めだったので、この実装例ではメンバーの列挙結果はキャッシュし、IEnumerable からの Linq 操作は通常の構文に展開して速度を向上させています。
この実装、動作速度は通常版の1.2-1.4倍程度と単一フィールドのよりマシな速度で動作しますが、1フィールド辺り90BくらいGCAllocが増加するので同じ構造で3つのintで56B対384Bの割合で不要にメモリを使用します(intだとobject配列に代入されるときにBox化が発生 + キャッシュ領域のオブジェクト配列分でヒープを使用)なので、高頻度で生成と破棄を繰り返すと問題になる可能性があります。
C#9.0はrecodeキーワードが登場
まぁ通常版が一番優れているという他ないのですが、MSのサイトに掲載されているアイデアは一見よさそうでしたが、資源が限られている環境ですごく微妙なので(そのまま使うと動作速度が12-15倍、メモリ消費量に至っては7万倍(56B:390.6KB)でやばい)昨今サーバーサイドでもクラウドだと計算量が少ないに越したことはないのでスニペットやジェネレーターでコード生成したほうがよさそうです。
また、C# 9.0 からこういった実装は recode キーワードで非常に簡単に実装できるようになります。recode キーワードを付与したクラスは上記のような実装がコンパイル時に自動で付与されるとのことです。ただ C# 9.0 を使えるのは .NET 5 以降のため、普段の業務システムや Unity の標準環境ににもう少後になりそうです。
以上です。