アプリケーションが複雑になるにつれて、「どこにどのロジックを書けばいいの?」「Controller が肥大化してメンテナンスできない…」「新機能を追加するたびにバグが出る」そんな設計の混乱に悩んでいませんか?
DDD(Domain Driven Design:ドメイン駆動設計)は、まさにそんな開発現場の課題を解決するための設計手法です。ビジネスロジックを適切な場所に配置し、変更に強いアプリケーションを構築できるようになります。
この記事では、DDD初心者の方でも理解できるよう、従来のMVCアーキテクチャとの違いから実装例まで、段階的に解説していきます。
DDDってよく聞くけど、結局何なの?
エンジニアがDDDで困る典型的な3つの場面
まず、実際の開発現場でよくある困った状況を見てみましょう:
- 場面1:Controller肥大化問題
「ユーザー登録のController が200行超え。バリデーション、DB操作、メール送信がごちゃ混ぜ…」 - 場面2:重複コード大量発生
「価格計算ロジックが Controller、Service、Model の3箇所に散らばっている」 - 場面3:仕様変更の影響範囲が読めない
「『送料無料の条件変更』だけなのに、なぜか5つのファイルを修正することに…」
これらの問題の根本原因は、「ビジネスロジックが適切な場所に配置されていない」ことです。
5分で理解するDDDの本質
DDD(ドメイン駆動設計)とは、ビジネスの核心となる部分(ドメイン)を中心としたソフトウェア設計手法です。
従来のMVCパターンでは「データベース中心」や「UI中心」の設計になりがちでしたが、DDDは「ビジネスルール中心」で設計します。
例えば、ECサイトの「注文」機能を考えてみましょう:
❌ 従来の考え方:「注文テーブルにデータを挿入する」
✅ DDDの考え方:「顧客が商品を注文する(ビジネス行為)」この視点の違いが、設計全体に大きな影響を与えます。
従来設計(MVC・レイヤードアーキテクチャ)との違い
なぜ従来設計では限界があるのか?
従来のMVCアーキテクチャで、よくあるController の実装を見てみましょう:
// ❌ 従来のMVC:Controllerにビジネスロジックが集中
class OrderController {
async createOrder(req, res) {
// バリデーション
if (!req.body.userId || !req.body.items) {
return res.status(400).json({ error: 'Invalid input' });
}
// ユーザー検索
const user = await User.findById(req.body.userId);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
// 在庫チェック
for (const item of req.body.items) {
const product = await Product.findById(item.productId);
if (product.stock < item.quantity) {
return res.status(400).json({ error: 'Insufficient stock' });
}
}
// 価格計算(複雑なビジネスロジック)
let totalPrice = 0;
for (const item of req.body.items) {
const product = await Product.findById(item.productId);
totalPrice += product.price * item.quantity;
}
// 割引適用
if (user.membershipLevel === 'premium') {
totalPrice *= 0.9; // 10%割引
}
// 送料計算
const shippingFee = totalPrice >= 5000 ? 0 : 500;
totalPrice += shippingFee;
// 注文作成
const order = await Order.create({
userId: user.id,
items: req.body.items,
totalPrice,
status: 'pending'
});
// メール送信
await EmailService.sendOrderConfirmation(user.email, order);
res.json(order);
}
}この実装には3つの大きな問題があります:
DDDが解決する3つの根本問題
- 責任の分散
価格計算、割引適用、送料計算などのビジネスルールがController に散らばっている - テストの困難さ
DB操作とビジネスロジックが混在しており、単体テストが書きにくい - 変更の影響範囲
「プレミアム会員の割引率変更」でも、Controller を修正する必要がある
設計手法比較表(実装例付き)
| 項目 | 従来MVC | DDD |
|---|---|---|
| 中心となる考え方 | データベース・UI中心 | ビジネスルール中心 |
| ビジネスロジックの場所 | Controller・Service が肥大化 | ドメインモデルに集約 |
| テストのしやすさ | DB依存で困難 | ドメインロジックは単体テスト可能 |
| 変更への対応 | 複数箇所の修正が必要 | ドメインモデル内で完結 |
| コードの再利用性 | 低い(Controller に依存) | 高い(ドメインモデルは独立) |
DDDの核となる4つの基本概念
DDDを理解するために、まず4つの基本概念を押さえましょう。これらは相互に関連し合って、堅牢なアプリケーションを構成します。
1. ドメインモデル:ビジネスロジックの中心
ドメインモデルは、現実世界のビジネス概念をコードで表現したものです。単なるデータの入れ物ではなく、ビジネスルールを持った「生きたオブジェクト」として設計します。
// ✅ ドメインモデル例:Order(注文)
class Order {
private constructor(
private readonly id: OrderId,
private readonly customerId: CustomerId,
private items: OrderItem[],
private status: OrderStatus
) {}
// ファクトリメソッド:ビジネスルールを満たした注文のみ作成
static create(customerId: CustomerId, items: OrderItem[]): Order {
if (items.length === 0) {
throw new Error('注文には少なくとも1つの商品が必要です');
}
return new Order(
OrderId.generate(),
customerId,
items,
OrderStatus.PENDING
);
}
// ビジネスロジック:価格計算
calculateTotalPrice(): Money {
const itemsTotal = this.items.reduce(
(total, item) => total.add(item.calculatePrice()),
Money.zero()
);
return itemsTotal.add(this.calculateShippingFee());
}
// ビジネスロジック:送料計算
private calculateShippingFee(): Money {
const itemsTotal = this.items.reduce(
(total, item) => total.add(item.calculatePrice()),
Money.zero()
);
return itemsTotal.isGreaterThanOrEqual(Money.of(5000))
? Money.zero()
: Money.of(500);
}
// ビジネスロジック:注文確定
confirm(): void {
if (this.status !== OrderStatus.PENDING) {
throw new Error('確定できるのは保留中の注文のみです');
}
this.status = OrderStatus.CONFIRMED;
}
}2. エンティティとバリューオブジェクト
DDDでは、オブジェクトを「アイデンティティを持つもの」と「値そのものに意味があるもの」に分けて考えます。
エンティティ:一意のIDを持ち、時間とともに変化するオブジェクト
// ✅ エンティティ例:User(ユーザー)
class User {
constructor(
private readonly id: UserId, // 一意のID
private name: UserName, // 変更可能な属性
private email: Email // 変更可能な属性
) {}
// 同一性の判定はIDで行う
equals(other: User): boolean {
return this.id.equals(other.id);
}
// 名前変更(ビジネスルールを含む)
changeName(newName: UserName): void {
if (newName.isEmpty()) {
throw new Error('名前を空にはできません');
}
this.name = newName;
}
}バリューオブジェクト:値そのものに意味があり、不変なオブジェクト
// ✅ バリューオブジェクト例:Money(金額)
class Money {
private constructor(private readonly value: number) {
if (value < 0) {
throw new Error('金額は0以上である必要があります');
}
}
static of(value: number): Money {
return new Money(value);
}
static zero(): Money {
return new Money(0);
}
// 同一性の判定は値で行う
equals(other: Money): boolean {
return this.value === other.value;
}
// 操作は新しいオブジェクトを返す(不変)
add(other: Money): Money {
return new Money(this.value + other.value);
}
isGreaterThanOrEqual(other: Money): boolean {
return this.value >= other.value;
}
getValue(): number {
return this.value;
}
}3. ドメインサービスとアプリケーションサービス
ドメインサービス:複数のエンティティにまたがるビジネスロジックを扱う
// ✅ ドメインサービス例:重複チェック
class UserDuplicationService {
constructor(private userRepository: UserRepository) {}
async isDuplicated(email: Email): Promise {
const existingUser = await this.userRepository.findByEmail(email);
return existingUser !== null;
}
}
// ✅ ドメインサービス例:価格計算(会員レベル考慮)
class PriceCalculationService {
calculateWithMemberDiscount(
basePrice: Money,
membershipLevel: MembershipLevel
): Money {
switch (membershipLevel) {
case MembershipLevel.PREMIUM:
return basePrice.multiply(0.9); // 10%割引
case MembershipLevel.GOLD:
return basePrice.multiply(0.95); // 5%割引
default:
return basePrice;
}
}
} アプリケーションサービス:ユースケースの実行を統制し、ドメインモデルを協調させる
// ✅ アプリケーションサービス例:注文作成
class CreateOrderApplicationService {
constructor(
private orderRepository: OrderRepository,
private userRepository: UserRepository,
private productRepository: ProductRepository,
private priceCalculationService: PriceCalculationService,
private emailService: EmailService
) {}
async execute(command: CreateOrderCommand): Promise {
// 1. 必要なエンティティを取得
const user = await this.userRepository.findById(command.userId);
if (!user) {
throw new Error('ユーザーが見つかりません');
}
// 2. 注文アイテムを構築
const orderItems: OrderItem[] = [];
for (const itemData of command.items) {
const product = await this.productRepository.findById(itemData.productId);
if (!product) {
throw new Error('商品が見つかりません');
}
orderItems.push(OrderItem.create(product, itemData.quantity));
}
// 3. ドメインモデルで注文を作成
const order = Order.create(user.getId(), orderItems);
// 4. 会員割引を適用
const discountedPrice = this.priceCalculationService
.calculateWithMemberDiscount(
order.calculateTotalPrice(),
user.getMembershipLevel()
);
// 5. 注文を保存
await this.orderRepository.save(order);
// 6. 確認メールを送信(インフラ層への依存)
await this.emailService.sendOrderConfirmation(
user.getEmail(),
order
);
}
} 4. リポジトリパターンとインフラ層
リポジトリパターンは、ドメインモデルをデータの永続化方法から独立させるための仕組みです。
// ✅ リポジトリインターface(ドメイン層)
interface OrderRepository {
save(order: Order): Promise;
findById(id: OrderId): Promise;
findByUserId(userId: UserId): Promise;
}
// ✅ リポジトリ実装(インフラ層)
class PrismaOrderRepository implements OrderRepository {
constructor(private prisma: PrismaClient) {}
async save(order: Order): Promise {
await this.prisma.order.upsert({
where: { id: order.getId().getValue() },
update: {
status: order.getStatus().getValue(),
totalPrice: order.calculateTotalPrice().getValue(),
updatedAt: new Date()
},
create: {
id: order.getId().getValue(),
userId: order.getCustomerId().getValue(),
status: order.getStatus().getValue(),
totalPrice: order.calculateTotalPrice().getValue(),
items: {
create: order.getItems().map(item => ({
productId: item.getProductId().getValue(),
quantity: item.getQuantity(),
price: item.getPrice().getValue()
}))
}
}
});
}
async findById(id: OrderId): Promise {
const orderData = await this.prisma.order.findUnique({
where: { id: id.getValue() },
include: { items: true }
});
if (!orderData) return null;
return this.toDomainModel(orderData);
}
private toDomainModel(data: any): Order {
// Prismaのデータをドメインモデルに変換
// 実装詳細は省略
}
} 実際のコードで見るDDD実装例
ここまでの概念を踏まえて、実際にBefore/Afterのコード比較を見てみましょう。
Before: 従来のMVC実装
// ❌ 従来のMVC実装:すべてがControllerに集中
class OrderController {
async createOrder(req, res) {
try {
// バリデーション(本来はドメインロジック)
if (!req.body.userId || !req.body.items || req.body.items.length === 0) {
return res.status(400).json({ error: 'Invalid order data' });
}
// ユーザー取得
const user = await User.findById(req.body.userId);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
// 在庫チェック
let totalPrice = 0;
const orderItems = [];
for (const item of req.body.items) {
const product = await Product.findById(item.productId);
if (!product) {
return res.status(404).json({ error: `Product ${item.productId} not found` });
}
if (product.stock < item.quantity) {
return res.status(400).json({
error: `Insufficient stock for ${product.name}`
});
}
// 価格計算(ビジネスロジックがControllerに...)
const itemPrice = product.price * item.quantity;
totalPrice += itemPrice;
orderItems.push({
productId: item.productId,
quantity: item.quantity,
price: itemPrice
});
}
// 会員割引(ここもビジネスロジック)
if (user.membershipLevel === 'premium') {
totalPrice *= 0.9;
} else if (user.membershipLevel === 'gold') {
totalPrice *= 0.95;
}
// 送料計算(またまたビジネスロジック)
const shippingFee = totalPrice >= 5000 ? 0 : 500;
totalPrice += shippingFee;
// 注文作成
const order = await Order.create({
userId: user.id,
items: orderItems,
totalPrice,
shippingFee,
status: 'pending',
createdAt: new Date()
});
// メール送信
const emailHtml = `
ご注文ありがとうございます
注文番号: ${order.id}
合計金額: ${totalPrice}円
`;
await sendEmail({
to: user.email,
subject: '注文確認',
html: emailHtml
});
res.json({
orderId: order.id,
totalPrice,
status: 'success'
});
} catch (error) {
console.error('Order creation failed:', error);
res.status(500).json({ error: 'Internal server error' });
}
}
}After: DDD実装
同じ機能をDDDで実装し直してみましょう:
// ✅ DDD実装:責任が適切に分離されている
// 1. ドメインモデル
class Order {
private constructor(
private readonly id: OrderId,
private readonly customerId: UserId,
private items: OrderItem[],
private status: OrderStatus
) {}
static create(customerId: UserId, items: OrderItem[]): Order {
if (items.length === 0) {
throw new DomainError('注文には少なくとも1つの商品が必要です');
}
return new Order(
OrderId.generate(),
customerId,
items,
OrderStatus.pending()
);
}
calculateTotalAmount(membershipLevel: MembershipLevel): Money {
const itemsTotal = this.calculateItemsTotal();
const discountedTotal = this.applyMemberDiscount(itemsTotal, membershipLevel);
const shippingFee = this.calculateShippingFee(discountedTotal);
return discountedTotal.add(shippingFee);
}
private calculateItemsTotal(): Money {
return this.items.reduce(
(total, item) => total.add(item.calculateAmount()),
Money.zero()
);
}
private applyMemberDiscount(amount: Money, level: MembershipLevel): Money {
return level.applyDiscount(amount);
}
private calculateShippingFee(amount: Money): Money {
const freeShippingThreshold = Money.of(5000);
return amount.isGreaterThanOrEqual(freeShippingThreshold)
? Money.zero()
: Money.of(500);
}
}
// 2. バリューオブジェクト
class MembershipLevel {
private constructor(private readonly value: string) {}
static premium(): MembershipLevel {
return new MembershipLevel('premium');
}
static gold(): MembershipLevel {
return new MembershipLevel('gold');
}
static regular(): MembershipLevel {
return new MembershipLevel('regular');
}
applyDiscount(amount: Money): Money {
switch (this.value) {
case 'premium':
return amount.multiply(0.9); // 10%割引
case 'gold':
return amount.multiply(0.95); // 5%割引
default:
return amount;
}
}
}
// 3. アプリケーションサービス
class CreateOrderUseCase {
constructor(
private orderRepository: OrderRepository,
private userRepository: UserRepository,
private productRepository: ProductRepository,
private stockService: StockService,
private emailService: EmailService
) {}
async execute(command: CreateOrderCommand): Promise {
// ユーザー取得
const user = await this.userRepository.findById(command.userId);
if (!user) {
throw new ApplicationError('ユーザーが見つかりません');
}
// 注文アイテム構築と在庫チェック
const orderItems: OrderItem[] = [];
for (const itemCommand of command.items) {
const product = await this.productRepository.findById(itemCommand.productId);
if (!product) {
throw new ApplicationError(`商品が見つかりません: ${itemCommand.productId}`);
}
// 在庫チェック(ドメインサービス)
await this.stockService.checkAvailability(product.getId(), itemCommand.quantity);
orderItems.push(
OrderItem.create(product, Quantity.of(itemCommand.quantity))
);
}
// ドメインモデルで注文作成
const order = Order.create(user.getId(), orderItems);
// 注文保存
await this.orderRepository.save(order);
// メール送信(インフラ層)
await this.emailService.sendOrderConfirmation(
user.getEmail(),
order,
user.getMembershipLevel()
);
return CreateOrderResult.success(
order.getId(),
order.calculateTotalAmount(user.getMembershipLevel())
);
}
}
// 4. Controller(薄くなった!)
class OrderController {
constructor(private createOrderUseCase: CreateOrderUseCase) {}
async createOrder(req: Request, res: Response): Promise {
try {
const command = CreateOrderCommand.fromRequest(req.body);
const result = await this.createOrderUseCase.execute(command);
res.json({
orderId: result.getOrderId().getValue(),
totalAmount: result.getTotalAmount().getValue(),
status: 'success'
});
} catch (error) {
if (error instanceof DomainError || error instanceof ApplicationError) {
res.status(400).json({ error: error.message });
} else {
res.status(500).json({ error: 'Internal server error' });
}
}
}
} 実装の改善ポイント
DDD実装により、以下の改善が実現されました:
- 単一責任の原則
各クラスが明確な責任を持ち、Controller は HTTP リクエスト/レスポンスの変換のみを担当 - テスタビリティ向上
ビジネスロジック(Order、MembershipLevel)は DB に依存せず単体テスト可能 - 変更への対応力
「プレミアム会員の割引率変更」は MembershipLevel クラスのみ修正すれば完了 - コードの再利用性
Order クラスは異なる API エンドポイントでも再利用可能 - ビジネスルールの表現力
コードを読むだけでビジネスルールが理解できる
DDD導入の判断基準と進め方
導入すべきプロジェクト・避けるべきプロジェクト
DDD は万能ではありません。適切な判断基準を持つことが重要です:
| 判断基準 | DDD導入を推奨 | DDD導入を避ける |
|---|---|---|
| プロジェクト規模 | 中〜大規模(3ヶ月以上) | 小規模(1ヶ月未満) |
| ビジネスルールの複雑さ | 複雑(計算式、状態遷移が多い) | 単純(CRUD中心) |
| 変更頻度 | 頻繁(仕様変更が多い) | 少ない(一度作ったら終わり) |
| チーム規模 | 3名以上 | 1-2名 |
| 保守期間 | 長期(2年以上) | 短期(半年未満) |
具体的な導入例:
- ✅ ECサイト:価格計算、在庫管理、配送ルールなど複雑なビジネスロジック
- ✅ 人事管理システム:給与計算、勤怠ルール、評価システムなど
- ✅ 予約システム:空室管理、料金計算、キャンセルポリシーなど
- ❌ 単純なAPI:データの登録・取得のみ
- ❌ プロトタイプ:短期間での検証目的
- ❌ 管理画面:マスターデータのメンテナンス中心
チームでの段階的導入ステップ
DDD は一度にすべてを導入すると混乱を招きます。段階的に導入しましょう:
Phase 1: 基本概念の理解(1-2週間)
- 学習:チーム全員でDDD基本概念を学習
- 実践:既存の1つの機能をバリューオブジェクトで書き直し
- レビュー:コードレビューで理解度を確認
// Phase 1の実践例:既存のpriceフィールドをMoneyクラス化
// Before
const price = 1000;
const tax = price * 0.1;
// After
const price = Money.of(1000);
const tax = price.calculateTax(TaxRate.of(0.1));Phase 2: ドメインモデルの導入(2-3週間)
- 設計:核となる1つのエンティティを特定してモデリング
- 実装:ビジネスロジックをドメインモデルに移動
- テスト:ドメインモデルの単体テストを作成
Phase 3: アプリケーションサービスの導入(2-3週間)
- 分離:Controller からビジネスロジックを完全に分離
- 統合:複数のドメインモデルを協調させるユースケース実装
- テスト:統合テストでワークフロー全体を確認
Phase 4: インフラ層の整備(3-4週間)
- 抽象化:リポジトリパターンでデータ永続化を抽象化
- DI:依存性注入でテストしやすい構造を構築
- 完成:DDDアーキテクチャの完成
よくある失敗パターンと対策
失敗パターン1:過度な抽象化
// ❌ 悪い例:単純なデータもすべてバリューオブジェクト化
class UserName {
constructor(private value: string) {}
getValue(): string { return this.value; }
}
class UserAge {
constructor(private value: number) {}
getValue(): number { return this.value; }
}
class UserEmail {
constructor(private value: string) {}
getValue(): string { return this.value; }
}
// ✅ 良い例:ビジネスルールがある場合のみバリューオブジェクト化
class Email {
constructor(private value: string) {
if (!this.isValidEmail(value)) {
throw new Error('無効なメールアドレスです');
}
}
private isValidEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
}対策:「ビジネスルールがあるか?」「バリデーションが必要か?」を判断基準にする
失敗パターン2:貧血ドメインモデル
// ❌ 貧血ドメインモデル:データの入れ物だけ
class Order {
constructor(
public id: string,
public userId: string,
public items: any[],
public totalPrice: number
) {}
// getter/setterのみ...
}
// ビジネスロジックは外部のサービスクラスに
class OrderService {
calculateTotalPrice(order: Order): number {
// 計算ロジック...
}
}
// ✅ 豊富なドメインモデル:ビジネスロジックを持つ
class Order {
private constructor(
private readonly id: OrderId,
private items: OrderItem[]
) {}
// ビジネスロジックを内包
calculateTotalPrice(): Money {
return this.items.reduce(
(total, item) => total.add(item.calculatePrice()),
Money.zero()
);
}
addItem(item: OrderItem): void {
if (this.isConfirmed()) {
throw new Error('確定済みの注文に商品を追加できません');
}
this.items.push(item);
}
}対策:「このオブジェクト自身に関するルール」はオブジェクト内に実装する
失敗パターン3:技術的関心事の混入
// ❌ ドメインモデルにDB操作が混入
class User {
async save(): Promise {
await db.user.update({
where: { id: this.id },
data: { name: this.name }
});
}
}
// ✅ ドメインモデルは純粋なビジネスロジックのみ
class User {
changeName(newName: string): void {
if (newName.trim().length === 0) {
throw new Error('名前は空にできません');
}
this.name = newName;
}
}
// DB操作はリポジトリが担当
class UserRepository {
async save(user: User): Promise {
await db.user.update({
where: { id: user.getId() },
data: { name: user.getName() }
});
}
} 対策:ドメイン層では外部システム(DB、API等)への依存を避ける
DDDよくある質問
❓ DDDは小さなプロジェクトでも使えますか?
1〜2ヶ月程度の小規模プロジェクトでは、DDDの恩恵を受ける前に開発が終了するためオーバーエンジニアリングとなる可能性が高いです。単純なCRUD操作が中心のプロジェクトでは、従来のMVCパターンで十分でしょう。
❓ DDDとMVCアーキテクチャはどちらを選ぶべきですか?
複雑なビジネスルールがあり、頻繁な仕様変更が予想される中〜大規模プロジェクトならDDD、シンプルなデータ管理が中心の小〜中規模プロジェクトならMVCがおすすめです。チーム規模や保守期間も判断材料となります。
❓ DDD導入にはどれくらいの学習時間が必要ですか?
基本概念の理解に1〜2週間、実践的な実装スキルの習得に2〜3ヶ月程度が目安です。ただし、チーム全体でのモデリングや設計スキルの習得には6ヶ月〜1年程度の継続的な学習と実践が必要です。
❓ DDDにはパフォーマンス面での制約がありますか?
適切に設計されたDDDアプリケーションでは、パフォーマンスが大幅に劣化することはありません。むしろドメインモデルが明確になることで最適化ポイントが特定しやすくなります。ただし、過度な抽象化は避け、必要に応じてパフォーマンス重視の実装も検討しましょう。
DDDをマスターしたら学ぶべき関連技術
DDDの基本概念を理解したら、さらに実践的な開発力を向上させる関連技術も学んでいきましょう:
🏗️ アーキテクチャ・設計パターン
- 実践DDD本 第7章「ドメインサービス」~複数の物を扱うビジネスルール~ – ドメインサービスの具体的な実装方法
- 実践DDD本 第5章「エンティティ」 ~一意な識別子で同一性を識別~ – エンティティ設計のベストプラクティス
- DDD実装を始めるのに一番とっつきやすいアーキテクチャは何か – DDDアーキテクチャ選択の実践ガイド
⚙️ AI開発・次世代ツール
- Qoder – AIが完全理解するソフトウェア開発向け次世代IDE – AIとDDDを組み合わせた次世代開発環境
- opencode – ターミナル向けAIコーディングエージェント – 複数AIモデル対応の開発支援ツール
- Byterover – AI開発者向け自己改善型コーディング知識管理プラットフォーム – チーム全体の開発品質向上
次のステップ:さらに学びを深めるために
この記事でDDDの基本概念と実装方法をご理解いただけたかと思います。さらに深く学びたい方のために、次のステップをご案内します。
推奨学習リソース
- 書籍:『実践ドメイン駆動設計』(Vaughn Vernon著)- 実装中心の実践的な内容
- 公式サイト:Domain Language – Eric Evans の公式サイト
- 実践:小さなプロジェクトでバリューオブジェクトから始めてみる
- コミュニティ:DDD Community Japan での情報交換
実践で身につけるためのヒント
- 小さく始める:既存コードの一部をバリューオブジェクトに変換することから
- ペアプロ活用:チームメンバーと一緒にモデリングを行う
- 継続的リファクタリング:設計は育てるもの。完璧を最初から目指さない
- ドメインエキスパートとの対話:業務知識を深めることで、より良いモデルを作れる
DDDは一朝一夕で身につくものではありませんが、段階的に導入することで、確実にコードの品質と保守性を向上させることができます。ビジネスロジックが適切な場所に配置された、変更に強いアプリケーションを目指して、ぜひチャレンジしてみてください!
設計で迷ったときは、「このロジックは誰の責任か?」「ビジネス的にどういう意味があるのか?」を常に問いかけることが、DDD的思考の第一歩です。
