【TypeScript】ドメイン駆動設計 - 実装ガイド
2024-11-10
2024-11-10
概要
本記事では、TypeScript
を使ったドメイン駆動設計(DDD)
の実装ガイドを提供します。DDDは複雑なビジネスロジックを持つシステムの設計に適しており、コードにビジネスルールを明確に反映させるための手法です。TypeScript
を用いることで型安全性を高めながら、ビジネスドメインを反映した堅牢で拡張性の高いアプリケーションを構築することが可能です。
ドメイン駆動設計(DDD)の基本概念
DDDはビジネスのドメイン(業務領域)に基づいてシステムを設計するアプローチです。以下に主要なコンポーネントを紹介します。
エンティティ
エンティティは、システム内で識別されるオブジェクトで、主にIDによって区別されます。顧客や注文などのオブジェクトは、一般的にエンティティとして表現されます。
class Customer {
constructor(public readonly id: string, public name: string) {}
}
エンティティの特長は、IDが同じであれば、属性が異なっても同じものとして認識される点です。
バリューオブジェクト
バリューオブジェクトは、エンティティとは異なり、属性の値が同じであれば同じものとみなされます。値そのものが重要であり、IDを持たないオブジェクトです。例えば、住所や通貨の単位などです。
class Address {
constructor(
public readonly street: string,
public readonly city: string,
public readonly postalCode: string
) {}
equals(other: Address): boolean {
return (
this.street === other.street &&
this.city === other.city &&
this.postalCode === other.postalCode
);
}
}
アグリゲート
アグリゲートは関連するエンティティやバリューオブジェクトの集合で、一貫性を持って操作される単位です。アグリゲートにはルートエンティティがあり、他のエンティティはルートエンティティを経由してアクセスされます。
class Order {
private items: OrderItem[] = [];
constructor(public readonly id: string) {}
addItem(item: OrderItem): void {
this.items.push(item);
}
get totalAmount(): number {
return this.items.reduce((sum, item) => sum + item.price, 0);
}
}
リポジトリ
リポジトリはアグリゲートの保存や検索を行うためのインターフェースで、データアクセスの抽象化に役立ちます。データベースへのアクセス方法を隠蔽し、アグリゲートが簡潔に保たれます。
interface CustomerRepository {
save(customer: Customer): void;
findById(id: string): Customer | null;
}
ドメインサービス
ドメインサービスはエンティティやバリューオブジェクトで表現できないビジネスロジックを実装するためのコンポーネントです。特定のエンティティに属さない複雑な操作を提供します。
class OrderService {
createOrder(customer: Customer, items: OrderItem[]): Order {
const order = new Order(customer.id);
items.forEach((item) => order.addItem(item));
return order;
}
}
TypeScriptでのDDD実装
型システムを活用してビジネスルールを表現する
TypeScript
では、型を使ってビジネスルールを表現しやすく、ドメインの構造を明確に反映できます。たとえば、エンティティやバリューオブジェクトにおける必須フィールドやデータ型を厳密に指定することで、ロジックの整合性を保ちます。
インターフェースでリポジトリを抽象化する
リポジトリの実装には、インターフェースを使用してデータアクセスの詳細を抽象化します。例えば、以下のようにインターフェースを定義し、リポジトリの具象クラスでそのインターフェースを実装することで、リポジトリの実装を柔軟に変更できます。
interface ProductRepository {
findById(id: string): Product | null;
save(product: Product): void;
}
class InMemoryProductRepository implements ProductRepository {
private products: Map<string, Product> = new Map();
findById(id: string): Product | null {
return this.products.get(id) || null;
}
save(product: Product): void {
this.products.set(product.id, product);
}
}
ドメインイベントの利用
DDDでは、ドメインイベントを使うことで、システム内で発生する重要な出来事を表現できます。TypeScript
では、イベントエミッターやオブザーバーパターンを使ってドメインイベントの処理を実装できます。
class ProductAddedEvent {
constructor(public readonly productId: string, public readonly timestamp: Date) {}
}
class DomainEventEmitter {
private static events: ProductAddedEvent[] = [];
static emit(event: ProductAddedEvent): void {
this.events.push(event);
}
}
実装例:顧客管理システム
簡単な顧客管理システムを例に、DDDをTypeScript
で実装する方法を説明します。ここでは、顧客のエンティティ、顧客リポジトリ、顧客を登録するドメインサービスを定義します。
顧客エンティティ
class Customer {
constructor(public readonly id: string, public name: string) {}
}
顧客リポジトリのインターフェースと実装
interface CustomerRepository {
findById(id: string): Customer | null;
save(customer: Customer): void;
}
class InMemoryCustomerRepository implements CustomerRepository {
private customers: Map<string, Customer> = new Map();
findById(id: string): Customer | null {
return this.customers.get(id) || null;
}
save(customer: Customer): void {
this.customers.set(customer.id, customer);
}
}
顧客登録のドメインサービス
class CustomerService {
constructor(private customerRepository: CustomerRepository) {}
registerCustomer(id: string, name: string): Customer {
const customer = new Customer(id, name);
this.customerRepository.save(customer);
return customer;
}
}
DDD実装の注意点とベストプラクティ
ス
- エンティティとバリューオブジェクトの明確な区別
識別子の有無やビジネス上の意味に基づいて、エンティティとバリューオブジェクトを適切に区別することが重要です。 - 依存関係の管理
ドメイン層は、リポジトリやサービスへの依存関係を最小限に保ち、ビジネスロジックに集中するようにします。 - 型定義の一貫性
TypeScript
の型システムを積極的に活用して、ドメイン層のモデルに厳密な型定義を施し、コードの一貫性と堅牢性を高めましょう。
まとめ
TypeScript
を活用したDDDの実装は、複雑なビジネスロジックを持つシステムで有効です。型定義による堅牢な設計、リポジトリやドメインサービスによるビジネスロジックの分離、ドメインイベントの導入により、柔軟かつ拡張性の高い設計が可能となります。DDDの基本コンポーネントと実装テクニックを理解し、TypeScript
での実践に活用してみてください。