paint-brush
ドメイン駆動設計の実践的な入門@tobi
1,571 測定値
1,571 測定値

ドメイン駆動設計の実践的な入門

Piotr Tobiasz15m2023/07/22
Read on Terminal Reader

長すぎる; 読むには

DDD の用語、構成要素、その他の概念について説明します。
featured image - ドメイン駆動設計の実践的な入門
Piotr Tobiasz HackerNoon profile picture
0-item
1-item

ソフトウェアを書くということは単にコードを書くことではなく、特定の問題を解決することであると言うのは革新的ではありません。最終的にソリューションを実装するのは開発者であっても、そもそも何が問題なのかを定義するのは開発者ではありません。このタスクはさまざまなビジネス担当者によって実行され、プロセス、リスク、結果を考慮して、問題が何であるか、問題が存在する理由、およびそれにどのように対処する必要があるかを説明します。ドメイン主導のコンテキストでは、これらのビジネスマンはドメインエキスパートと呼ばれます。


エンジニアリングの観点から見ると、ドメイン専門家は、ドメインに関する知識という貴重な資産を持っているようです。ただし、この知識がそのままの形で共有されることはほとんどありません。代わりに、開発者が理解して実装できるように、通常は要件に変換されます。このアプローチの問題は、ビジネス担当者と開発者のドメイン知識が異なる可能性があることです。これは、問題を定義する人とその解決に取り組む人の視点が一致せず、誤解や対立が生じる可能性があることを意味します。


それで、出口は何ですか?ビジネス担当者と技術担当者が同じ言語と用語を使用するようにしてください。

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

ドメイン駆動設計 (DDD) は、ドメインの専門家と技術関係者の間で共通の理解を築き、ソフトウェア ソリューションを基礎的なビジネス要件に適合させることの重要性を強調する方法論です。これは高度な非技術的な定義のように見えますが、より開発者にとってわかりやすいものに分解することもできます。


DDD は、ビジネス ドメインをモデル化して構築されたユビキタス言語を開発および使用することにより、現実世界の概念をコードとその構造で表現します。


まだ導入すべき用語がいくつかあるため、現時点では 100% 明確ではない可能性があります。最も重要なことは、DDD が、ビジネス ビジョンに沿ったコードの作成と構造化を可能にするツールとアクティビティを提供することです。したがって、コミュニケーションだけでなく、実際に共通言語を形成する設計上の決定を下すことも重要になります。

用語

DDD の世界で最も重要な用語がドメインであることは驚くべきことではありません。 『Learning Domain-Driven Design』の著者、Vlad Khononov 氏は次のように説明しています。


ビジネスドメインは、企業の主な活動分野を定義します。


つまり、ビジネスドメインは次のようにも考えることができます。


  • 企業の主な収益源である、
  • この会社が最もよく知られているのは、
  • その会社が競合他社よりも優れていること。


ドメインは、より具体的な範囲のアクティビティであるサブドメインに分割することもできます。サブドメインには 3 つの異なるタイプがありますが、最も重要なものはコアです。企業がどのようにしてビジネス上の優位性を達成するかを説明します。他の 2 つは、認証システムや内部管理パネルなど、より一般的で一般的な問題に関するものです。


ドメイン駆動設計の利点を最大限に活用するには、企業のビジネス ドメインを深く理解することが不可欠です。このことを理解するための最良の情報源は、他ならぬドメイン専門家です。これらは、ソフトウェアを使用して問題に対処している個人、つまり利害関係者、さまざまなビジネス関係者、さらにはユーザーです。エンジニアが自分たちが取り組んでいるドメインについて無知であるということではなく、むしろエキスパートがそのドメインの知識の真実の源であるということです。ドメインの専門家と協力することで、開発者はドメインのモデルを正確かつ最新の状態に保つことができます。


これは、重要だが潜在的に曖昧な用語であるモデルという別の用語につながります。 Eric Evans は、DDD に関する著書の中で、このモデルについて次のように説明しています。


それは、目前の問題の解決に関連する側面を抽象化し、無関係な詳細を無視する現実の解釈です。


ヴラド・コノノフはさらにこのアイデアを、より共感しやすい言葉で説明しています。


モデルは現実世界のコピーではなく、現実世界のシステムを理解するのに役立つ人間の構造物です。


結論として、モデルは、ドメインの根底にある複雑さの理解と伝達を容易にするビジネス概念またはプロセスを表現したものです。


Vlad は、ドメイン モデルの概念を効果的に説明するためにマップを使用しました。地図は、地形、道路、国境など、地図の種類に関連する情報のみを表示する好例です。すべての詳細を一度に表示する地図は圧倒的で、ほとんど役に立ちません。ドメイン モデルは、次のような他の形式でも見つかります。


  • 顧客の注文は、バックグラウンドで行われるすべてのプロセスの簡略化されたバージョンを表し、
  • レストランのメニューでは、メニューに記載されている品目は最終製品であり、すべての材料や準備プロセスのステップが記載されているわけではありません。
  • 旅行の予約。旅行やホテルなどの計画にはさらに多くのことが費やされますが、予約された旅行では最も重要な詳細のみが強調表示されます。


