🐶

「ドメイン駆動設計入門」要点

2022/02/06に公開約18,600字

書籍

https://www.shoeisha.co.jp/book/detail/9784798150727

この書籍は、ドメイン駆動設計において重要なモデリングパターンの内、パターンを重点的に説明している
パターンを理解することで、モデリングを含めたドメイン駆動設計という大きなテーマを理解する準備ができる。

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

Gitリポジトリ

本書に登場するJavaのコードの一部を、「Rubyで表現してみる」「動かして理解する」ためのコードを置いています。

https://github.com/ito0804takuya/domein-driven-design

ドメイン駆動設計とは

ドメイン駆動設計のコンセプト

ビジネスの問題を解決するためにビジネスの理解を進め、ビジネスの表現をする
ビジネスとコードを結びつけて、継続的な改良ができるように枠組みを作ることで、ソフトウェアをより役立つものにする。

ドメインとは

ドメインとはプログラムを適用する対象となる領域のこと。 (ドメインの和訳 = 領域)

重要なのは、ドメインとは何か でなく、ドメインに含まれるものが何なのか。
つまり、システムで問題を解決する上で重要な知識は何か。

モデル、モデリングとは

モデルとは、現実の事象や概念を抽象化した概念。
抽象とは、抽出して象る(かたどる)ため、現実を忠実に再現しない。

では何を抽出したらいいのか?
例えば、物流システムにおいて、トラックは貨物を運ぶものと表現すればいい。
キーを回すとエンジンが動くということは不要。

モデリングとは、こういうように事象・概念を抽象化する作業のことを言う。
モデリングをした結果得られる結果がモデル。
(ドメイン駆動設計では、ドメインの概念をモデリングして得られたモデルをドメインモデルと言う。)

ドメインオブジェクトとは

ドメインオブジェクトとは、ドメインモデルをソフトウエアで動くモジュールとして表現(実装)したもの。

ドメインで起こった変化は、ドメインモデルを媒介として、ドメインオブジェクトに伝えられる(変更される)。
ドメインの概念 ⇔ ドメインモデル ⇔ ドメインオブジェクト

値オブジェクト

システム固有の値を表現するために定義したオブジェクト。

なぜ値オブジェクトを使うのか

システムで必要な処理にしたがって、システムならではの値の表現があるため(それを表現するため)

具体例 : 名前

例えば名前の姓を表現するとき、プリミティブな値では、色々な国の人の名前を表現することが難しい。
※ プリミティブ : intやstringなど、言語が元々用意してくれている型のこと。

プリミティブな値
fullname = "山田 太郎"
lastname = fullname.split(' ')[0] # 姓
p lastname # 山田

fullname = "john smith"
lastname = fullname.split(' ')[0] # 姓
p lastname # 姓はsmithなのに、johnと出力されてしまう
値オブジェクト
class Fullname
  attr_accessor :firstname, :lastname

  def initialize(firstname:, lastname:)
    @firstname, @lastname = firstname, lastname
  end
end

fullname = Fullname.new(firstname: "太郎", lastname: "山田")
p fullname.lastname # 山田

fullname = Fullname.new(firstname: "john", lastname: "smith")
p fullname.lastname # smith

どこまで値オブジェクトにするか

例えば、上では名前(姓名)はオブジェクトにしたが、は別々のオブジェクトにすべきか?
→それはシステムによって異なる。

値オブジェクトを使うモチベーション

1. 表現力が増す

定義したクラスを見ることで、その値オブジェクトがどういったものであるかが分かる
(= 自己文書化される。)

また、独自の振る舞いを定義できる

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

クラス内でバリデーションを設けることができるため、システム内に不正な値が存在しない。
(値オブジェクトにしない場合、使用する箇所すべてで不正な値でないかチェックしないといけなく、1箇所でも修正されないと破綻が起きる。)

3. 誤った代入を防ぐ

class User
  attr_accessor :id

  def self.create_user(name)
    user = User.new()
    user.id = name # 正しい代入?
    user
  end
end

user = User.create_user("田中")

