DDDのServiceパターンをTypeScript関数型アプローチで実装する実践ガイド
はじめに
ドメイン駆動設計(DDD)を学ぶ際、多くの開発者がつまずくポイントの一つが「Service」の理解と適切な実装です。特に、EntityやValue Objectには属さないビジネスロジックをどう扱うべきか、どのようにServiceを分類し実装すべきかについては、実践的な指針が不足しがちです。
本記事では、TypeScriptの関数型アプローチを用いて、DDDにおけるServiceパターンを実装する方法を詳しく解説します。クラスベースの実装ではなく、関数と型を活用したモダンなアプローチで、保守性と可読性を両立させる手法をお伝えします。
対象読者
- DDDの基本概念は理解しているが、Serviceの実装で悩んでいる方
- TypeScriptで関数型プログラミングのアプローチを学びたい方
- 実践的なDDD実装パターンを知りたい方
目次
- DDDにおけるServiceとは
- TypeScript関数型アプローチの基本
- Domain Serviceの実装
- Application Serviceの実装
- Infrastructure Serviceの実装
- 実践的な設計パターン
- よくある問題とその対策
- まとめ
DDDにおけるServiceとは
なぜServiceが必要なのか
DDDでは、ビジネスロジックを主にEntityとValue Objectで表現しますが、以下のような場合には適切な表現が困難になります:
- 複数のEntityが関わる操作
- 複雑な計算や変換処理
- 外部システムとの連携
このような処理をEntityに無理やり押し込むと、責務が曖昧になり、保守性が損なわれます。
// 悪い例:どちらのEntityに属するべきか不明確
type User = {
readonly id: UserId
readonly email: Email
readonly balance: Money
// このオブジェクトに送金処理を含めるべき?
}
// この処理はどこに属するべきか?
const transferMoney = (from: User, to: User, amount: Money) => {
// 2つのUserが関わる処理...
}
Serviceの3つの分類
DDDでは、Serviceを以下の3つに分類します:
種類 | 責務 | 実装場所 |
---|---|---|
Domain Service | ビジネスロジック | Domain層 |
Application Service | ユースケースの調整 | Application層 |
Infrastructure Service | 技術的な処理 | Infrastructure層 |
TypeScript関数型アプローチの基本
型定義による設計
まず、ドメインオブジェクトを型として定義します:
// ブランド型を用いた型安全性の向上
type UserId = string & { readonly _brand: 'UserId' }
type AccountId = string & { readonly _brand: 'AccountId' }
type Email = string & { readonly _brand: 'Email' }
// Value Object
type Money = {
readonly amount: number
readonly currency: Currency
}
type Currency = 'JPY' | 'USD' | 'EUR'
// Entity
type User = {
readonly id: UserId
readonly email: Email
readonly name: string
readonly createdAt: Date
}
type Account = {
readonly id: AccountId
readonly userId: UserId
readonly balance: Money
readonly status: AccountStatus
}
type AccountStatus = 'active' | 'suspended' | 'closed'
エラーハンドリングの戦略
関数型アプローチでは、エラーを戻り値の型として表現します:
type Result<T, E> = T | E
type DomainError = {
readonly type: string
readonly message: string
readonly details?: Record<string, unknown>
}
Domain Serviceの実装
複数Entityを扱うビジネスロジック
最も典型的なDomain Serviceの例として、銀行の送金処理を実装してみましょう:
// 送金結果の型定義
type TransferResult = {
readonly fromAccount: Account
readonly toAccount: Account
readonly transferredAmount: Money
readonly executedAt: Date
}
// エラーの詳細な型定義
type TransferError =
| {
type: 'InsufficientFunds'
required: Money
available: Money
}
| {
type: 'CurrencyMismatch'
fromCurrency: Currency
toCurrency: Currency
}
| {
type: 'AccountSuspended'
accountId: AccountId
}
| {
type: 'SameAccountTransfer'
accountId: AccountId
}
// 純粋関数としてのDomain Service
const transferMoney = (
fromAccount: Account,
toAccount: Account,
amount: Money
): Result<TransferResult, TransferError> => {
// ビジネスルールの検証
// 同一アカウント間の送金禁止
if (fromAccount.id === toAccount.id) {
return {
type: 'SameAccountTransfer',
accountId: fromAccount.id
}
}
// アカウント状態の確認
if (fromAccount.status !== 'active') {
return {
type: 'AccountSuspended',
accountId: fromAccount.id
}
}
if (toAccount.status !== 'active') {
return {
type: 'AccountSuspended',
accountId: toAccount.id
}
}
// 通貨の確認
if (fromAccount.balance.currency !== amount.currency) {
return {
type: 'CurrencyMismatch',
fromCurrency: fromAccount.balance.currency,
toCurrency: amount.currency
}
}
// 残高の確認
if (fromAccount.balance.amount < amount.amount) {
return {
type: 'InsufficientFunds',
required: amount,
available: fromAccount.balance
}
}
// 送金処理の実行(不変性を保持)
const updatedFromAccount: Account = {
...fromAccount,
balance: {
...fromAccount.balance,
amount: fromAccount.balance.amount - amount.amount
}
}
const updatedToAccount: Account = {
...toAccount,
balance: {
...toAccount.balance,
amount: toAccount.balance.amount + amount.amount
}
}
return {
fromAccount: updatedFromAccount,
toAccount: updatedToAccount,
transferredAmount: amount,
executedAt: new Date()
}
}
複雑な計算処理
次に、複雑な料金計算を行うDomain Serviceを実装します:
// 商品とお客様の型定義
type Product = {
readonly id: ProductId
readonly name: string
readonly basePrice: Money
readonly category: ProductCategory
readonly weight: number
}
type Customer = {
readonly id: CustomerId
readonly tier: CustomerTier
readonly registeredAt: Date
readonly totalPurchases: Money
}
type ProductId = string & { readonly _brand: 'ProductId' }
type CustomerId = string & { readonly _brand: 'CustomerId' }
type ProductCategory = 'electronics' | 'clothing' | 'books' | 'food'
type CustomerTier = 'bronze' | 'silver' | 'gold' | 'platinum'
// 割引条件の定義
type Discount = {
readonly id: DiscountId
readonly type: DiscountType
readonly value: number
readonly conditions: readonly DiscountCondition[]
readonly validUntil: Date
}
type DiscountId = string & { readonly _brand: 'DiscountId' }
type DiscountType = 'percentage' | 'fixed_amount'
type DiscountCondition =
| { type: 'minimum_quantity'; quantity: number }
| { type: 'customer_tier'; tier: CustomerTier }
| { type: 'product_category'; category: ProductCategory }
| { type: 'new_customer'; daysFromRegistration: number }
// 価格計算Domain Service
const calculatePrice = (
product: Product,
customer: Customer,
quantity: number,
discounts: readonly Discount[] = []
): Money => {
let totalPrice = product.basePrice.amount * quantity
// 顧客ティア基本割引の適用
const tierDiscount = calculateTierDiscount(customer.tier)
totalPrice = totalPrice * (1 - tierDiscount)
// カテゴリ別ボリューム割引
const volumeDiscount = calculateVolumeDiscount(product.category, quantity)
totalPrice = totalPrice * (1 - volumeDiscount)
// 追加割引の適用
const applicableDiscounts = discounts.filter(discount =>
isDiscountApplicable(discount, customer, product, quantity)
)
for (const discount of applicableDiscounts) {
totalPrice = applyDiscount(totalPrice, discount)
}
return {
amount: Math.max(0, Math.round(totalPrice * 100) / 100), // 小数点2桁で丸め
currency: product.basePrice.currency
}
}
// ヘルパー関数群
const calculateTierDiscount = (tier: CustomerTier): number => {
const tierDiscounts: Record<CustomerTier, number> = {
bronze: 0,
silver: 0.05,
gold: 0.10,
platinum: 0.15
}
return tierDiscounts[tier]
}
const calculateVolumeDiscount = (
category: ProductCategory,
quantity: number
): number => {
const volumeThresholds: Record<ProductCategory, { threshold: number; discount: number }[]> = {
electronics: [
{ threshold: 5, discount: 0.05 },
{ threshold: 10, discount: 0.10 }
],
clothing: [
{ threshold: 3, discount: 0.10 },
{ threshold: 7, discount: 0.15 }
],
books: [
{ threshold: 5, discount: 0.15 },
{ threshold: 15, discount: 0.25 }
],
food: [
{ threshold: 10, discount: 0.05 }
]
}
const thresholds = volumeThresholds[category]
const applicableDiscount = thresholds
.filter(t => quantity >= t.threshold)
.sort((a, b) => b.discount - a.discount)[0]
return applicableDiscount?.discount ?? 0
}
const isDiscountApplicable = (
discount: Discount,
customer: Customer,
product: Product,
quantity: number
): boolean => {
// 有効期限チェック
if (new Date() > discount.validUntil) {
return false
}
// すべての条件を満たす必要がある
return discount.conditions.every(condition => {
switch (condition.type) {
case 'minimum_quantity':
return quantity >= condition.quantity
case 'customer_tier':
return customer.tier === condition.tier
case 'product_category':
return product.category === condition.category
case 'new_customer':
const daysSinceRegistration = Math.floor(
(Date.now() - customer.registeredAt.getTime()) / (1000 * 60 * 60 * 24)
)
return daysSinceRegistration <= condition.daysFromRegistration
default:
return false
}
})
}
const applyDiscount = (price: number, discount: Discount): number => {
switch (discount.type) {
case 'percentage':
return price * (1 - discount.value / 100)
case 'fixed_amount':
return Math.max(0, price - discount.value)
default:
return price
}
}
Application Serviceの実装
Application Serviceは、ユースケースを実現するための調整役として機能します。Repository、Domain Service、Infrastructure Serviceを組み合わせて、エンドツーエンドの処理を実現します。
ユーザー登録のApplication Service
// 外部依存の抽象化
type UserRepository = {
readonly findById: (id: UserId) => Promise<User | null>
readonly findByEmail: (email: Email) => Promise<User | null>
readonly save: (user: User) => Promise<void>
readonly existsByEmail: (email: Email) => Promise<boolean>
}
type EmailService = {
readonly sendWelcomeEmail: (email: Email, name: string) => Promise<void>
readonly sendVerificationEmail: (email: Email, token: string) => Promise<void>
}
// コマンドとイベントの定義
type RegisterUserCommand = {
readonly email: string
readonly name: string
readonly password: string
}
type UserRegisteredEvent = {
readonly type: 'UserRegistered'
readonly userId: UserId
readonly email: Email
readonly registeredAt: Date
}
// バリデーションエラーの詳細な定義
type UserRegistrationError =
| { type: 'InvalidEmail'; email: string; reason: string }
| { type: 'InvalidName'; name: string; reason: string }
| { type: 'WeakPassword'; reason: string }
| { type: 'DuplicateEmail'; email: string }
| { type: 'ExternalServiceError'; service: string; message: string }
// バリデーション関数群
const validateEmail = (email: string): Result<Email, UserRegistrationError> => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!email.trim()) {
return { type: 'InvalidEmail', email, reason: 'Email is required' }
}
if (email.length > 254) {
return { type: 'InvalidEmail', email, reason: 'Email is too long' }
}
if (!emailRegex.test(email)) {
return { type: 'InvalidEmail', email, reason: 'Invalid email format' }
}
return email as Email
}
const validateName = (name: string): Result<string, UserRegistrationError> => {
const trimmedName = name.trim()
if (!trimmedName) {
return { type: 'InvalidName', name, reason: 'Name is required' }
}
if (trimmedName.length < 2) {
return { type: 'InvalidName', name, reason: 'Name must be at least 2 characters' }
}
if (trimmedName.length > 50) {
return { type: 'InvalidName', name, reason: 'Name must be 50 characters or less' }
}
const nameRegex = /^[a-zA-Za-zA-Z\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF\s]+$/
if (!nameRegex.test(trimmedName)) {
return { type: 'InvalidName', name, reason: 'Name contains invalid characters' }
}
return trimmedName
}
const validatePassword = (password: string): Result<string, UserRegistrationError> => {
if (password.length < 8) {
return { type: 'WeakPassword', reason: 'Password must be at least 8 characters' }
}
if (password.length > 100) {
return { type: 'WeakPassword', reason: 'Password must be 100 characters or less' }
}
const hasUpperCase = /[A-Z]/.test(password)
const hasLowerCase = /[a-z]/.test(password)
const hasNumbers = /\d/.test(password)
const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(password)
const strengthChecks = [hasUpperCase, hasLowerCase, hasNumbers, hasSpecialChar]
const passedChecks = strengthChecks.filter(Boolean).length
if (passedChecks < 3) {
return {
type: 'WeakPassword',
reason: 'Password must contain at least 3 of: uppercase, lowercase, numbers, special characters'
}
}
return password
}
// Application Service本体
const createUserRegistrationService = (
userRepository: UserRepository,
emailService: EmailService
) => {
const registerUser = async (
command: RegisterUserCommand
): Promise<Result<UserRegisteredEvent, UserRegistrationError>> => {
// 1. 入力値のバリデーション
const emailResult = validateEmail(command.email)
if ('type' in emailResult) return emailResult
const nameResult = validateName(command.name)
if ('type' in nameResult) return nameResult
const passwordResult = validatePassword(command.password)
if ('type' in passwordResult) return passwordResult
const validatedEmail = emailResult
const validatedName = nameResult
const validatedPassword = passwordResult
try {
// 2. ビジネスルールの検証(重複チェック)
const emailExists = await userRepository.existsByEmail(validatedEmail)
if (emailExists) {
return { type: 'DuplicateEmail', email: command.email }
}
// 3. ドメインオブジェクトの生成
const userId = crypto.randomUUID() as UserId
const hashedPassword = await hashPassword(validatedPassword) // 実装は省略
const verificationToken = generateVerificationToken() // 実装は省略
const user: User = {
id: userId,
email: validatedEmail,
name: validatedName,
createdAt: new Date()
}
// 4. 永続化
await userRepository.save(user)
// 5. 外部サービスとの連携
await Promise.all([
emailService.sendWelcomeEmail(validatedEmail, validatedName),
emailService.sendVerificationEmail(validatedEmail, verificationToken)
])
// 6. イベントの生成
const event: UserRegisteredEvent = {
type: 'UserRegistered',
userId,
email: validatedEmail,
registeredAt: user.createdAt
}
return event
} catch (error) {
// エラーハンドリング
if (error instanceof Error) {
return {
type: 'ExternalServiceError',
service: 'UserRegistration',
message: error.message
}
}
return {
type: 'ExternalServiceError',
service: 'UserRegistration',
message: 'Unknown error occurred'
}
}
}
return { registerUser }
}
// ヘルパー関数(実装は省略)
const hashPassword = async (password: string): Promise<string> => {
// bcrypt等を使用したハッシュ化
return `hashed_${password}`
}
const generateVerificationToken = (): string => {
return crypto.randomUUID()
}
注文処理のApplication Service
type OrderRepository = {
readonly save: (order: Order) => Promise<void>
readonly findById: (id: OrderId) => Promise<Order | null>
}
type InventoryService = {
readonly checkAvailability: (items: readonly OrderItem[]) => Promise<InventoryCheckResult>
readonly reserveItems: (items: readonly OrderItem[]) => Promise<void>
}
type PaymentService = {
readonly processPayment: (amount: Money, paymentMethod: PaymentMethod) => Promise<PaymentResult>
}
// 注文関連の型定義
type Order = {
readonly id: OrderId
readonly customerId: CustomerId
readonly items: readonly OrderItem[]
readonly totalAmount: Money
readonly status: OrderStatus
readonly createdAt: Date
}
type OrderItem = {
readonly productId: ProductId
readonly quantity: number
readonly unitPrice: Money
readonly totalPrice: Money
}
type OrderId = string & { readonly _brand: 'OrderId' }
type OrderStatus = 'pending' | 'confirmed' | 'paid' | 'shipped' | 'delivered' | 'cancelled'
type CreateOrderCommand = {
readonly customerId: CustomerId
readonly items: readonly {
readonly productId: ProductId
readonly quantity: number
}[]
readonly paymentMethod: PaymentMethod
}
type PaymentMethod = {
readonly type: 'credit_card' | 'bank_transfer' | 'digital_wallet'
readonly details: Record<string, string>
}
type InventoryCheckResult =
| { type: 'Available' }
| { type: 'InsufficientStock'; unavailableItems: readonly ProductId[] }
type PaymentResult =
| { type: 'Success'; transactionId: string }
| { type: 'Failed'; reason: string }
type OrderCreationError =
| { type: 'CustomerNotFound'; customerId: CustomerId }
| { type: 'EmptyOrder' }
| { type: 'InsufficientStock'; products: readonly ProductId[] }
| { type: 'PaymentFailed'; reason: string }
| { type: 'PricingError'; message: string }
// 注文処理Application Service
const createOrderProcessingService = (
userRepository: UserRepository,
productRepository: ProductRepository,
orderRepository: OrderRepository,
inventoryService: InventoryService,
paymentService: PaymentService
) => {
const createOrder = async (
command: CreateOrderCommand
): Promise<Result<Order, OrderCreationError>> => {
try {
// 1. 顧客の存在確認
const customer = await userRepository.findById(command.customerId)
if (!customer) {
return { type: 'CustomerNotFound', customerId: command.customerId }
}
// 2. 注文内容の基本検証
if (command.items.length === 0) {
return { type: 'EmptyOrder' }
}
// 3. 商品情報の取得と注文項目の構築
const orderItems: OrderItem[] = []
let totalAmount = 0
let currency: Currency | undefined
for (const item of command.items) {
const product = await productRepository.findById(item.productId)
if (!product) {
continue // 商品が見つからない場合はスキップ
}
// 価格計算(Domain Serviceを使用)
const calculatedPrice = calculatePrice(product, customer, item.quantity)
// 通貨の統一性確認
if (!currency) {
currency = calculatedPrice.currency
} else if (currency !== calculatedPrice.currency) {
return { type: 'PricingError', message: 'Mixed currencies not supported' }
}
const orderItem: OrderItem = {
productId: item.productId,
quantity: item.quantity,
unitPrice: {
amount: calculatedPrice.amount / item.quantity,
currency: calculatedPrice.currency
},
totalPrice: calculatedPrice
}
orderItems.push(orderItem)
totalAmount += calculatedPrice.amount
}
if (orderItems.length === 0) {
return { type: 'EmptyOrder' }
}
// 4. 在庫確認
const inventoryCheck = await inventoryService.checkAvailability(orderItems)
if (inventoryCheck.type === 'InsufficientStock') {
return {
type: 'InsufficientStock',
products: inventoryCheck.unavailableItems
}
}
// 5. 在庫の予約
await inventoryService.reserveItems(orderItems)
// 6. 注文オブジェクトの生成
const order: Order = {
id: crypto.randomUUID() as OrderId,
customerId: command.customerId,
items: orderItems,
totalAmount: { amount: totalAmount, currency: currency! },
status: 'pending',
createdAt: new Date()
}
// 7. 注文の永続化
await orderRepository.save(order)
// 8. 決済処理
const paymentResult = await paymentService.processPayment(
order.totalAmount,
command.paymentMethod
)
if (paymentResult.type === 'Failed') {
// 決済失敗時は在庫予約を解除(実装は省略)
return { type: 'PaymentFailed', reason: paymentResult.reason }
}
// 9. 注文ステータスの更新
const paidOrder: Order = { ...order, status: 'paid' }
await orderRepository.save(paidOrder)
return paidOrder
} catch (error) {
return {
type: 'PricingError',
message: error instanceof Error ? error.message : 'Unknown error'
}
}
}
return { createOrder }
}
Infrastructure Serviceの実装
Infrastructure Serviceは、技術的な処理を担当し、Domain ServiceやApplication Serviceで定義されたインターフェースを実装します。
メール送信サービス
// 設定の型定義
type EmailConfig = {
readonly smtpHost: string
readonly smtpPort: number
readonly username: string
readonly password: string
readonly fromAddress: string
}
// メールテンプレートの型定義
type EmailTemplate = {
readonly subject: string
readonly htmlBody: string
readonly textBody: string
}
// メール送信Infrastructure Service
const createEmailInfrastructureService = (
config: EmailConfig
): EmailService => {
const sendWelcomeEmail = async (email: Email, name: string): Promise<void> => {
const template = getWelcomeEmailTemplate(name)
await sendEmail(email, template)
}
const sendVerificationEmail = async (email: Email, token: string): Promise<void> => {
const template = getVerificationEmailTemplate(token)
await sendEmail(email, template)
}
const sendEmail = async (email: Email, template: EmailTemplate): Promise<void> => {
try {
// 実際のSMTP送信処理(例:nodemailer使用)
console.log(`Sending email to ${email}`)
console.log(`Subject: ${template.subject}`)
console.log(`Body: ${template.textBody}`)
// 実装例(実際にはnodemailer等を使用)
// const transporter = nodemailer.createTransporter({ ... })
// await transporter.sendMail({
// from: config.fromAddress,
// to: email,
// subject: template.subject,
// html: template.htmlBody,
// text: template.textBody
// })
} catch (error) {
console.error(`Failed to send email to ${email}:`, error)
throw new Error(`Email sending failed: ${error}`)
}
}
return {
sendWelcomeEmail,
sendVerificationEmail
}
}
// テンプレート生成関数
const getWelcomeEmailTemplate = (name: string): EmailTemplate => ({
subject: 'Welcome to Our Service!',
htmlBody: `
<h1>Welcome, ${name}!</h1>
<p>Thank you for registering with our service.</p>
<p>We're excited to have you on board!</p>
`,
textBody: `
Welcome, ${name}!
Thank you for registering with our service.
We're excited to have you on board!
`
})
const getVerificationEmailTemplate = (token: string): EmailTemplate => ({
subject: 'Please verify your email address',
htmlBody: `
<h1>Email Verification</h1>
<p>Please click the link below to verify your email address:</p>
<a href="https://example.com/verify?token=${token}">Verify Email</a>
`,
textBody: `
Email Verification
Please visit the following link to verify your email address:
https://example.com/verify?token=${token}
`
})
データベースRepository
// データベース接続の抽象化
type DatabaseConnection = {
readonly query: <T>(sql: string, params: readonly unknown[]) => Promise<T[]>
readonly execute: (sql: string, params: readonly unknown[]) => Promise<void>
}
// UserRepository の実装
const createUserRepository = (db: DatabaseConnection): UserRepository => {
const findById = async (id: UserId): Promise<User | null> => {
const rows = await db.query<UserRow>(
'SELECT id, email, name, created_at FROM users WHERE id = ?',
[id]
)
return rows.length > 0 ? mapRowToUser(rows[0]) : null
}
const findByEmail = async (email: Email): Promise<User | null> => {
const rows = await db.query<UserRow>(
'SELECT id, email, name, created_at FROM users WHERE email = ?',
[email]
)
return rows.length > 0 ? mapRowToUser(rows[0]) : null
}
const save = async (user: User): Promise<void> => {
await db.execute(
`INSERT INTO users (id, email, name, created_at)
VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
email = VALUES(email),
name = VALUES(name)`,
[user.id, user.email, user.name, user.createdAt.toISOString()]
)
}
const existsByEmail = async (email: Email): Promise<boolean> => {
const rows = await db.query<{ count: number }>(
'SELECT COUNT(*) as count FROM users WHERE email = ?',
[email]
)
return rows[0].count > 0
}
return {
findById,
findByEmail,
save,
existsByEmail
}
}
// データベース行の型定義
type UserRow = {
id: string
email: string
name: string
created_at: string
}
// マッピング関数
const mapRowToUser = (row: UserRow): User => ({
id: row.id as UserId,
email: row.email as Email,
name: row.name,
createdAt: new Date(row.created_at)
})
外部API連携サービス
// 決済サービスの実装
type PaymentConfig = {
readonly apiKey: string
readonly apiUrl: string
readonly timeout: number
}
const createPaymentInfrastructureService = (
config: PaymentConfig
): PaymentService => {
const processPayment = async (
amount: Money,
paymentMethod: PaymentMethod
): Promise<PaymentResult> => {
try {
// 外部決済APIとの通信
const requestBody = {
amount: amount.amount,
currency: amount.currency,
payment_method: {
type: paymentMethod.type,
...paymentMethod.details
}
}
const response = await fetch(`${config.apiUrl}/payments`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${config.apiKey}`
},
body: JSON.stringify(requestBody),
signal: AbortSignal.timeout(config.timeout)
})
if (!response.ok) {
const errorData = await response.json()
return {
type: 'Failed',
reason: errorData.message || 'Payment processing failed'
}
}
const data = await response.json()
return {
type: 'Success',
transactionId: data.transaction_id
}
} catch (error) {
if (error instanceof Error) {
return {
type: 'Failed',
reason: `Payment service error: ${error.message}`
}
}
return {
type: 'Failed',
reason: 'Unknown payment error'
}
}
}
return { processPayment }
}
実践的な設計パターン
Dependency Injectionパターン
関数型アプローチでは、高階関数を使ってDependency Injectionを実現します:
// サービス全体をまとめるコンテナ
type ServiceContainer = {
readonly userRepository: UserRepository
readonly emailService: EmailService
readonly paymentService: PaymentService
readonly userRegistrationService: ReturnType<typeof createUserRegistrationService>
readonly orderProcessingService: ReturnType<typeof createOrderProcessingService>
}
// コンテナの作成
const createServiceContainer = (
dbConnection: DatabaseConnection,
emailConfig: EmailConfig,
paymentConfig: PaymentConfig
): ServiceContainer => {
// Infrastructure Serviceの作成
const userRepository = createUserRepository(dbConnection)
const emailService = createEmailInfrastructureService(emailConfig)
const paymentService = createPaymentInfrastructureService(paymentConfig)
// Application Serviceの作成(依存性を注入)
const userRegistrationService = createUserRegistrationService(
userRepository,
emailService
)
const orderProcessingService = createOrderProcessingService(
userRepository,
createProductRepository(dbConnection), // 省略した実装
createOrderRepository(dbConnection), // 省略した実装
createInventoryService(), // 省略した実装
paymentService
)
return {
userRepository,
emailService,
paymentService,
userRegistrationService,
orderProcessingService
}
}
Compositionパターン
複数のServiceを組み合わせて、より複雑な処理を実現:
// 複数のサービスを組み合わせた複合処理
const createOrderWorkflowService = (
orderProcessingService: ReturnType<typeof createOrderProcessingService>,
emailService: EmailService,
inventoryService: InventoryService
) => {
const processCompleteOrder = async (
command: CreateOrderCommand
): Promise<Result<OrderWorkflowResult, OrderWorkflowError>> => {
// 1. 注文の作成
const orderResult = await orderProcessingService.createOrder(command)
if ('type' in orderResult) {
return mapOrderErrorToWorkflowError(orderResult)
}
const order = orderResult
try {
// 2. 顧客への注文確認メール
await emailService.sendOrderConfirmationEmail(order)
// 3. 在庫システムへの通知
await inventoryService.updateStock(order.items)
// 4. 発送準備の開始
await initiateShippingProcess(order)
return {
order,
status: 'completed',
processedAt: new Date()
}
} catch (error) {
// エラー時のロールバック処理
await handleOrderProcessingError(order, error)
return {
type: 'ProcessingFailed',
orderId: order.id,
error: error instanceof Error ? error.message : 'Unknown error'
}
}
}
return { processCompleteOrder }
}
type OrderWorkflowResult = {
readonly order: Order
readonly status: 'completed'
readonly processedAt: Date
}
type OrderWorkflowError =
| { type: 'OrderCreationFailed'; details: OrderCreationError }
| { type: 'ProcessingFailed'; orderId: OrderId; error: string }
// エラーマッピング関数
const mapOrderErrorToWorkflowError = (
error: OrderCreationError
): OrderWorkflowError => ({
type: 'OrderCreationFailed',
details: error
})
// ヘルパー関数(実装は省略)
const initiateShippingProcess = async (order: Order): Promise<void> => {
// 発送処理の実装
}
const handleOrderProcessingError = async (order: Order, error: unknown): Promise<void> => {
// エラーハンドリングとロールバック処理
}
よくある問題とその対策
1. 貧血症モデルの回避
// ❌ 悪い例:すべてのロジックがServiceに集中
const userService = {
updateProfile: async (userId: UserId, name: string, email: string) => {
const user = await userRepository.findById(userId)
if (!user) throw new Error('User not found')
// すべてのバリデーションと更新ロジックがServiceに
user.name = name
user.email = email as Email
await userRepository.save(user)
},
changePassword: async (userId: UserId, oldPassword: string, newPassword: string) => {
// パスワード変更ロジックもServiceに
},
deactivateAccount: async (userId: UserId) => {
// アカウント無効化ロジックもServiceに
}
}
// ✅ 良い例:Entityの操作は専用の関数群として分離
const userOperations = {
updateProfile: (
user: User,
name: string,
email: Email
): Result<User, ProfileUpdateError> => {
// バリデーション
const nameValidation = validateUserName(name)
if ('type' in nameValidation) return nameValidation
const emailValidation = validateUserEmail(email)
if ('type' in emailValidation) return emailValidation
// 不変更新
return {
...user,
name: nameValidation,
email: emailValidation
}
},
changePassword: (
user: User,
newPasswordHash: string
): User => ({
...user,
passwordHash: newPasswordHash,
passwordChangedAt: new Date()
}),
deactivate: (user: User): User => ({
...user,
status: 'inactive',
deactivatedAt: new Date()
})
}
// Application Serviceは調整に専念
const createUserApplicationService = (
userRepository: UserRepository,
emailService: EmailService
) => ({
updateProfile: async (
command: UpdateProfileCommand
): Promise<Result<User, UpdateProfileError>> => {
const user = await userRepository.findById(command.userId)
if (!user) {
return { type: 'UserNotFound', userId: command.userId }
}
// Domain操作を呼び出し
const updateResult = userOperations.updateProfile(
user,
command.name,
command.email
)
if ('type' in updateResult) return updateResult
// 永続化
await userRepository.save(updateResult)
// 通知
await emailService.sendProfileUpdatedEmail(updateResult.email)
return updateResult
}
})
2. Serviceの肥大化防止
// ❌ 悪い例:巨大なサービス
const orderService = {
createOrder: () => { /* ... */ },
cancelOrder: () => { /* ... */ },
updateOrder: () => { /* ... */ },
calculateShipping: () => { /* ... */ },
processPayment: () => { /* ... */ },
generateInvoice: () => { /* ... */ },
sendNotifications: () => { /* ... */ },
updateInventory: () => { /* ... */ },
// 数十のメソッド...
}
// ✅ 良い例:責務ごとに分割
namespace OrderServices {
// 注文作成に特化
export const creation = {
createOrder: (/* params */) => { /* 注文作成 */ },
validateOrderItems: (/* params */) => { /* 注文項目検証 */ }
}
// 注文管理に特化
export const management = {
cancelOrder: (/* params */) => { /* 注文キャンセル */ },
updateOrder: (/* params */) => { /* 注文更新 */ }
}
// 配送関連に特化
export const shipping = {
calculateShipping: (/* params */) => { /* 配送料計算 */ },
scheduleDelivery: (/* params */) => { /* 配送スケジュール */ }
}
// 決済関連に特化
export const payment = {
processPayment: (/* params */) => { /* 決済処理 */ },
refundPayment: (/* params */) => { /* 返金処理 */ }
}
}
// 使用例
const order = await OrderServices.creation.createOrder(command)
const shippingCost = OrderServices.shipping.calculateShipping(order)
3. テスタビリティの向上
// テスト用のモックサービス作成
const createMockUserRepository = (): UserRepository => {
const users = new Map<UserId, User>()
return {
findById: async (id: UserId) => users.get(id) || null,
findByEmail: async (email: Email) => {
for (const user of users.values()) {
if (user.email === email) return user
}
return null
},
save: async (user: User) => {
users.set(user.id, user)
},
existsByEmail: async (email: Email) => {
for (const user of users.values()) {
if (user.email === email) return true
}
return false
}
}
}
const createMockEmailService = (): EmailService => ({
sendWelcomeEmail: async () => {
console.log('Mock: Welcome email sent')
},
sendVerificationEmail: async () => {
console.log('Mock: Verification email sent')
}
})
// テストの例
const testUserRegistration = async () => {
// Arrange
const mockUserRepository = createMockUserRepository()
const mockEmailService = createMockEmailService()
const userRegistrationService = createUserRegistrationService(
mockUserRepository,
mockEmailService
)
const command: RegisterUserCommand = {
email: 'test@example.com',
name: 'Test User',
password: 'SecurePassword123!'
}
// Act
const result = await userRegistrationService.registerUser(command)
// Assert
if ('type' in result) {
throw new Error(`Registration failed: ${result.type}`)
}
console.log('User registration test passed:', result)
// 副作用の確認
const savedUser = await mockUserRepository.findByEmail('test@example.com' as Email)
console.log('User saved successfully:', savedUser !== null)
}
まとめ
本記事では、TypeScriptの関数型アプローチを用いたDDDのServiceパターンの実装方法を詳しく解説しました。
重要なポイント
1. 適切な責務分離
- Domain Service: ビジネスロジック
- Application Service: ユースケースの調整
- Infrastructure Service: 技術的な処理
2. 型安全性の活用
- ブランド型による型の強化
- Result型によるエラーハンドリング
- 詳細なエラー型定義
3. 関数型アプローチの利点
- 不変性による予測可能性
- 純粋関数によるテスタビリティ
- 関数合成による柔軟性
4. 実践的な設計パターン
- Dependency Injection
- Service Composition
- 適切な粒度での分割
実装時の注意点
- 貧血症モデルを避ける: EntityやValue Objectの責務を適切に分離
- Serviceの肥大化を防ぐ: 単一責任の原則を守る
- テスタビリティを重視: モックしやすい設計にする
- エラーハンドリング: 型安全なエラー処理を実装
このアプローチにより、保守性が高く、テストしやすい、堅牢なDDDアプリケーションを構築することができます。実際のプロジェクトでは、チームの習熟度や要件に応じて、段階的に導入することをお勧めします。
この記事が、DDDとTypeScriptを組み合わせた開発の参考になれば幸いです。質問や改善点があれば、お気軽にコメントください。
Discussion