0
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?

パターンから学習するドメイン駆動設計

0
Last updated at Posted at 2025-10-04

参考本

ドメイン駆動設計入門 ボトムアップでわかる! ドメイン駆動設計の基本

ドメイン駆動設計を学習するうえで登場する用語は、

  • モデリング・・・ソフトウェアにとって重要な概念を抽出すること
  • パターン・・・概念を実装に落とし込むためのパターン

の二つに大別される。

モデリングに関しては言葉による説明を理解し実践して学ぶほかないが、パターンに関しては詳細なサンプルをもって説明でき、初学者にとって理解しやすい。

そこで一旦は具体的なコードをベースにパターンを集中的に学習し、ボトムアップ的にドメイン駆動設計を理解しようという本。

パターンだけを取り入れる方法は軽量DDDと呼ばれ、推奨されていない。
ドメイン駆動設計が目指すことは決してパターンに終始することではなく、ドメインの本質に向き合うこと。パターンを理解した後はモデリングについても理解すべし、とも書いてあった。

ドメイン駆動設計とは何か

ドメインの知識に焦点をあてた設計手法。
ドメインとは「領域」の意味をもつ言葉であり、ソフトウェア開発におけるドメインは「プログラムを適用する対象となる領域」ことを指す。

会計システムであれば金銭や帳簿といったものがドメインに含まれ、物流システムであれば倉庫、貨物、輸送手段といったものがドメインに含まれる。

このようなドメインの知識からシステムにとって必要なものを抽出、抽象化(モデリング)されたものがドメインモデルであり、ドメインモデルをソフトウェアで動作するモジュールとして表現したものがドメインオブジェクトである。

パターンの全体像

ドメインの知識を表現するためのパターン

ドメインの知識をオブジェクト(=ドメインオブジェクト)として表現するパターン

  • 値オブジェクト
  • エンティティ
  • ドメインサービス

アプリケーションを実現するためのパターン

ドメインの知識を表現しただけではドメインの写しがコードとして表現されているだけに過ぎず、ソフトウェアに求められている必要は満たされていない。利用者の必要を満たすアプリケーションを構築するためのパターン

  • リポジトリ
  • アプリケーションサービス
  • ファクトリ

ドメインの知識を表現するより発展的なパターン

知識を表現するより発展的なパターン

  • 集約
  • 仕様

値オブジェクト

システムに登場する「金銭」「単価」「製品番号」「氏名」といった値をオブジェクトとして定義したもの

例、「氏名」を表現するFullNameクラス

class FullName
{
  private string FirstName;
  private string LastName;

  public FullName(string firstName, string lastName)
  {
    FirstName = firstName;
    LastName = lastName;  
  }
}

値オブジェクトを採用するメリット

  1. 表現力を増す
  2. 不正な値を存在させない
  3. ロジックの散在を防ぐ
1.表現力を増す

例えばある工業製品の製品番号(modelNumber)を、プリミティブな値で表現した時は下のようなコードとなる。

var modelNumber = "a20421-100-1";

上のコードを見れば直接代入されている値を確認できるため、製品番号は3種類の番号で構成されていると認識できる。
しかし下のようにスクリプト上に突然modelNumberという変数が現れたときには、modelNumberがどういったものなのかうかがい知ることは出来ない。

void Method(string modelNumber)//かろうじて文字列であることはわかる
{
  (略)
}

では値オブジェクトを使って製品番号を表したときはどうなるか

//製品番号を表すクラス(値オブジェクト)
class ModelNumber
{
  private readonly string productCode;
  private readonly string branch;
  private readonly string lot;

  public override string ToString()
  {
    return productCode + "-" + branch + "-" + lot;
  }
}

ModelNumberクラスの定義を確認してみれば、製品番号はプロダクトコード(productCode)と枝番(branch)とロット番号(lot)から構成されていることがわかる。無口な文字列と比べると大きな進歩!
値オブジェクトはその定義により自分がどういったものであるかを主張する自己文書化を推し進める。

2.不正な値を代入させない

