プログラミングに真剣に取り組むようになると、ビデオ、書籍、記事などで「インターフェイスにコードを書く」というフレーズに必ず遭遇します。しかし、私にはそれがまったく理解できませんでした。インターフェイスを作成してから実装する必要性に疑問を感じていました。これらのインターフェイスをいつ、どこで使用するかをどのように判断すればよいのでしょうか。
チュートリアルを見たり記事を読んだりするたびに、インターフェースとは何かが「実装のないクラスです」と説明されていて、私は「うーん、ありがとうございます 😏」という感じです。つまり、それはすでに知っていたのですが、本当に知りたかったのは、なぜ、いつそれを使うのかということでした。
ある日、Discord コミュニティで質問したところ、先輩の 1 人が「心配しないでください。そのうち理解できるようになります」とだけ言っていました。実際、時間はかかりましたが、理解できました。このような状況に陥っている場合は、私たち全員が同じ経験をしたことがあるということを知っておいてください。インターフェースに合わせてコーディングする必要がある理由を理解できるようにお手伝いしましょう。
AI が主流になりつつあり、誰もがそれに熱狂しているので、私たちは遅れをとりたくありません。それを当社の Web サイトに追加し、当社の製品に関する質問に答える小さなチャットボットで実現したいと考えています。
この例では PHP を使用しますが、使い慣れた言語を自由に使用してください。重要なのは概念です。
私たちのチャットボットは次のようにシンプルです:
<?php class ChatBot { public function ask(string $question): string { $client = new OpenAi(); $response = $client->ask($question); return $response; } }
ask()
という単一のメソッドがあり、 OpenAI
SDKを使用して API に接続し、質問を投げかけて、応答を返すだけです。
チャットボットの使用を開始できるようになりました
$bot = new ChatBot(); $response = $bot->ask('How much is product X'); // The product costs $200.
これまでのところ、実装は順調で、期待どおりに機能しており、プロジェクトは展開されて使用されています。しかし、私たちのチャットボットが Open AI API に大きく依存していることは否定できません。皆さんも同意していただけると思います。
さて、Open AI の価格が 2 倍になり、さらに上昇し続けるシナリオを考えてみましょう。選択肢は何でしょうか? 運命を受け入れるか、別の API を探すかのどちらかです。最初の選択肢は簡単で、支払いを続けるだけですが、2 番目の選択肢は思ったほど簡単ではありません。新しいプロバイダーには独自の API と SDK がある可能性が高いため、Open AI 用に元々設計されたすべてのクラス、テスト、および関連コンポーネントを更新する必要があります。これは大変な作業です。
これによって懸念も生じます。新しい API が精度の点で期待に応えられなかったり、ダウンタイムが増加したりしたらどうなるでしょうか。異なるプロバイダーを同時に試したい場合はどうでしょうか。たとえば、サブスクライブしたクライアントには OpenAI クライアントを提供し、ゲストにはよりシンプルな API を使用するなどです。これがいかに複雑になるかおわかりでしょう。その理由は、コードの設計が適切ではなかったためです。
私たちにはビジョンがありませんでした。ただ API を選択し、それとその実装に完全に依存していました。さて、「コードからインターフェースへ」という原則があれば、これらすべてから私たちを救えたでしょう。どのようにでしょうか? 見てみましょう。
インターフェースの作成から始めましょう:
<?php interface AIProvider { public function ask(string $question): string; }
インターフェース、または私が好んで呼ぶ「コントラクト」ができました。それを実装するか、それに合わせてコードを記述してみましょう。
<?php class OpenAi implements AIProvider { public function ask(string $question): string { $openAiSdk = new OpenAiSDK(); $response = $openAiSdk->ask($question); return "Open AI says: " . $response; } } class RandomAi implements AIProvider { public function ask(string $question): string { $randomAiSdk = new RandomAiSDK(); $response = $randomAiSdk->send($question); return "Random AI replies: " . $response->getResponse(); } }
実際には、
OpenAiSDK
とRandomAiSDK
両方がコンストラクターを通じて注入されます。このようにして、複雑なインスタンス化ロジックをDI コンテナーに委任します。これは、制御の反転と呼ばれる概念です。これは、各プロバイダーが通常、特定の構成を必要とするためです。
これで、質問に答えるために使用できるプロバイダーが 2 つできました。実装に関係なく、質問が与えられた場合、プロバイダーは API に接続して応答すると確信しています。プロバイダーはAIProvider
契約に準拠する必要があります。
さて、私たちのChatBot
では次のことができるようになりました
class ChatBot { private AIProvider $client; // A dependency can be injected via the constructor public function __construct(AIProvider $client) { $this->client = $client; } // It can also be set via a setter method public function setClient(AIProvider $client): void { $this->client = $client; } public function ask(string $question): string { return $this->client->ask($question); } }
この例は、依存関係(この場合は
AIProvider
)を注入する複数の方法を示すことを目的としていることに注意してください。コンストラクターとセッターの両方を使用する必要はありません。
いくつか調整を加えたことがわかります。OpenAI には依存しなくなり、OpenAI への参照も見つかりません。代わりに、コントラクト/インターフェースに依存します。そして、どういうわけか、この例は現実の生活に関連付けることができます。私たちは皆、少なくとも一度はChatBot
になったことがあるのです。
ソーラーパネル システムを購入するとします。会社は、技術者を派遣して設置することを約束し、派遣する従業員が誰であっても作業は完了し、最終的にはパネルが設置されると保証します。したがって、ジョシュとジョージのどちらが派遣されるかは、あまり気にしません。2 人は異なるかもしれませんが、どちらかが他方よりも優れているかもしれませんが、どちらもパネルの設置を請け負っています。
彼らは、「あなたのテレビを修理しているんですよ」とは言いません。彼らには会社から指定された仕事をするRandomAi
があるのです。RandomAi とOpenAi
どちらもAIProvider
の従業員として機能します。つまり、あなたが質問すれば、彼らが答えてくれます。あなたがパネルを誰が取り付けるかを気にしなかったのと同じように、 ChatBot
は誰が仕事をするかをまったく気にするべきではありません。ChatBot は、提供された実装がそれを実行できることを知るだけでよいのです。
今ならどちらかを自由に使用できます。
$bot = new ChatBot(); // For subscribed users $bot = new ChatBot(new OpenAi()); $response = $bot->ask('How much is Product X'); // Open AI says: 200$ // For guests $bot->setClient(new RandomAi()); $response = $bot->ask('How much is Product X'); // Random AI replies: 200$
これで、API プロバイダー全体を柔軟に変更できるようになり、コードは常に同じように動作します。インターフェースにコーディングしたので、何も変更する必要がなく、先ほど挙げた懸念はどれも問題になりません。
この例では、インターフェースにコーディングすることで、知らないうちに 3 つのSOLID原則も尊重していました。詳しく説明しましょう。
詳細は説明しません。それぞれの原則を説明すると長い記事になります。これは、インターフェースにコーディングすることで得られるものを示すための簡単な説明です。
私たちが尊重した最初の原則は、オープン クローズ原則です。これは、コードは拡張に対してオープンで、変更に対してはクローズであるべきというものです。難しいように聞こえるかもしれませんが、あなたはそれを達成しました。考えてみてください。ChatBot ChatBot
変更に対してクローズされており、二度とコードに触れることはありません。これが当初からの私たちの目標でした。
しかし、これは拡張可能です。3 番目、4 番目、または 5 番目のプロバイダーを追加する場合でも、何も妨げることはありません。インターフェイスを実装でき、クラスはそのまま使用でき、変更は必要ありません。
定義を詳しく説明するのは退屈ですが、基本的には、クラスをそのすべてのサブクラスに置き換えることができ、その逆も可能であることを示しています。技術的には、すべての AI プロバイダーはAIProvider
are-a
、その実装はChatBot
の正確性に影響を与えることなく相互に交換できます。ChatBot はどのプロバイダーを使用しているかさえ知りません 😂、つまり、私たちは Liskov 氏を尊敬しています。
正直に言うと、これについては別の記事を書く必要があるでしょう。しかし、簡単に言えば、この原則は、具象ではなく抽象に依存すべきだと述べています。これはまさに私たちが行っていることです。私たちは、Open AI のような特定のプロバイダーではなく、プロバイダーに依存しています。
覚えておいてください、これらすべては、インターフェースにコーディングしたためです。
更新すべきではないとわかっているクラスを更新し、コードが if ステートメントでハッキング状態になっている場合は、必ずインターフェイスが必要です。常に自分自身に問いかけてください。このクラスは本当に how を知る必要があるでしょうか? このサービス プロバイダーはずっと使用するのでしょうか? または、データベース ドライバーはずっと使用するのでしょうか? そうでない場合は、何をすべきかがわかります。
そうは言っても、少し時間をかけてみれば、やがて理解できるようになります。