Interactive

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層のエラーは技術的な原因(NetworkErrorTimeoutError等)を表しています。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層を導入します。