✍️ はじめに
自分が今取り組んでいるプロジェクトでは、Node
というエンティティがあり、それには「質問」と「回答」が含まれている。
今回実装したいのは、このNodeを受け取って「次に来そうな質問を推論する」というユースケースだ。
一見すると簡単な処理のようにも思えるが、「そのロジックをどこに書くべきか?」という問いに対して、意外なほど深い設計の思索に踏み込むことになった。
🔍 最初の構想:ユースケース層に直接ロジックを書く
最初に試みたのは、以下のようなユースケース層への直書きだった:
func (uc *createExpectedTemplateInteractor) Execute(ctx context.Context, input CreateExpectedTemplateInput) (CreateExpectedTemplateOutput, error) {
node, err := uc.nodeRepo.FindByID(ctx, domain.NodeID(input.NodeID))
// ...
if strings.Contains(node.Question(), "志望動機") {
// 次の質問を追加
}
}
このように strings.Contains
を使っていくつかのルールベース推論を行い、次の質問候補を列挙して返すという構成だ。
🤔 でも、これはユースケース層の責務だろうか?
書いているうちに、ふと疑問が湧いた。
「この“次の質問を推論する”という処理は、ユースケースの責務なのか?」
クリーンアーキテクチャの原則に従えば、ユースケース層はあくまで「処理の流れ」を組み立てる場所。
「どう推論するか」まで担うのは過剰な責務ではないか?
💡 気づき:これはドメイン知識に基づく判断だ
「質問と回答の内容から、次に来そうな質問を導き出す」というのは、人間の知識や経験に基づく意味的な判断である。
つまりこれは、**ドメインロジック(意味のあるビジネス判断)**なのだ。
だから、本来この処理は エンティティにもユースケースにも属さず、ドメインサービスが担うべきだという結論に至った。
🧱 ドメインサービスとして実装する
そこで設計したのがこのインターフェース:
type QuestionInferenceService interface {
InferNextQuestions(node domain.Node) []string
}
そしてドメイン層の domain/service/question_inference_service.go
に、シンプルな初期ロジックを実装した:
func (s *questionInferenceService) InferNextQuestions(node domain.Node) []string {
q := node.Question()
a := node.Answer()
if strings.Contains(q, "志望動機") {
return []string{"なぜその業界を選んだのですか?"}
}
// ...
}
これで、推論ロジックをユースケースから完全に分離できた。
🔄 そして、将来的な拡張の余地を残す
特に意識したのは、「将来この推論ロジックをAI化したくなるかもしれない」という点だ。
そこでドメインサービスをインターフェースとして定義したことが生きてくる。
将来的には、例えばOpenAIのAPIを使ったインフラ実装を用意し、次のようにDIで差し替えるだけで済む。
var svc QuestionInferenceService
if config.UseAI {
svc = infra.NewAIBasedInferenceService()
} else {
svc = service.NewQuestionInferenceService()
}
✅ ユースケース側はこうしてシンプルに
func (uc *createExpectedTemplateInteractor) Execute(ctx context.Context, input CreateExpectedTemplateInput) (CreateExpectedTemplateOutput, error) {
node, err := uc.nodeRepo.FindByID(ctx, domain.NodeID(input.NodeID))
if err != nil {
return CreateExpectedTemplateOutput{}, err
}
nextQs := uc.inferenceService.InferNextQuestions(node)
return uc.presenter.Output(nextQs), nil
}
何をするか(処理の流れ)だけに責務が限定され、読みやすく、拡張にも強くなった。
🧩 学びとまとめ
- 「この処理は“意味”を扱っているか?」と考えることで、ドメインロジックかどうかが判断できる
- ユースケースはフローを制御するだけにすることで、責務が明確になる
- ドメインサービス + インターフェース + DI の組み合わせは最強
- インフラ層での差し替えを前提に作ることで、将来の柔軟性が担保される