このコードで、idnameが代入されていることは、正しいのか分からない。
(システムによっては、名前がidになっている可能性も捨てきれないため。)
UserIdオブジェクトをidプロパティに定義していれば、何が代入されるべきかが明確に分かる。

(Rubyでは3.0以降でRBS(Rubyの型定義のための言語)を使えば、IDEで型エラーを見つけれれるはず。)

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

ルールをクラス内でまとめることができる。

エンティティ

ドメイン駆動設計におけるエンティティとは、同一性(identity)で区別されるドメインオブジェクト。
※ ER図やORMで登場するエンティティとは別の概念。

これとは反対に、属性によって区別されるのが値オブジェクト

同一性とは

値オブジェクトは、等価性によって区別するため、属性が同じであれば全く同じものとして扱うが、
エンティティは、属性が同じでも区別される。

< 例: ユーザの名前(姓名) >
  • 値オブジェクトの場合は、ユーザの名前が同じなら、同じ人物として見なす。
    (現実的にはそうでないのに、そう見なしてしまう)
  • エンティティの場合は、ユーザの名前が同じでも、別の人物として見なす。

では、エンティティは何で区別するのか。それが識別子(identifier = ID)。

逆に、識別子が同じなら、名前が変更されてもそれは同じ人物である。

エンティティの判断基準

値オブジェクトとエンティティはドメインの概念を表現するオブジェクトとして似通っている。
(値オブジェクトでなく)エンティティにすべきと判断する基準は下記。

< 例: ユーザ >

システム上のユーザという概念は、作成されて生を受けて、削除されて死を迎える。
→ ライフサイクルが存在し、連続性があるため、エンティティとして表現すべき。

(当然、同じ概念を指していても、システムによっては値オブジェクトorエンティティにすべきかというのは異なってくる。)

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

(値オブジェクトとエンティティはいずれもドメインオブジェクト)

1. コードのドキュメント性が高まる

例えば、ユーザの名前は3文字以上でなければいけないというルール(仕様)がある場合、
ドメインオブジェクトとして定義していれば、クラス内にバリデーションとして存在するため、新たにジョインした開発者でも、コードを手がかりにルール(仕様)が把握できる

逆に、ドメインオブジェクトにしていない場合、更新されているかも分からないドキュメントを頼りにしないといけない。(ドキュメントが誤っていても、コードは動く)

2. ドメインにおける変更をコードに伝えやすくなる

例えば、ユーザの名前は3文字以上でなければいけないというルールが6文字以上というように変更された場合(ドメインに変更があった場合)、ドメインオブジェクトとして定義していれば、クラス内で1箇所を修正すればいい
(ドメインオブジェクトにルールや振る舞いが定義されているため。)

逆に、ドメインオブジェクトにしていない場合、クラスとして定義していないため、散在する文字数チェック箇所をすべて探し出して修正しないといけない。

ドメインサービス

値オブジェクトやエンティティに記述すると不自然になる振る舞いを解決するオブジェクト。
複数のドメインオブジェクト間を横断するような操作に多く見られる。

不自然な振る舞いとは

例えば、ユーザの名前は重複してはいけないという要件があるとする。
ユーザに関する関心事はUserクラスにと単に考えてコード(下記)を書くと、ユーザが自分自身に重複していない?と聞くことになる。

不自然な振る舞い
class User
  attr_accessor :name

  def initialize(name:)
    @name = name
  end

  def exists?(user:)
    # 重複を確認するコード
  end
end

user = User.new(name: '山田')
is_duplicate = user.exists?(user) # 自分自身に問い合わせている
ドメインサービス
class User
  attr_accessor :name

  def initialize(name:)
    @name = name
  end
end

class UserService
  def exists?(user:)
    # 重複を確認するコード
  end
end

user = User.new(name: '山田')
user_service = UserService.new
is_duplicate = user_service.exists?(user) # UserServiceに問い合わせる

ドメインモデル貧血症とは

本来ドメインモデルに記述されるべき知識や振る舞いが、ドメインサービスやアプリケーションサービスに記述され、語るべきことを語っていないドメインオブジェクトの状態。

