0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【備忘録】面接質問の推論ロジックをどこに実装するか──クリーンアーキテクチャと共に考えた設計判断の記録

Last updated at Posted at 2025-04-13

✍️ はじめに

自分が今取り組んでいるプロジェクトでは、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 の組み合わせは最強
  • インフラ層での差し替えを前提に作ることで、将来の柔軟性が担保される
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?