「ユーザー名は3文字以上」といったようにシステムに存在する値には、ルールが存在する。値オブジェクトをうまく利用すると、ルールに反した値の存在を妨げることが出来る。

//ユーザー名を表す値オブジェクト
class UserName
{
  private readonly string value;

  public UserName(string value)
  {
    if(value == null) throw new ArgementNullException(nameof(value));
    if(value.Length < 3) throw new ArgementNullException("ユーザー名は3文字以上です", nameof(value));

    this.value = value;
  }
}

このUserNameクラスはコンストラクタで3文字未満のユーザー名を許可しないようになっている。異常な値はこのチェックにより許容されず、結果としてシステムは、ルールに従っていない不正な値の存在におびえる必要がなくなったのである。

3.ロジックの散在を防ぐ

あるシステムのユーザー新規作成処理をつくる場面を考える。
先程の例と同様に「ユーザー名は3文字以上」というルールがあるとする。
値オブジェクトを利用しない場合は、ユーザー新規作成処理を行う関数内で、入力値の確認が必要となる。

//ユーザー新規作成処理
void CreateUser(string name)
{
  if(value == null) throw new ArgementNullException(nameof(value));
  if(value.Length < 3) throw new ArgementNullException("ユーザー名は3文字以上です", nameof(value));

  var user = new User(name);
  (略)
}

局所的にこのコードは問題ないが、たとえば追加でユーザー情報を更新する処理もつくることになったとするとどうだろう。

//ユーザー情報更新処理
public void UpdateUser(int age, String name){
    if(age < 0){
        throw new IllegalException("年齢が0以上でありません");
    }
    if(name == null){
        throw new NullPointerException("名前を設定してください");
    }
    user.setAge(age);
    user.setName(name);
}

新規作成処理に書かれていたコードが更新処理にも記述され、コードは重複することになる。

このようなコードの重複が引き起こす弊害はルールの変更が必要となった時に表れる。
たとえば「ユーザー名の最小文字数」を変更したいとき、書き換える箇所が新規作成処理、更新処理の最低2箇所、今後の拡張によってはより多くの箇所を修正しなければならない。つまり変更難易度が跳ね上がる。

一方、値オブジェクトを採用する場合は、値オブジェクトのなかに値のルールをまとめることで、ルールの変更に対するコードの変更箇所が1箇所で済むようになる。

class UserName
{
  private readonly string value;

  public UserName(string value)
  {
    if(value == null) throw new ArgementNullException(nameof(value));
    //ここを変更するだけでよい
    if(value.Length < 3) throw new ArgementNullException("ユーザー名は3文字以上です", nameof(value));

    this.value = value;
  }
}

↓値オブジェクトを採用した場合の新規作成処理と更新処理

void CreateUser()
{
  var userName = new UserName(name);
  var user = new User(userName);
  ()
}

void UpdateUser(string name)
{
  var userName = new UserName(name);
  ()
}

何を値オブジェクトとして扱い、何をプリミティブデータのまま扱うか、の基準

→その値にルールが存在しているか
例.
「氏名」には「姓と名から構成される」というルールがある→値オブジェクトにする
「姓」や「名」にはシステム上の制限は特にない→値オブジェクトにしない

エンティティ

システム上でデータとして取り扱うことができるよう抽象的にモデル化された、現実世界の対象物や概念のこと。例えば、「学生」というエンティティをシステム上で表すために、「氏名」「学年」「学籍番号」という属性と「進級」「退学」「卒業」という操作の集合として定義する、といったモデル化が行われる。

//「学生」を表現するエンティティ
class Student
{
 //学籍番号
  private string studentNumber;
  //氏名
  private string name;
  //学年
  private string grade;
  
  //進級する
  void promote(){}
  //退学する
  void quit(){}
  //卒業する
  void graduate(){}
}

エンティティの性質

  1. 可変である
  2. 同じ属性であっても区別される
  3. 一意性のある「識別子」によってのみ識別される
1.可変である