オブジェクト指向設計のデータと振る舞いをまとめるという基本戦略の真逆をいくもの。

class User
  attr_accessor :name

  def initialize(name:)
    @name = name
  end
end

class UserService
  def exists?(user:)
    # 重複を確認するコード
  end

  # このメソッドはUserクラスにあるべき
  def change_user_name(name:)
    # ユーザの名前を変更するコード
  end
end

Railsでは (余談)

Railsではサービスを使わないため、Railsは思想が異なるのか?と思い調べてみたところ、こんな記述があった。

テーブルとActiveRecordモデルが一対一の関係にあるため、例えばUserに関するデータ・振る舞い(CRUD操作も含む)・制約などがActiveRecordのUserモデルに集約されがち。
( https://ryota21silva.hatenablog.com/entry/2021/09/12/153120 )

つまり、モデルに何もかも書き過ぎてしまう傾向になりやすいため、モデルの肥大化に注意してコーディングしないといけない。

リポジトリ

データを永続化・再構築する処理を抽象的に扱うためのオブジェクト。(リポジトリの和訳 : 保管庫)

(オブジェクトの)インスタンスを保存するとき、データストアに書き込む処理を直接実行するのでなく、リポジトリに依頼する。
(永続化したデータからインスタンスを再構築したいときも同様。)

利用例

以下のコードではデータを保存・検索する処理を直接記述しているため、本来の目的(何をしたいのか)がぼやける。

リポジトリ利用前
require 'mysql2'

class User
  attr_accessor :name

  def initialize(name:)
    @name = name
  end
end

class UserService
  def exists?(user:)
    connection = Mysql2::Client.new(host: '127.0.0.1', username: '', password: '', encoding: 'utf8',
      database: 'sample')
    sql = "SELECT * FROM users WHERE name = #{user.name}"
    result = connection.query(sql)
    connection.close

    # 以下、重複を確認するコード (略)
  end
end

# main
user = User.new(name: "山田")
user_service = UserService.new

raise StandardError.new("#{user.name}は既に存在しています。") if user_service.exists?(user: user)

# 以降、DBへの保存処理
connection = Mysql2::Client.new(host: '127.0.0.1', username: '', password: '', encoding: 'utf8',
                                database: 'sample')
sql = "INSERT INTO users (name) VALUES (#{name});"
connection.query(sql)
connection.close

データの保存・検索といったDBへの操作は、リポジトリに任せることで、本来やりたい処理の趣旨が際立つ。(= 意図を示す)

リポジトリ利用
require 'mysql2'

class User
  attr_accessor :name

  def initialize(name:)
    @name = name
  end
end

class UserService
  def exists?(user:)
    user_repository = UserRepository.new
    user_repository.find(user.name)
  end
end

class UserRepository
  def save(object)
    # 保存処理
  end

  def find(name)
    # 重複確認処理
  end
end

# main
user = User.new(name: "山田")
user_service = UserService.new

raise StandardError.new("#{user.name}は既に存在しています。") if user_service.exists?(user: user)

# 以降、DBへの保存処理
user_repository = UserRepository.new
user_repository.save(user)

↑のコードは生のRubyで書いたが、今はORMを使うことが大半。
(生で書いたことが無かったので、勉強になった。やっぱりORMのほうが手軽に書ける。)

アプリケーションサービス

ドメインオブジェクトを操作し、利用者の目的(ユースケース)を達成するように導くオブジェクト。

  • サービス
    • ドメインサービス
      ドメインの知識を表現する。
      (例:ユーザ名の重複確認)
    • アプリケーションサービス
      アプリケーションを成り立たせるための操作を行う。
      (例:ユーザの登録処理、退会処理)
# ドメインオブジェクト
class User
  attr_accessor :name

  def initialize(name:)
    @name = name
  end
end

# リポジトリ
class UserRepository
  def save(object)
    # 保存処理
  end
end

# アプリケーションサービス
class UserApplicationService
  def register(name:)
    user = User.new(name: name)
    user_repository = UserRepository.new
    user_repository.save(user)
  end
end

依存関係のコントロール

依存とは、依存先のオブジェクトが無くなると成り立たない状態。

< 例 >

例えば、上のコードではUserApplicationServiceUserRepositoryに依存している。

オブジェクト同士の依存は避けられないもの。避けるのでなく、コントロールする。

(下記の記事にも書いた内容。)

「自身よりも変更されないもの」に依存すること

https://zenn.dev/itoo/articles/object-oriented_design#依存方向の管理

依存関係逆転の原則

  1. 上位レベルのモジュールは、下位レベルのモジュールに依存してはいけない。どちらのモジュールも抽象に依存すべき。
    ( レベル = 入出力からの距離。)
  2. 抽象 は 実装の詳細 に依存してはいけない。実装の詳細 が 抽象 に依存すべき。
< 例 >

先程の例では、UserApplicationServiceUserRepositoryに依存している。
UserRepositoryのほうが(より機械に近い)具体的な処理を行っているため、下位レベル。
UserApplicationServiceはそれよりは上位のレベルなので、原則に反している。

そこで、UserRepositoryの抽象を作り、2つのモジュールはそれに依存させる。(原則1が解決)

また、UserRepositoryの抽象は、UserApplicationServiceのために存在することになり、その抽象によってUserRepositoryは具体的に実装される。(原則2が解決)

依存性の注入

オブジェクト内に依存関係を記述する(クラス内で他のインスタンスを生成する)のではなく、外部からオブジェクトを注入する。

(下記の記事にも書いたので詳細は割愛。)

https://zenn.dev/itoo/articles/object-oriented_design#疎結合なコードを書く

ファクトリ

複雑なオブジェクトの生成処理を責務とするオブジェクト。
ファクトリを使って生成処理をカプセル化することで、コードの論点が明確になる。

コンストラクタは単純であるべき。
コンストラクタが単純でなくなるときは、ファクトリを定義する。

ファクトリ使用前
class User
  attr_accessor :id, :name

  def initialize(name:)
    @name = name
    
    # 重複していないIDをDBから取得するためのコード
    # connection = Mysql2::Client.new(...)
    # id = ...

    @id = id
  end

  # インスタンスを再構築
  def rebuild_user(id:, name:)
    @id = id
    @name = name
  end
end
ファクトリ使用後
class UserFactoryInterface
  def create(name:)
    # サブクラスにcreateメソッドの実装を強制させるコード
  end

end

class UserFactory < UserFactoryInterface
  def create(name:)
    # 重複していないIDをDBから取得するためのコード
    # connection = Mysql2::Client.new(...)
    # id = ...
    User.new(id: id, name: name)
  end
end

class User
  attr_accessor :id, :name

  def initialize(id:, name:)
    @id = id
    @name = name
  end

  # 不要になった(コンストラクタが1つになった)
  # def rebuild_user(id:, name:)
  #   @id = id
  #   @name = name
  # end
end

class UserApplicationService
  def register
    user_factory = UserFactory.new
    user = user_factory.create(name: name)
    
    # 略 (UserRepositoryを使って保存)
  end
end

# ------------------------------------------------------

# 開発時の検証用にメモリ上で動かしたい場合、このファクトリに切り替えるだけでよい
class InMemoryUserFactory < UserFactoryInterface
  @@current_id = 0

  def create(name:)
    # テスト用にメモリ上で動かす
    current_id += 1
    User.new(id: current_id, name: name)
  end
end

整合性

データの整合性を保つための手段として2つの手段がある。

  1. ユニークキー制約
  2. トランザクション

ユニークキー制約

ユニークキー制約を設定さえすれば良いのではない。
コードを見て、データに重複が許されないというドメインの重大なルールが読み取れないため。
ユニークキー制約は主体でなく、セーフティネットとして併用して活用すべき

トランザクション

トランザクションを使うとき、データがどこまでロックされるかは常に念頭に置く必要がある。
ロックは可能な限り小さくすべき。(処理が失敗する可能性を下げるため)
一度のトランザクションで保存するオブジェクトを1つに限定し、さらにそのオブジェクトをなるべく小さくする。

アプリケーションを組み立てるフロー

  1. 要求に対して必要な機能を考える。
  2. 機能を成立させるために必要なユースケースを洗い出す。
  3. ユースケースを実現するために必要な概念とそこに存在するルールから、アプリケーションが必要とする知識を選び出し、ドメインオブジェクトを準備する。
  4. ドメインオブジェクトを用いて、ユースケースを実現するアプリケーションサービスを実装する。
< 例 >
  1. 同じ趣味をもつユーザ同士で交流したい。そのためにサークル機能を作る。
  2. ユースケースとして、サークルの作成、サークルへの参加が考えられる。
  3. サークル名は3文字以上、20文字以下。サークル名は重複してはいけない。サークルの人数は30人までというルールがある。サークルはライフサイクルがあるオブジェクトなのでエンティティ。
  4. サークルオブジェクトを使って実装する。

集約

ドメインオブジェクトを複数まとめたもの。

集約には、境界ルートが存在する。

  • 境界
    その集約に何が含まれるのかを定義。(線引き)
  • ルート
    集約に含まれるオブジェクト。
    集約を操作するための直接のインターフェースになるオブジェクト。

集約内部のオブジェクトに対する外部からの操作は、集約ルート(それを保持するオブジェクト)を経由(依頼)して実行する。

NG ... 境界内部を直接操作
user.name = user_name
OK ... ルートに依頼
# change_nameメソッド内で値のチェック(文字数やnullのチェック等)が可能になる
user.change_name(user_name)

また、集約は不変条件を維持する単位

不変条件とは

データが変更される時は常に維持されなければならない一貫性のルール
計算途中はともかく、最後に各プロパティの値が満たすべきデータの状態のルール

例)合計=小計+消費税
例)配送先が国外の場合は、電話番号に国コードが入っている

