你好 👋
当你开始严肃编程时,你不可避免地会遇到“面向接口编写代码”的说法,无论是在视频中、书中还是文章中。这对我来说从来都不合逻辑。我质疑创建接口然后实现它的必要性。我如何确定何时何地使用这些接口?无论我观看教程还是阅读文章,它们都会解释什么是接口:“它是一个没有实现的类”,而我就像“嗯,谢谢 😏”。我的意思是,我已经知道了;我真正想了解的是为什么要使用它,什么时候使用。
我记得有一天我在Discord社区中提出了一个问题,一位前辈简单地说:“别担心,它最终会为你所理解的”,确实如此,花了些时间,但确实如此。如果你正经历这种情况,要知道,我们都在那里,让我们帮助你理解为什么你需要 面向接口编写代码。
让我们写一些代码
由于AI正在接管,每个人都对它疯狂不已,我们不想错过这场派对。我们想在网站上添加它,一个小型聊天机器人,它将回答关于我们的产品的问题。
我将使用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的价格翻倍,并且继续上涨,我们有哪些选择?我们要么接受命运,要么寻找另一个API。第一个选项很简单,我们继续支付他们,而第二个选项并不是听起来那么简单。新的提供者可能有自己的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
都将通过构造函数注入。这样,我们将复杂的实例化逻辑委托给依赖注入容器,这是一个称为控制反转的概念。这是因为每个提供者通常需要一定的配置。
现在,我们有两个提供者可以用作回答问题。无论它们的实现如何,我们有信心,当给出一个问题,它们将连接到它们的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
。你不需要使用构造函数和setter。
你可以看到我们做了一些调整;我们不再依赖OpenAI,你也看不到任何关于它的引用。相反,我们依赖于合同/接口。而且,某种意义上,我们可以将这个例子与现实生活中联系起来;我们至少都当过一次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提供者的灵活性,而且你的代码将始终表现一致。你不必为此做任何事情,因为你面向接口编码,所以我们之前提出的所有担忧都不会成为问题。
总有一些额外的好处
在我们的例子中,通过实现接口进行编码,我们也遵循了 SOLID 原则中的三个,尽管我们没有意识到这一点,让我详细说明。
我不会详细解释;每条原则都可以写一整篇文章。这里只是简要说明通过接口编码我们所获得的好处。
开闭原则
我们遵循的第一个原则是开闭原则,它表示代码应该对外扩展开放,对修改封闭。这听起来可能很有挑战性,但你们已经做到了。想想看,现在的 ChatBot
对修改是封闭的;我们不会再触及这段代码。这是我们一开始的目标。但是,它对外扩展是开放的;如果我们想要添加第 3、4 或甚至第 5 个提供商,没有什么可以阻止我们。我们可以实现这个接口,我们的类可以无缝使用它,无需任何修改。
Liskov 替换原则
我不会浪费你的时间来定义它,但基本上,它表示你可以用所有子类替换类,反之亦然。技术上,我们所有的 AI 提供者 are-a
AIProvider
,它们的实现可以互相替换,而不会影响 ChatBot
的正确性,后一个甚至不清楚它使用的是哪个提供商😂,所以是的,我们已经遵循了李斯柯夫的原则。
依赖倒置原则
我必须承认,这可以写成一整篇文章。但简单来说,这个原则表示你应该依赖抽象而不是具体实现,这正是我们正在做的事情。我们是依赖一个提供商,而不是一个具体的提供商,如 Open AI。
记住,所有这一切,都是因为我们实现了接口。
最终你会有所领悟
每次当你更新一个你本不应该更新的类,你的代码因为 if 语句变得混乱不堪时,你需要一个接口。总是问问自己,这个类真的需要知道“怎么做”吗?我会永远使用这个服务提供商吗?或者数据库驱动程序?如果不是,你知道该怎么做。
话虽如此,但请给它一些时间,它最终会让你有所领悟。
thatgardnerone, s010m0n 点赞了这篇文章