🔥

値オブジェクトを採用する動機や理由について

2022/11/21に公開

参考資料:「ドメイン駆動設計入門」2.5章

モチベーション

コードは原則適切な大きさに分割して(クラスなど)分散して定義するべきだが、クラスを増やすことに抵抗があることが多かったのでまとめてみる
その大体の基準を知り心理的ハードルをなくす為の値オブジェクトのモチベーションは以下にあげたようなこと

  • 値の自己主張性を強めて値を具体的にする
  • 不正値を存在させない
  • 誤った代入を防ぐ
  • ロジックの散在を防ぐ(高凝集化する)

1. 値の自己主張性を強めて値を具体的にする

変数や番号には「数字だけのもの」「ハイフンなど記号必須のもの」などいろんな物があるが、

var modelNumber = "a342234-100-1";

これだとぱっと見でmodelNumberが内容の細かいことがわからず、どこで定義され、どこのメソッドで使われているのかが上記の例ではわかりにくい。

void Method(String modelNumber){//なんとかStringであることだけわかる程度。
  //割愛
} 

値オブジェクトを用いて(「ModelNumberクラス」と概念として見立てて意義のあるまとまりにして)変数を表すとどうなるのか

class ModelNumber{
  private readonly string productCode //製品番号と
  private readonly string branch //枝番と
  private readonly string lot //ロット番号
  //の3種類の番号からなるクラスであると判明したのでだいぶStringの具体性が増した

  public ModelNumber(){//コンストラクタ省略
   // ガード節省略
  }

  public override string ToString(){
    //メソッド内容も割愛
  }
}

このようにクラスとして定義して意義のあるまとまりにすることで

  • 変数の具体性を増すことができる

2. 不正値を存在させない

たとえば「ユーザ名は3文字以上」という制約がああったとする。

var userName = "t";

これはプリミティブ型の観点からすると別に間違った値ではないが、「業務上のルールに違反した値」である。
そこでクラスとしてまとめたコンストラクタに、値が初期値として入ってくる時に不正な値を弾く処理「ガード節」を持たせる

class UserName{
  private readonly string value;
  public UserName(string value){
    if(value == null) throw new ArgumentNullException(nameof(value));
    if(value.Length < 3)throw new ArgumentExeption("ユーザ名は2文字以上。", nameof(value));

    this.value = value;
  }
}
  • UserNameクラスはガード節によって2文字以下を許さない形にしている。クラス内で不正値の存在可能性を除いておく

3. 誤った代入を防ぐ

代入は開発においてたびたび起こる操作であり、間違った代入をしてしまうことも起きうる。

User CreateUser(string name){//メソッド
  var user = new User();//インスタンス化
  user.Id = name;//代入 Idにnameを代入してしまった。いとわろし。
  return user;//
}

それを防ぐために値オブジェクトを採用する。具体的には

  • new User();してインスタンス化したオブジェクトuserのIdをUserIdクラスとし、nameもUserNameクラスとしてUser内で定義する。

  • nameはUserName型、userIdはUSerId型として型の不一致を判別できるのでエラーを防ぎやすい

という方針をとる。クラス設置数が多くなるが堅牢なコードにするために全然作ってOK。

User CreateUser(UserName name){//CreateUserメソッド
  var user = new User();
  user.Id = name; //Error!型の不一致
  return user;
}
class User{
  //UserクラスではフィールドをUSerIdなどとして型宣言しておくと
  //インスタンス化し変数Idを使う時にUserIdクラス型出なければ弾くことができる
  public UserId{get; set;}
  public UserName Name{get; set;}
}

(UserIdとUserNameクラスは以下。dartで書いてみた)

class UserId{
  private final String value;
  // UserIdの値オブジェクト。以下割愛
}
class UserName{
  private final String value;
  //UserNameの値オブジェクト。以下割愛
}
  • 変数などに値オブジェクトの「クラス型」を持たせ、開発者側が意識せずミスがあれば教えてくれる仕組みにすることで、よりエラーの起きにく設計とすることができる。

4.ロジックの散在の防止

  • 値オブジェクト利用することでロジックの散在を防げる。
    例としてUserNameの最小文字数を3に変更したい場合、ガード節の条件文を毎回変えていくと漏れや記入ミスの可能性が多いにある。そこで
    User
class UserName{
  private readonly string value;
  public UserName(string value){
    if(value == null) throw new ArgumentNullException(nameof(value));
    if(value.Length < 3)throw new ArgumentExeption("ユーザ名は2文字以上。", nameof(value));

    this.value = value;
  }
}
//UserNameに値オブジェクトとしてまとめておく。
//UserNameにコンストラクタのガード節として設定しておくことで
//ここを変更するだけで後は「クラス型」として宣言すれば堅牢さを維持したまま変更にも強くなれる。

  void CreateUser(string name){
    //ユーザの新規作成処理
    var userName = new Username(name);
    var user = new User(userName);
  }
  void UpdateUser(string id, string name){
    // ユーザの更新処理
    var userName = new UserName(name);
  }
  • ルール(ユーザ名の最小文字数)はUserNameクラスにまとめられることで、変更する箇所が一個だけで済む。

値オブジェクトのコンセプトの再確認

  • システム固有の値を作る

ことを目指している。

以上です。次は「エンティティって何すか」

c#で書こうと思ったんですけど色付いてくれなくて困ってたまにdartで書いてすんません。

GitHubで編集を提案

Discussion