集約をどう区切るか

集約をどう区切るかについてのメジャーな方針は、変更の単位

< 例 >

サークルが、そのサークルに属しているユーザの属性(名前など)を変更するのはおかしい。
つまり、サークルの集約、ユーザの集約というように分けるべき。

集約の単位

リポジトリは集約ごとに用意する。
集約はなるべく小さくするべき。また、複数の集約を同一トランザクションで操作することも避ける。
(トランザクションによるロックを小さくするため。)

仕様

オブジェクトがある判定基準に達しているかを判定するオブジェクト。

オブジェクトの評価が複雑な場合、アプリケーションサービスに記述されてしまうことが多い。
しかし、オブジェクトの評価は重要なドメインのルールなので、サービスに記述されてしまうのは問題。

仕様として切り出すことで、複雑な評価手順がカプセル化されて、コードの意図が明確になる。

仕様 導入前
class Circle
  # サークル内のユーザ数は30人が上限 (単純な評価)
  def isFull()
    count_members >= 30
  end
end

class CircleApplicationService
  # サークルにユーザを追加
  # (複雑な評価が混入されている)
  def join()
    circle_repository = CircleRepository.new
    circle = circle_repository.find(id)

    user_repository = UserRepository.new
    users = user_repository.find(circle.members)

    # サークル内のユーザ数は30人が上限なのだが、
    # プレミアムユーザというユーザ種別があり、
    # プレミアムユーザが10人所属しているサークルは、上限が50人
    premium_user_count = users.count(user.is_premium)
    circle_member_limit = premium_user_count > 10 ? 50 : 30

    raise StandardError.new("満員です") if circle.count_members >= circle_member_limit

    # 追加処理...(略)
  end