ドメイン駆動設計 (DDD) の用語パズルの最後のピースは、ユビキタス言語です。これは、プロジェクトの技術関係者とビジネス関係者の両方が使用する共有言語を指します。 DDD では、ドメイン モデルから派生したビジネス ドメインを記述するための共通言語を持つことが重要です。これは、すべてのチーム メンバーが問題空間、その概念、およびそれらの関係を明確に理解するのに役立ちます。これにより、調整が向上し、誤解のリスクが軽減されます。ユビキタス言語を使用すると、ソフトウェア ソリューションは基礎となるビジネス要件を正確に反映できるため、DDD の重要なコンポーネントになります。


ほとんどの用語がカバーされているので、ドメイン駆動設計とは何なのかをより簡単に理解できるはずです。ここで、実際の方法、つまり DDD の構成要素を詳しく掘り下げてみましょう。

ビルディングブロック

DDD ビルディング ブロックは、効果的かつ効率的なドメイン モデルを作成するための基盤として機能します。 Vlad Khononov はドメイン モデルを次のように定義しています。


ドメイン モデルは、動作とデータの両方を組み込んだドメインのオブジェクト モデルです。


ドメイン モデルは、さまざまな構成要素と構造で構成されます。最も重要なものは次のとおりです。


  • 値オブジェクト、
  • エンティティ、
  • 骨材、
  • ドメインイベント、
  • リポジトリ、
  • ドメインサービス。

値オブジェクト

値オブジェクトは、利用可能な最も基本的な構成要素です。これらは、一連の属性と値によって定義されるオブジェクトです。これらには一意の識別子はありません。その値がそのアイデンティティを定義します。異なる値がすでに異なる値オブジェクトを表しているという意味で、それらは不変です。値オブジェクトの例は次のとおりです。


  • 金額、
  • 日付範囲、
  • 郵便番号。


単純な値オブジェクトを Python で実装する方法は次のとおりです。


 from pydantic import BaseModel class Address(BaseModel):  """Customer address."""  country: str  city: str  street: str  house_number: str  class Config:    frozen = True


つまり、等価演算子 ( == ) を使用して 2 つのアドレスを比較すると、両方のオブジェクトにまったく同じ値が割り当てられている場合にのみTrueが返されます。

エンティティ

エンティティは、次のタイプの構成要素です。エンティティは、人や注文など、明確なアイデンティティを持つドメイン内の個々のオブジェクトを表します。これらはデータを保存するという点で値オブジェクトと似ていますが、その属性は変更される可能性があり、変更されることが予想されるため、一意の識別子が必要です。注文と個人情報は、エンティティの 2 つの単純なインスタンスにすぎません。


 import uuid from pydantic import BaseModel, Field from practical_ddd.building_blocks.value_objects import Address class Person(BaseModel):  """Personal data."""  id: uuid.UUID = Field(default_factory=uuid.uuid4)  first_name: str  last_name: str  address: Address class Order(BaseModel):  """Customer order."""  id: uuid.UUID = Field(default_factory=uuid.uuid4)  description: str  value: float


どちらの場合もインスタンスの値は変更可能であるため、識別情報 (UUID など) が必要です。さらに重要なことは、ほとんどの場合、エンティティは直接管理されるのではなく、集計を通じて管理されることを意図しているということです。

集計

集約は変更可能であり、一意の識別子を必要とするため、エンティティの一種です。ただし、その主な役割はデータを保存することではなく、関連するオブジェクト (エンティティと値オブジェクト) のセットを単一の一貫性単位としてグループ化することです。 Aggregate はルート オブジェクトであり、その内部状態をカプセル化し、グループ全体の一貫性を確保するために不変条件を適用する明確に定義された境界があります。集約を使用すると、オブジェクト自体ではなくオブジェクト間の関係に焦点を当てることで、より自然かつ直観的な方法でドメインについて推論することができます。


前述の例に続いて、集合体を顧客として表すことができます。


 import uuid from pydantic import BaseModel, Field from practical_ddd.building_blocks.entities import Person, Order from practical_ddd.building_blocks.value_objects import Address class Customer(BaseModel):  """Customer aggregate.  Manages personal information as well as orders.  """  id: uuid.UUID = Field(default_factory=uuid.uuid4)  person: Person  orders: list[Order] = Field(default_factory=list)  def change_address(self, new_address: Address) -> None:    self.person.address = new_address  def add_order(self, order: Order) -> None:    if self.total_value + order.value > 10000:      raise ValueError("Order cannot have value higher than 10000")    self.orders.append(order)  def remove_order(self, order_id: uuid.UUID) -> None:    order = next((order for order in self.orders if order.id == order_id), None)    if order is None:      raise IndexError("Order not found")    self.orders.remove(order)  @property  def total_value(self) -> float:    return sum(order.value for order in self.orders)


