1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

実務で使える「等価性」の設計(C#, DDD, EFCore)

Last updated at Posted at 2025-10-20

DDDを学び始めると、必ず「エンティティ」と「値オブジェクト」でつまずくと思います。
書籍やサンプルでは、record型を使用する例が多いですが、実務ではrecordを使用しない設計をしたい!という場合も多いはずです。

この記事では、recordを使用せずに、実務で使える等価性の設計方法をまとめます。

ざっくりエンティティと値オブジェクトの違い

区分 意味 等価性の基準 状態
Entity 「誰か(何か)そのもの」 IDで判定 可変
Value Object 「性質や値の組み合わせ」 値の一致で判定 不変

Entity(例:社員クラス)

同じ名前でも社員番号が異なれば別の人物扱いです。
IDが同じなら属性(名前)が変わっても同一人物として扱います。

public sealed class Employee : EntityBase<Employee>
{
    public int Id { get; private set; }  // 不変(ORMのValueGenerateOnAddを設定していることを前提)
    public string Name { get; private set; }

    // EFCore では引数無のコンストラクタが必要
    private Employee() { }
    // 完全コンストラクタパターン
    private Employee(string name)
    {
        Name = name;
    }

    // ファクトリメソッドで生成
    public static Employee Create(string name)
        => new Employee(name);

    // 可変プロパティの操作
    public void ReName(string name)
        => this.Name = name;
    
    protected override bool EqualsCore(Employee other)
    {
        return Id == other.Id;
    }

    protected override int GetHashCodeCore()
    {
        return Id.GetHashCode();
    }
}

// 識別子(Id)が異なるため、名前が同じでも別の人扱いとなる
// Id は DB 側で自動採番される前提
var yamada1 = Employee.Create("山田太郎");  // Id = 1
var yamada2 = Employee.Create("山田太郎");  // Id = 2
Console.WriteLine(yamada1.Equals(yamada2)); // False

yamada1.ReName("山田花子");
Console.WriteLine(yamada1.Name);  // 山田花子

ポイント

  • ID基準で等価にする
    (IDとかないよ!という場合は、業務キーを等価判定に入れて、IDはサロゲートキー(連番)にするっていう方法もあります)
  • プロパティを操作する場合は、setterを公開せずに、ReName()越しに変更する(カプセル化)
  • 完全コンストラクタパターンを使用することで安全性を上げる

Value Object(例:社員コード)

構成する値が同じなら同一とみなし、不変に設計する。

public sealed class ProductCode : ValueObject<ProductCode>
{
    public int Code { get; }  // ここは private set; を付ける必要はありません。(後の説明で理解できると思います。)

    private ProductCode(int code)
    {
        // ビジネスロジックはここに配置
        if (code < 0)
            throw new InvalidInputException("商品コードは、0以上を入力してください。");  // 例外は自作する
        Code = code;
    }

    public static ProductCode Create(int code)
        => new ProductCode(code);
    
    protected override bool EqualsCore(ProductCode other)
    {
        return Code == other.Code;
    }

    protected override int GetHashCodeCore()
    {
        return Code.GetHashCode();
    }
}

// Chair1 と Chair2 は同一であるが、
// Chair3は商品コードがことなるので同一でない
var Chair1 = ProductCode.Create(12345);
var Chair2 = ProductCode.Create(12345);
var Chair3 = ProductCode.Create(67890);
Console.WriteLine(Chair1.Equals(Chair2)); // True
Console.WriteLine(Chair1.Equals(Chair3)); // False

ポイント

  • 不変オブジェクトとしてCodeはgetterのみ
  • 等価性の判定にはVOの全プロパティを入れる
  • バリデーションなどドメインルールはコンストラクタに集中させる(間違ってもCreate()とかに入れたらダメです!)

Baseクラス

EntityBaseの実装

public abstract class EntityBase<T> where T : EntityBase<T>
{
    public override bool Equals(object? obj)
    {
        var vo = obj as T;
        if (vo is null)
        {
            return false;
        }

        return EqualsCore(vo);
    }

    public static bool operator ==(EntityBase<T> vo1,
        EntityBase<T> vo2)
    {
        return Equals(vo1, vo2);
    }

    public static bool operator !=(EntityBase<T> vo1,
        EntityBase<T> vo2)
    {
        return !Equals(vo1, vo2);
    }

    protected abstract bool EqualsCore(T other);
    protected abstract int GetHashCodeCore();

    public override string? ToString()
    {
        return base.ToString();
    }

    public override int GetHashCode()
    {
        return GetHashCodeCore();
    }
}

ValueObjectの実装

public abstract class ValueObject<T> where T : ValueObject<T>
{
    public override bool Equals(object? obj)
    {
        var vo = obj as T;
        if (vo is null)
        {
            return false;
        }

        return EqualsCore(vo);
    }

    public static bool operator ==(ValueObject<T> vo1,
        ValueObject<T> vo2)
    {
        return Equals(vo1, vo2);
    }

    public static bool operator !=(ValueObject<T> vo1,
        ValueObject<T> vo2)
    {
        return !Equals(vo1, vo2);
    }

    protected abstract bool EqualsCore(T other);
    protected abstract int GetHashCodeCore();

    public override string? ToString()
    {
        return base.ToString();
    }

    public override int GetHashCode()
    {
        return GetHashCodeCore();
    }
}

まとめ

  • 等価性の基準を明確化しておくと、テスト・リポジトリ・集約境界がする
  • recordを使用しなくても、堅牢な等価性実装は可能である
  • 等価性の判定を間違えるとあり得ないレコードがDBに保存される危険性がある(チーム内でタイポした人がいて、原因究明に4時間以上費やしました。)間違えそうだなという人はrecordを使用するのが安全だと思います。
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?