end
仕様 導入後
# 複雑な評価手順がカプセル化され、コードの意図が明確に
class CircleApplicationService
  # サークルにユーザを追加
  def join()
    circle_full_specification = CircleFullSpecification.new
    raise StandardError.new("満員です") if circle_full_specification.is_safisfied_by(circle)

    # 追加処理...(略)
  end
end

# 仕様
class CircleFullSpecification
  def is_safisfied_by(circle)
    user_repository = UserRepository.new
    users = user_repository.find(circle.members)

    # サークル内のユーザ数は30人が上限なのだが、
    # プレミアムユーザというユーザ種別があり、
    # プレミアムユーザが10人所属しているサークルは、上限が50人
    premium_user_count = users.count(user.is_premium)
    circle_member_limit = premium_user_count > 10 ? 50 : 30

    circle.count_members >= circle_member_limit
  end
end

仕様とリポジトリを組み合わせる

仕様は単独で使うのみでなく、リポジトリと組み合わせる手法が存在する。
つまり、リポジトリに仕様を引き渡して、仕様に合致するオブジェクトを取得する

リポジトリは検索を行うメソッドが定義されるが、検索処理の中には重要なルールが含まれることがある。
重要なルールは仕様オブジェクトとして定義しリポジトリに引き渡せば、重要なルールがリポジトリに漏れ出すことを防げる

