前回
型と実装の分離の続き。
実際に書いてみる
前回の記事で分離したクラスを使用するアプリケーション層を書いていくよ。
public class TalkToGPTApplicationService {
//ChatGPT型の変数を定義
public String getResponse(String text) {
chatGPT.getResponse(text);
}
}
chatGPTという変数はChatGPT型という定義をしている(ChatGPTImple型ではない)ので、実装に何を使用しているのかを意識せずにコードを書き進めることができるよ。
あれっ?変数定義、どうやってるの?隠してない?
あっ、あの、隠してないです。
public class TalkToGPTApplicationService {
ChatGPT chatGPT = new ChatGPTImple();
}
あっれれ~?おっかしいぞ~?
実際に動くクラスを意識しなくていいと 豪語 しながら、実際に動くクラス、 書いちゃってませんか~?
といういちゃもんに対抗すべく、Spring Boot というフレームワークを活用していきます。
Spring Bootを使用したDIの実現
上のいちゃもんはまあ正しいです。
そのままのJavaの仕組みでは完全に依存しないことは難しいです。できるみたいだけど。
じゃあどうするの?
そこで、Spring Bootの機能である 依存性注入(DI) を使用していきます。
//サービスクラスであることを明示的に定義
@Service
public class TalkToGPTApplicationService {
//ChatGPT型の変数を定義
private final ChatGPT chatGPT;
//受け取った引数をフィールドに代入するコンストラクタの定義
public TalkToGPTApplicationService(ChatGPT chatGPT){
this.chatGPT = chatGPT;
}
}
これだけです。
正確には
//DIコンテナで管理するコンポーネントであることを明示的に定義
@Component
public ChatGPTImpl implements ChatGPT
です。
これをすることによって、DIコンテナが型に基づいて実装を注入してくれるので、実装を完全に分離することが可能になりました。
アプリケーションを完成させる。
コントローラー(プレゼンテーション層)
//RESTfulなコントローラーであることを定義
@RestController
class TalkToGPTController{
private final TalkToGPTApplicationService talkToGPTApp;
//DIコンテナを使用して依存性を解決
public TalkToGPTController(TalkToGPTApplicationService talkToGPTApp) {
this.talkToGPTApp = talkToGPTApp;
}
//POSTリクエスト時に動作するメソッドであることを定義
@PostMapping("/talk")
//パラメーターからnameとtextを取得する
String talk(@RequestParam String name, @RequestParam String text){
return talkToGPTApp.getResponse(name,text);
}
}
アプリケーションサービス(アプリケーション層)
@Service
public class TalkToGPTApplicationService {
private final ChatGPT chatGPT;
public TalkToGPTApplicationService(ChatGPT chatGpt) {
this.chatGPT = chatGPT;
}
public String getResponse(String name, String text) {
return chatGPT.sendRequest(new AiRequest(text, name));
}
}
ドメインモデル(ドメイン層)
@Getter
public class AiRequest {
private final String text;
private final String prompt;
public AiRequest(String text,String name) {
this.text = text;
prompt = "あなたの名前はごふです。"
+ "会話相手の名前は「" + name + "」です。"
+ "敬語を使わないでください。";
}
}
インフラストラクチャの型定義(ドメイン層)
public interface ChatGPT {
String sendRequest(AiRequest request);
}
インフラストラクチャの実装(インフラストラクチャ層)
class ChatGPTImpl implements ChatGPT{
//application.propertiesから値を取得
//トークンなどをハードコーディングする必要がなく安全
@Value("${openai.service.token}")
private String token;
private final OpenAiService openAiService = new OpenAiService(token);
//実装の詳細な解説は割愛
@Override
public String sendRequest(AiRequest aiRequest) {
List<ChatMessage> chatMessages = new ArrayList<>();
chatMessages.add(new ChatMessage(ChatMessageRole.SYSTEM.value(), aiRequest.getPrompt()));
chatMessages.add(new ChatMessage(ChatMessageRole.USER.value(),aiRequest.getText()));
ChatCompletionRequest request = ChatCompletionRequest.builder()
.messages(chatMessages)
.model("gpt-3.5-turbo-0301")
.build();
List<ChatCompletionChoice> choices = openAiService.createChatCompletion(request).getChoices();
StringBuilder response = new StringBuilder();
choices.forEach(choice -> {
response.append(choice.getMessage().getContent());
});
return response.toString();
}
}
以上です。
上記のコードはアプリケーション層がかなり薄くなっているので、あまり意義は感じられないかもしれないし、実際、このような設計は過剰に適用するとパフォーマンスの低下につながることがあるので、 業務の複雑さと相談 しながら適用するべきだよ。
まとめ
パッケージ(レイヤ) で分けてみよう。
情報を取得するところがプレゼンテーション層
プレゼンテーション層とドメイン層の繋ぎ目を担うのがアプリケーション層
業務上実現したいことを記述するのがドメイン層
業務を達成するためにやるべきことをインフラストラクチャ層が担当する
位の認識から始めてみよう。
型と実装 を分けてみよう。
型と実装を分けることで、依存性の逆転や変更容易性を高めよう。
フレームワーク や DI を活用して実装に落とし込めてみよう。
フレームワークやDIを活用することで、よりシンプルな記述でかつ変更容易性をさらに高めてみよう。