✏️

【DDD】値オブジェクトについて勉強内容をまとめてみる

に公開

はじめに

本記事は『ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本』を読んで学んだことを自分なりにまとめることを目的としています。

なお、本記事内のコード例はすべてC#で記述しています。

対象者

  • ドメイン駆動設計を学び始めた人
  • バリューオブジェクトとエンティティの違いについて理解したい人

用語について整理

ドメイン駆動設計について理解する上で、定義が曖昧になりがちな用語について整理します。

ドメイン

  • ソフトウェア開発において、「プログラムを適用する対象となる領域」のこと
  • 例えば図書館システムでは、本の貸し出し、返却がドメインに含まれる

モデル

  • 現実の事象や概念を抽象化したもの
  • 現実を忠実に再現するのではなく、ソフトウェアが責務を全うするために必要な情報を表現できればよい

ドメインモデル

  • ドメインの概念をモデリング(抽象化)して得られたモデル
  • 図書館システムにおける本棚は「貸し出し可能な書籍がわかる」ことが表現できればよく、「本棚は何段あるか」までは表現しなくて良い。重要なのは「本棚がどの本を所蔵しており、それが貸し出し中かどうか」がわかること

ドメインオブジェクト

  • 「ドメインモデル」をソフトウェアで動作するモジュールとして表現したもの(課題解決のための知識をコードで表現したもの)

値オブジェクト

値オブジェクトとは

値オブジェクト(Value Object)とは、システム固有の値を表現したものです。
例えば

  • 氏名
  • パスワード
  • (図書館のシステムの例における)書籍番号

など。

例:Passwordクラス

public class Password
{
    // プロパティ
    public string Value { get; }
    
    // コンストラクタ
    public Password (string value)
    {
        // 簡易的なバリデーションチェック
        if (string.IsNullOrWhiteSpace(value))
        {
            throw new ArgumentException("パスワードは空白にできません");
        }
        if (value.Length < 8)
        {
            throw new ArgumentException("パスワードは8文字以上である必要があります");
        }
        Value = value;
        }
}

氏名であれば「苗字 + 名前」、パスワードであれば「◯文字以上◯文字以下」「半角英数」といったように、そのシステムにおけるルールが存在します。こういった値を取り扱う際に、単にString等の型にするのではなく、システムに必要とされる処理に従って値を表現したオブジェクトのことを値オブジェクトと呼びます。

余談

開発の中で初めて値オブジェクトのコードを拝見したときのことを書いておきます。
当時、値オブジェクトについて理解していなかった私は「ははーん、Passwordクラスを独自で定義しとけば、stringよりも型として安全だな!」と思っておりました。
その解釈自体は間違っていなかったと思いますが、不足もしていました。
本書を読み進める中で、値オブジェクトへの断片的な理解を整理することができました。

値の性質

システム開発において、「値」を扱うことは日常的なことです。
そんな値が持つ代表的な性質は以下の3つです。

  • 不変
  • 交換可能
  • 等価性で比較される

不変と交換の違い

プログラミングにおいて、値を操作することは日常的な行為です。
そのため値が不変と言われると何だか矛盾しているように感じますが、値が不変であることと交換可能であることは異なります。

        // これは値の交換をしているのであり、値が変わったわけではない
        var password = "password123";
        System.Console.WriteLine(password); // password123と出力される
        password = "Hello456";
        System.Console.WriteLine(password); // Hello456と出力される

初期値"password123"の変数passwordが宣言され、途中でHello456が代入されてる、何の変哲もないありふれたコードです。
値の性質から考えると、変数の中身が交換されただけであり、"password123"という文字列自体は何も変化していないと言えます。

        // "password123"自体が"Hello456"に変化した(良くない)
        var password = "password123";
        "password123".ChangeTo("Hello456") // こんなメソッドは存在しない
        System.Console.WriteLine(password); // Hello456と出力される

このコードでは、"password123"という値自体が"Hello456"に変化しています。"password123"という文字列が急に"Hello456"として扱われるようになっては困りますよね。ユーザーは一生ログインできないでしょう。

等価性

エンティティと違い、値オブジェクトは中身の値が同じであれば等価とみなされるという性質を持ちます。
例えば、2つのパスワードオブジェクトが "password123" という値を持っていれば、それらは「等しい」と扱われます。

var a = new Password("password123");
var b = new Password("password123");

Console.WriteLine(a.Equals(b)); // trueになるべき

値オブジェクトのメリット

  • 表現力が増す(値が持つ意味や制約コードを読むだけでわかるようになる)
  • ロジックの散在を防ぐ
  • 不正な値を防ぐ
  • 誤った代入を防ぐ

個人的に特に重要だと思ったのは上2つです。

表現力が増す

極端ですが、下記のようなコードでは引数が文字列であることしかわかりません。

void Send(string to) // toが何なのかわかりにくい
{
    // 何らかの処理
}

EmailAddressを定義しておけば、コードの意図が明確になります。

void Send(EmailAddress to) // EmailAddressであることがわかる
{
    // 何らかの処理
}

ロジックの散在を防ぐ

コードの重複は、使用変更を格段に難しくします。
先述したPasswordクラスでは、「パスワードを空白にできない」「8文字以上で作成すること」の2つの仕様が存在します。

    // コンストラクタ
    public Password (string value)
    {
        // 簡易的なバリデーションチェック
        if (string.IsNullOrWhiteSpace(value))
        {
            throw new ArgumentException("パスワードは空白にできません");
        }
        if (value.Length < 8)
        {
            throw new ArgumentException("パスワードは8文字以上である必要があります");
        }
        Value = value;
        }

このロジックをパスワードの登録や変更に関する処理毎に記述していては、コードの保守性が低下してしまいます。
パスワードの値を操作したいのであれば、値オブジェクトを利用することで後々の運用・保守が行いやすくなります。

    void UpdatePassword (string password)
    {
        // 値オブジェクトを利用してパスワードを変更する
        var password = new Password(password);
    }

また、表現力が増すと関連して、値オブジェクト内にロジックを記述することはコードの自己文書化を進めることにつながります。開発者はPasswordクラスを見るだけでシステムにおけるパスワードの仕様を理解することができます。
コードで仕様を明確に表現できるに越したことはないということです。

どこまでを値オブジェクトにすべきか

一概にこれが正解とは言えず、どこまでを値オブジェクトにするかはその人・プロジェクト次第です。
本書では以下の点を判断基準として挙げていました。

  • そこにルールが存在しているか
  • それ単体で扱いたいか

良くある例としては氏名(姓+名)があると思います(本書でも何度も例として使用されていました)。
氏名には「姓+名」というルールが存在します。また、氏名は「姓」と「名」を単純な文字列で扱うよりも、「氏名」というひとつのまとまりとして取り扱った方が、システム上の意図が明確になります。
一方、「姓」と「名」個別に処理することがないのであれば、上記の判断基準から値オブジェクトにする必要はないでしょう。
重要なのは値オブジェクトを避けることではありません。値オブジェクトにすべきかどうかを見極めて、そう判断したのであれば大胆に実行すべきということです(『ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本』より引用)。

おわりに

ここまでお読みいただき、ありがとうございました。
思いの外長くなってしまったので、今回はここまでとします。
次はエンティティについてまとめたいですね!

参考

https://zenn.dev/chida/articles/aa2a63cdf2eb52

Discussion