< 例 >

オススメのサークルを検索できる機能があったとする。
オススメの定義 : 直近1ヶ月以内に結成され、かつ、メンバー数が10以上 とする。)

リポジトリのみ
class CircleApplicationService
  # オススメのサークルを取得
  def get_recommend_circle()
    circle_repository = CircleRepository.new
    # リポジトリに問い合わせる (つまり、[オススメ]に関する重要なルールはリポジトリに存在する)
    circle_repository.find_recommend_circle
  end
end
仕様+リポジトリ
class CircleApplicationService
  # オススメのサークルを取得
  def get_recommend_circle()
    circle_repository = CircleRepository.new
    circles = circle_repository.find_all

    circle_recommend_spec = CircleRecommendSpecification.new
    circles.select { |circle| circle_recommend_spec.is_safisfied_by(circle) }
  end
end

# オススメサークルの仕様
class CircleRecommendSpecification
  def is_safisfied_by(circle)
    circle.created_at > Date.today.months_ago(3) && circle.count_members > 10
  end
end

アーキテクチャ

アーキテクチャは方針
何がどこに記述させるべきかという疑問に対する回答を明確にし、ロジックが無秩序に点在することを防ぐ

ドメイン駆動設計においては、ドメインが隔離されることのみが重要。

レイアードアーキテクチャ

4つの層で構成される。

  • プレゼンテーション層(ユーザインタフェース層)
    表示と解釈が責務。
    利用者に分かるように表示し、利用者の入力を解釈する。
  • アプリケーション層
    ドメインオブジェクトのクライアントとなり、ユースケースを実現するための進行役。
    ドメイン層はドメインの表現に徹しているため、アプリケーションとして成り立たせるためにそれらを使って問題解決に導く。
    ここに属するのはアプリケーションサービスなど。
  • ドメイン層
    最も重要。
    ソフトウェアを適用する領域で問題解決に必要な知識を表現する。他の層に流出しないようにする。
  • インフラストラクチャ層

原則は、依存の方向を上から下にすること。
上層は自身より下層に依存することが許される

ヘキサゴナルアーキテクチャ

コンセプトは、アプリケーションそれ以外のインターフェースや保存媒体は付け外しできるようにするというもの。

例えばゲーム機は、コントローラーやディスプレイが取替えできる。
それと同じように、インターフェースはGUIでもCLIでもよく、保存媒体はDBでもメモリでもよいようにする。

クリーンアーキテクチャ

ビジネスルールをカプセル化したモジュールを中心に据え、ユーザインタフェースやデータストアなどの詳細を端に追いやり、依存の方向を内側に向ける
そうすることで、詳細が抽象に依存するという依存関係逆転の法則を達成する。

(著者のQiita記事が、本書より詳細にまとめられていた。)

https://qiita.com/nrslib/items/a5f902c4defc83bd46b8

ドメイン駆動設計

本当に解決したいものを見つける

最も重要なことはドメインの本質に向き合うこと。
ドメインの問題を解決するにはドメインを知る必要がある。

開発者はドメインの精通者(ドメインエキスパート)と会話しないといけない
ドメインエキスパートは、ドメインの実践者であり、決してステークホルダーのことではない

