Application層
複数のドメインオブジェクトや外部サービスを調整する層です。弊社ではServiceクラスで1つのユースケースを表現します。
Serviceクラスの構造
すべてのServiceクラスはコンストラクタでContextを受け取り、executeメソッドを持ちます。AdapterやRepositoryはコンストラクタのデフォルト引数でDIし、Contextを渡します。戻り値はT | Errorとし、throwしません。
type Props = {
email: string
name: string
}
export class UserRegistrationService {
constructor(
private readonly c: Context,
private readonly deps = {
email: new EmailAdapter(c),
repo: new UserRepository(c),
}
) {}
async execute(props: Props): Promise<User | Error> {
const user = await this.deps.repo.write(props)
if (user instanceof Error) {
return new Error('ユーザーの作成に失敗しました')
}
const sent = await this.deps.email.sendWelcome(user.email)
if (sent instanceof Error) {
return new Error('ウェルカムメールの送信に失敗しました')
}
return user
}
}
テスト時はdepsにモックを渡すだけで差し替えられます。
const service = new UserRegistrationService(mockContext, {
email: { sendWelcome: vi.fn(() => true) },
repo: { write: vi.fn(() => mockUser) },
})
エラーの詰め替え
Infrastructure層のエラーは技術的な原因(NetworkError、TimeoutError等)を表しています。Application層ではカスタムエラーに詰め替えて、アプリケーション的に何が起きたのかを明確にします。lib/errors.tsに定義したカスタムエラーを使い、Interface層がinstanceofで分岐できるようにします。
import { PaymentFailedError } from '@/lib/errors'
export class PaymentService {
constructor(
private readonly c: Context,
private readonly deps = {
stripe: new StripeAdapter(c),
repo: new PaymentRepository(c),
}
) {}
async execute(props: Props): Promise<Payment | Error> {
// Infrastructure層: NetworkError等が返る
const result = await this.deps.stripe.createPayment({
amount: props.amount,
customerId: props.customerId
})
// カスタムエラーに詰め替え
if (result instanceof Error) {
return new PaymentFailedError('決済に失敗しました')
}
const saved = await this.deps.repo.write({
intentId: result.id,
amount: props.amount,
})
if (saved instanceof Error) {
return new PaymentFailedError('決済記録の保存に失敗しました')
}
return saved
}
}
import { RefundFailedError } from '@/lib/errors'
export class RefundService {
constructor(
private readonly c: Context,
private readonly deps = {
stripe: new StripeAdapter(c),
}
) {}
async execute(props: { paymentIntentId: string }): Promise<Refund | Error> {
const result = await this.deps.stripe.refund(props.paymentIntentId)
if (result instanceof Error) {
return new RefundFailedError('返金に失敗しました')
}
return result
}
}
Interface層がこのカスタムエラーをinstanceofで判定し、ユーザーに表示するメッセージやHTTPステータスコードを決定します。
Serviceが不要な場合
単純なCRUD操作や単一の外部API呼び出しはService層を経由せず直接実装します。2つ以上の処理を組み合わせる場合にService層を導入します。