接口如同契约。接口并不包含任何代码实现,只是定义了一个实现该接口的对象必须实现的一系列方法。
如果一个对象实现了一个接口,那么我们就能保证这个接口所定义的一系列方法都能在这个对象上调用。
由于有接口契约保证特定方法的实现,通过多态也能使类型安全的语言变得更灵活。
关于多态:多态含义很广,从本质上说,是一个实体拥有多种形式。在本书中,我们讲多态说的是一个接口有多钟实现方式。例如,UserRepositoryInterface 可以有 MySQL 和 Redis 两种实现,并且每一种实现都是 UserRepositoryInterface 的一个实例。
为了说明接口在强类型语言中的灵活性,我们们来写一个简单的酒店客房预订代码。考虑以下接口:(代码可滑动查看)
interface ProviderInterface
{
public function getLowestPrice($location);
public function book($location);
}
当用户预订房间时,我们需要将此事记录在系统里。所以在 User
类里添加如下方法:(代码可滑动查看)
class User
{
public function bookLocation(ProviderInterface $provider, $location)
{
$amountCharged = $provider->book($location);
$this->logBookedLocation($location, $amountCharged);
}
}
由于我们对 $provider
做了类型约束,在 User
类的 bookLocation
方法中,就可以放心大胆的认为 $provider
实例上的 book
方法是可以调用的。
这给我们复用 bookLocation
方法带来了灵活性,完全不必关心用户倾向哪家酒店提供商。最后,我们编写一些代码来体验下这种灵活性:(代码可滑动查看)
$location = '希尔顿, 达拉斯';
$cheapestProvider = $this->findCheapest($location, array(
new PricelineProvider,
new OrbitzProvider,
));
$user->bookLocation($cheapestProvider, $location);
不管哪家酒店是最便宜的,我们都能够将它传入 User
对象来预订房间了。由于 User
对象只需要有一个遵从 ProviderInterface
契约的对象实例就可以了,所以未来如果有新的酒店供应商,我们的代码也可以很好的工作。
忘掉细节:记住,接口实际上并不做任何事情。它只是简单的定义了实现类必须拥有的一系列方法。
构建大型应用
当你的团队在构建大型应用时,不同的功能模块往往有着不同的开发进度。例如,一个开发人员在开发数据层,另一个开发人员在做前端和控制器层。前端开发者想要测试他的控制器,但是后端开发进度比较慢,无法联调。
如果这两个开发者能以接口或契约的方式达成协议,然后后端开发的所有类都遵循这种协议,就像下面这段代码:(代码可滑动查看)
interface OrderRepositoryInterface
{
public function getMostRecent(User $user);
}
一旦建立了契约,就算契约还没有真正实现,前端开发者也可以测试他的控制器了!这样一来,应用中的不同组件就可以按不同的速度开发,同时仍然允许编写适当的单元测试。
此外,这种方式还可以使组件内部的改动不会影响到其它不相关的组件。要始终牢记「无知是福」。我们不想让类知道依赖是如何工作的,只需要知道它们能做什么。所以,先定义好契约,再来写控制器:(代码可滑动查看)
class OrderController {
public function __construct(OrderRepositoryInterface $orders)
{
$this->orders = $orders;
}
public function getRecent()
{
$recent = $this->orders->getMostRecent(Auth::user());
return View::make('orders.recent', compact('recent'));
}
}
前端开发者甚至可以为这接口写个「假」实现,然后这个应用的视图就可以用假数据渲染了:(代码可滑动查看)
class DummyOrderRepository implements OrderRepositoryInterface
{
public function getMostRecent(User $user)
{
return array('Order 1', 'Order 2', 'Order 3');
}
}
编写好假实现之后,就可以在服务容器里将其绑定到契约上,然后在整个应用中都可以调用它了:(代码可滑动查看)
$this->app->bind(OrderRepositoryInterface::class, function ($app) {
return new DummyOrderRepository();
});
接下来,如果后台开发者写完了真正的实现代码,如RedisOrderRepository
。服务容器中的绑定可以轻松切换到新的实现,整个应用将会使用开始从 Redis 读取出来的订单数据。
接口即纲领:接口有助于开发应用所提供的、已定义好的功能「框架」。 在组件的设计阶段,团队里使用接口进行讨论是很方便的,例如,定义一个 BillingNotifierInterface 接口,然后讨论它提供哪些方法。在编写任何实现代码前,最好先通过接口讨论达成一致,这是构建一套好 API 的必要前提!