開発者はドメインエキスパートと協力してドメインモデルを作り上げないといけない。
ただし、ドメインエキスパートがドメインモデルを知っているわけではない。
ドメインエキスパートが保有している知識は膨大だが、その中からシステムに役立つ知識が何かまでは分からない。
開発者はそれを引き出さないといけない。

ユビキタス言語

プロジェクトにおける共通言語のこと。
ドメインエキスパートとの会話、開発者同士の会話、コードに使う

言葉のすれ違いからは、最終的にドメインとコードの断絶に至る。
コードは開発者の理解と言葉によって組み立てられ、ソフトウェアは見当違いな方向へ歩き出してしまう。
意思疎通をする上での障害を取り払うため、ドメインエキスパートと同じ言葉で会話しないといけない。
わずかな言葉の揺れが小さなストレスとなり、それが積み重なって膨大なコストになる。

ユビキタス言語で会話すると、ドメインの概念を伝えるときに扱いにくい用語や曖昧な言葉に気づく。それは深い洞察のきっかけであり、なぜ扱いにくいのか何が曖昧なのかを開発者とドメインエキスパートで指摘し合うことでモデルは洗練される。

境界づけられたコンテキスト

ドメインの国境のようなもの。

同じものを指しながら言葉が少し違ったり、逆に言葉が同じだが意味が少し異なったりすることがある。
それは定義が揺れているの、もしくは複数のコンテキストの境界に立っている可能性がある。

モデルに対する捉え方が異なる箇所でシステムを分断し、それぞれの領域ごとに言語の統一を目指す。

< 例 >

サークルにおけるユーザと、システムにログインするユーザは、別の背景や目的を持っている。
つまり、サークルを作ったり参加したりするユーザに、ログインするためのIDやパスワードといった概念は無い。

視点が変わることにより、着目すべき内容が変わる。
こういった場合、1つの同じオブジェクトに無理やり別の概念を定義するのでなく、(Javaにおける)パッケージによって分割して別のオブジェクトとして定義すれば良い。

(この例でいうと、Core.UserクラスとAuthenticate.Userクラスを定義。)


Javaのパッケージは、Rubyでいうところの何?と思い調べた。名前空間で分けることと理解。

http://blog.livedoor.jp/sparklegate/archives/50254891.html

コンテキストマップ

境界づけられたコンテキストにより細分化することは、各コンテキストの理解のしやすさに貢献するが、コンテキストが連なったドメインの全体像をぼやけさせる。

つまり上の例でいうと、Core.UserクラスとAuthenticate.Userが連なっているということが分からないと、片方のコンテキストの修正が他のコンテキストに影響するのか分からない。

コンテキスト同士の関係を定義し、ドメイン全体を俯瞰できるようにコンテキストマップを作る必要がある。

総括

  • ドメイン駆動設計とは、ドメインを中心に考えてシステムを作ること。
    そのためには、そのドメイン領域の中でビジネス活動をしているドメインエキスパートと協調して、何を作ればユーザが喜ぶのかを突き詰めないといけない。
    今の自分はこれができていない。

  • 特に学びを感じたのは、下記の点。
    その中でも仕様については最も意識していく。ビジネスロジックを書くときはそれを書くのはここで良いのか、一度呼吸を置いて考える。

    • アプリケーションサービス
    • 仕様
    • 依存関係のコントロール手法(特に依存関係逆転の原則)
  • 同じ言葉を使うようにというルールが社内にある。
    今回ユビキタス言語について初めて学び、システム開発という側面からも有用なルールであることが分かった。

  • アーキテクチャは(ドメイン駆動設計においては)ドメインに集中するための補助。
    ただし、依存だらけのコード(システム)を作らないためにも、アーキテクチャはもっと深く学びたい。


  1. ドメインモデル貧血症!というコード例
    https://zenn.dev/itoo/articles/domein_driven_design#ドメインモデル貧血症とは

  1. ドメインモデルに拘るとどんな現実的な問題がでてくるのか?
  • クラスのフィールド(DBのカラム)ごとに値オブジェクトやエンティティを生成することになり、値オブジェクトやエンティティまみれになってしまう。
User.new(UserId, UserName, ...)
  • コード量が増える。

Discussion

ログインするとコメントできます