顧客は個人データに直接リンクされており、すべての注文が保存されます。それに加えて、この集約は、個人の住所を管理したり、注文を追加および削除したりするためのインターフェイスを公開します。これは、集合体の状態は、対応するメソッドを実行することによってのみ変更できるという事実によるものです。


前の例は比較的単純で、制約が 1 つだけ (順序値は 10000 を超えることはできません) ですが、DDD ビルディング ブロックの使用法とその関係を示しています。実際のシステムでは、集合体は多くの場合、より複雑であり、より多くの制約、境界、さらに多くの関係が存在します。結局のところ、彼らの存在自体がこの複雑さを管理することなのです。さらに、現実の世界では、通常、集計はデータベースなどのデータ ストアに保存されます。ここでリポジトリパターンが役に立ちます。

リポジトリ

アグリゲートの状態変更はすべて、単一のアトミック操作でトランザクション的にコミットされる必要があります。しかし、「それ自体を存続させる」ことは集合体の責任ではありません。リポジトリパターンを使用すると、データ ストレージと取得の詳細を抽象化し、その代わりに、より高い抽象レベルで集計を操作できます。簡単に言うと、リポジトリは集計とデータ ストレージの間の層と考えることができます。JSON ファイルは、そのようなストアの非常に単純な例です。顧客の集計には、JSON ファイル上で動作するリポジトリを含めることができます。


 import json import uuid from practical_ddd.building_blocks.aggregates import Customer class CustomerJSONRepository:  """Customer repository operating on JSON files."""  def __init__(self, path: str) -> None:    self.path = path  def get(self, customer_id: uuid.UUID) -> Customer:    with open(self.path, "r") as file:      database = json.load(file)      customer = database["customers"].get(str(customer_id))      if customer is None:        raise IndexError("Customer not found")      person = database["persons"][str(customer["person"])]      orders = [database["orders"][order_id] for order_id in customer["orders"]]    return Customer(      id=customer["id"],      person=person,      orders=orders,    )  def save(self, customer: Customer) -> None:    with open(self.path, "r+") as file:      database = json.load(file)      # Save customer      database["customers"][str(customer.id)] = {        "id": customer.id,        "person": customer.person.id,        "orders": [o.id for o in customer.orders],      }      # Save person      database["persons"][str(customer.person.id)] = customer.person.dict()      # Save orders      for order in customer.orders:        database["orders"][str(order.id)] = order.dict()      file.seek(0)      json.dump(database, file, indent=4, default=str)


もちろん、このクラスはさらに多くのことを行うことができます (そしておそらくそうすべきです) が、完璧な多機能 ORM を意図したものではありません。これにより、リポジトリの役割についてのアイデアが得られるはずです。この場合、それはJSON ファイル内の顧客集計の保存と取得です。リポジトリが集約に関連付けられたエンティティをどのように処理するかにも注目する価値があります。個人データと注文は顧客のライフサイクルと密接に結びついているため、集計処理時に正確に管理する必要があります。

ドメインサービス

考慮すべきもう 1 つのケースは、集計、そのエンティティ、値オブジェクトのいずれにも適合しないビジネス ロジックがある場合です。複数の集計またはデータ ストアの状態に依存するロジックである可能性があります。このような場合、ドメイン サービスとして知られる構造が役に立ちます。ドメイン サービスは、たとえばリポジトリを使用してアグリゲートを管理できる必要があり、その後、アグリゲートに属さないドメイン ロジックを保存できます。たとえば、顧客は、あまりにも多くの注文を失うことを避けるためのロジックを必要とする場合があります。


 import uuid from typing import Protocol from practical_ddd.building_blocks.aggregates import Customer class CustomerRepository(Protocol):  """Customer repository interface."""  def get(self, customer_id: uuid.UUID) -> Customer:    ...  def save(self, customer: Customer) -> None:    ... class CustomerService:  """Customer service."""  def __init__(self, repository: CustomerRepository) -> None:    self.repository = repository  def get_customer(self, customer_id: uuid.UUID) -> Customer | None:    try:      return self.repository.get(customer_id)    except IndexError:      return None  def save_customer(self, customer: Customer) -> None:    existing_customer = self.get_customer(customer.id)    # If customer is already in the database and has more than 2 orders,    # he cannot end up with half of them after a single save.    if (      existing_customer is not None      and len(existing_customer.orders) > 2      and len(customer.orders) < (len(existing_customer.orders) / 2)    ):      raise ValueError(        "Customer cannot lose more than half of his orders upon single save!"      )    self.repository.save(customer)


