面向接口编程!
当你对编程越来越认真时,你不可避免地会在视频、书籍或文章中遇到“面向接口编程”这个词语。一开始我们或许都不能真正理解其中含义。我们真的需要创建一个接口然后实现它。我们如何确定何时何地使用这些接口?每当看教程或读文章时,它们都会解释什么是接口,“这是一个没有实现的类”,但是为什么以及何时使用它呢。
我们先来写一些代码
当下 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;
}
我们提供了自己的接口,也或者可以称之为 contract。让我们实现它。
<?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 容器,这一概念被称为控制反转。这是因为每个提供程序通常都需要特定的配置。
我们现在有两个提供者可以用来回答问题。无论它们如何实现,我们相信,当提出问题时,它们会连接到它们的 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,你也找不到任何引用。相反,我们依赖于 contract/interface。而且,不知何故,我们可以在现实生活中与这个例子联系起来;我们都至少做过一次聊天机器人(Chatbot
)。
假设你购买一个太阳能电池面板系统。该公司承诺会派技术人员来安装,并向ni 保证,无论他们派谁来,工作都会完成,最终你会安装好面板。所以,你真的不在乎他们是派谁来。RandomAi
和 OpenAi
均为 AIProvider
的员工;你提出一个问题,他们就会给出答案。就像你不关心谁安装面板一样,聊天机器人也不应该关心谁来做这项工作。它只需要知道所提供的任何实现都可以做到这一目标。
现在,你可以自由使用其中一个或另一个
$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 Provider,并且你的代码将始终保持相同的行为。你不必更改任何内容,因为你面对接口编程,所以我们之前提出的任何问题都不会成为问题。
还有其他好处
在我们的例子中,通过对接口进行编程,我们也遵守了 SOLID 原则中的三个原则,饿哦们甚至不知道我们遵守了,让我详细说明一下。
我不会详述;每个原则都可以有一篇长文。这只是一个简短的解释,展示了我们通过对接口进行编码所获得的结果。I
开闭原则
我们尊重的第一个原则是开闭原则,该原则规定代码实现应该对扩展开放,对修改关闭。尽管听起来很有挑战性,但你已经做到了。想想看,Chatbot
现在已经关闭,无法进行修改;我们不会再碰代码了。这是我们从一开始的目标。但是,它是开放的扩展;如果我们要添加第三个、第四个甚至第五个 Provider 程序,没有什么能阻止我们。我们可以实现接口,我们的类可以开箱即用,不需要任何更改。
里氏替换原则(Liskov Substitution Principle, LSP)
我不会用它的定义来烦你,但基本上,它规定你可以用所有的子类替换类,反之亦然。从技术上讲,我们所有的 AI 提供者都是一个 AIProvider
,它们的实现可以相互交换,而不会影响 ChatBot
的正确性,后者甚至不知道它使用的是哪个提供者😂。
依赖反转
我必须承认,这本书可能有自己的文章。但简单地说,原则规定你应该依赖抽象而不是具体,这正是我们正在做的。我们依赖于一个 Provider,而不是像 Open AI 这样的特定 Provider。
记住,所有这些,都是因为我们编码到了一个接口。
你终会理解
每当你更新一个你知道不应该更新的类,并且你的代码被 if 语句弄得一团糟时,你就需要一个接口。总是问自己,这门课真的需要知道怎么做吗?我会永远使用这个服务提供者吗?还是数据库驱动器?如果不是,你知道该怎么办。
话虽如此,只要给它一些时间,它终会让你理解。