人々がもつ年齢や身長といった属性が変化するのと同じように、エンティティの属性は変化することが許容されている

↓nameという属性が変更することが許されているUserエンティティ

//Userエンティティ
class User
{
  private string name;

  public User(string name)
  {
    ChangeName(name);
  }

  //名前を変更するためのメソッド
  public void ChangeName(string name)
  {
    if(name == null) throw new ArgumentNullException(nameof(name));
    if(name.Length < 3) throw new ArgumentNullException("ユーザー名は3文字以上です",nameof(name));

    this.name = name;
  }
}
2.同じ属性であっても区別される

値オブジェクトは同じ属性であれば同じものとして扱われるが、エンティティは同じ属性であっても区別される。

例えば、FirstName、LastNameという属性をもつUserNameという値オブジェクトの場合は
二つの属性の値が同じであるUserNameオブジェクト同士はまったく同じものとして扱われる
(例)
FirstName="しゅうま"、LastName="ほりい"、であるUserNameオブジェクトが2つあったらこれらはまったく同一とみなせる。

が、このような性質は、例えば人間には当てはめることが出来ない。
「ほりいしゅうま」という名前の人間が二人いたとしても、同一人物ということにはならない。

エンティティもこれと同様に、属性が同じだったとしても全く同じものであるということにはならない。

3.一意性のある「識別子」によってのみ識別される

エンティティを区別するには「識別子」が使われる。

//識別子(値オブジェクト)
class UserId
{
  ()
}

class User
{
  //識別子
  private readonly UserId
  private string name;

  public User(string name)
  {
    ChangeName(name);
  }

  public void ChangeName(string name)
  {
    ()
  }
}

逆に識別子が同一であれば、属性が変わったとしても同じものとしてみなされる。
ヤマダさん.jpg

何をエンティティとするかの判断基準

ライフサイクルが存在するか、が基準となる。例えばあるシステムのユーザーとしてUserエンティティを考えたとき、システムを利用するにあたって利用者がUserエンティティを生成し、必要に応じて利用していくうちにユーザー名を変更し、サービスから退会するときにUserエンティティを削除する。このようなライフサイクルがあるものをエンティティとする。

ドメインオブジェクトを定義するメリット

  • コードのドキュメント性が高まる
  • ドメインにおける変更をコードに伝えやすくなる

ドメインサービス

値オブジェクトやエンティティなどのドメインオブジェクトにはふるまいが記述される。しかし、ふるまいによっては値オブジェクトやエンティティに記述すると不自然になってしまうものも存在する。ドメインサービスはそういった不自然さを解消するためのオブジェクトである。

ユーザー名の重複を許さないための重複チェック関数exists()を定義したい。
しかしこれをUserエンティティのメソッドとして定義してしまうと、

User user = new User();
user.exists(user);

といったように重複の有無を自身に対して問い合わせているようなコードとなってしまう。これは不自然なので、解決策としてドメインサービスオブジェクトUserServiceを定義し、UserServiceのメソッドとしてexists()を定義する。すると

User user = new User();
UserServices userServices = new UserServices();

userServices.exists(user);

のようなコードとなり先ほどのものと比較して自然である。(第三者が重複チェックを行っている感じ)

ドメインサービス濫用の危険性

実はドメインオブジェクトのすべてのふるまいはドメインサービスに記述できてしまう。しかしこれをやってしまうと値オブジェクトやエンティティにはセッター、ゲッターしか残らなくなり、表現力の低下やロジックの点在を招いてしまう。(これを「ドメインモデル貧血症」という)
したがってドメインサービスに記述するものは「エンティティや値オブジェクトに記述すると不自然なふるまい」に限定するべきである。

まとめ

今回はドメインの知識を表現するためのパターンとして値オブジェクト、エンティティ、ドメインサービスを紹介した。次回はアプリケーションを実現するためのパターンである、リポジトリ、アプリケーションサービス、ファクトリをやりたい。

参考文献

ドメイン駆動設計入門 ボトムアップでわかる! ドメイン駆動設計の基本

0
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
0
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?