Aggregate は、そもそも JSON ファイルについての知識がないため、その状態が JSON ファイル内の状態とどのように異なるかを確認できません。このため、比較ロジックをドメイン サービスに含める必要があります。ドメイン サービスはリポジトリ抽象化と連動する必要があることに注意することも重要です。これにより、依存関係注入を使用して、具体的な実装を代替実装と簡単に交換できるようになります。

すべてを一緒に入れて

すべての部分がカバーされたので、動作するプログラムとして見ることができます。


 import uuid from practical_ddd.building_blocks import aggregates, entities, value_objects from practical_ddd.database.repository import CustomerJSONRepository from practical_ddd.service import CustomerService # Initialize domain service with json repository srv = CustomerService(repository=CustomerJSONRepository("test.json")) # Create a new customer customer = aggregates.Customer(  person=entities.Person(    first_name="Peter",    last_name="Tobias",    address=value_objects.Address(      country="Germany",      city="Berlin",      street="Postdamer Platz",      house_number="2/3",    ),  ), ) srv.save_customer(customer) # Add orders to existing customer customer = srv.get_customer(uuid.UUID("a32dd73a-6c1b-4581-b1d3-2a1247320938")) assert customer is not None customer.add_order(entities.Order(description="Order 1", value=10)) customer.add_order(entities.Order(description="Order 2", value=210)) customer.add_order(entities.Order(description="Order 3", value=3210)) srv.save_customer(customer) # Remove orders from existing customer # If there are only 3 orders, it's gonna fail customer = srv.get_customer(uuid.UUID("a32dd73a-6c1b-4581-b1d3-2a1247320938")) assert customer is not None customer.remove_order(uuid.UUID("0f3c0a7f-67fd-4309-8ca2-d007ac003b69")) customer.remove_order(uuid.UUID("a4fd7648-4ea3-414a-a344-56082e00d2f9")) srv.save_customer(customer)


すべてのものには責任と限界があります。 Aggregate は、そのエンティティと値オブジェクトの管理と、その制約の強制を担当します。ドメイン サービスは、挿入された JSON リポジトリを使用して JSON ファイル内のデータを永続化し、追加のドメイン境界を強制します。最終的に、各コンポーネントは、指定されたドメイン内で異なる機能と重要性を持ちます。

まとめ

ドメイン駆動設計は、理解するのが難しい複雑なアイデアであることに疑いの余地はありません。ソフトウェア チームがビジネス ドメインに重点を置き、最も困難なビジネス上の問題に取り組むのに役立つプラクティス、パターン、ツールを提供します。ただし、DDD は単なる構成要素のセットではありません。これは、技術関係者とビジネス関係者の間の協力とコミュニケーションを必要とする考え方です。 DDD プロジェクトの成功には、ユビキタス言語を通じて表現されるドメインの共通理解が不可欠です。 DDD がうまく機能すると、ビジネスのニーズにより適合し、複雑な問題をより効果的に解決できるソフトウェアを実現できます。

あとがきと次のステップ

この記事は、「DDD: From Zero To Hero」のようなものを意図したものではなく、むしろ DDD の世界への入門として機能することを目的としています。私は、ドメイン駆動設計の最も重要な概念を非常に簡単かつ実践的な方法で実証したいと考えました。ドメイン駆動設計を学ぶことは、プログラミングの専門知識を高める優れた方法であると私は信じています。ただし、この件についてはあまり頻繁に聞きません。少なくとも「11 の非常識な JavaScript のヒントとコツ - スレッド 🧵」ほどではありません。


いずれにせよ、これが興味深いと思った場合は、最初にこの記事を書くきっかけとなった本や記事のソースセクションを参照してください。この紹介の範囲を超えていると考えたため取り上げなかった概念がいくつかありますが、調査する価値は十分にあります。


  • 境界のあるコンテキスト
  • ドメインイベント
  • イベントストーミング


間違いなく、以下にリストされているソースでそれらを見つけることができます。

情報源

記事で使用されているコード例は、リンクにあります。


Vlad Khononov によるドメイン駆動設計の学習。私にとって大きなインスピレーションの源となった素晴らしい本。この記事で説明するすべての概念をさらに詳しく説明します。


Python のアーキテクチャ パターン ( Harry Percival と Bob Gregory 著)。私はほぼ 2 年前にこの本を読み、開発者としての私に大きな影響を与えました。この記事を書きながらそのことに戻ってきましたが、また役に立ちました。


Python の DDD (Przemysław Górecki 著)。記事を書き終えた頃にこのブログを発見しましたが、非常にプロフェッショナルなブログだったので興味をそそられました。面白い事実: 私はプシェミスワフと同じ会社で働いていましたが、そのことはまったく知りませんでした。


ここでも公開されています。