Laravel:依赖注入
整个 Laravel 框架的基石是一个功能强大的 IoC 容器(控制反转容器),如果你想真正从底层理解 Laravel 框架,就必须好好掌握它。不过,也不要被这个名头吓住,要知道 IoC 容器只不过是一种用于方便我们实现「依赖注入」这种软件设计模式的工具。而且要实现依赖注入并不一定非要通过 IoC 容器,只是使用 IoC 容器会更容易一点儿。首先,来看看我们为何要使用依赖注入,或者说它能为我们..
整个 Laravel 框架的基石是一个功能强大的 IoC 容器(控制反转容器),如果你想真正从底层理解 Laravel 框架,就必须好好掌握它。不过,也不要被这个名头吓住,要知道 IoC 容器只不过是一种用于方便我们实现「依赖注入」这种软件设计模式的工具。而且要实现依赖注入并不一定非要通过 IoC 容器,只是使用 IoC 容器会更容易一点儿。
首先,来看看我们为何要使用依赖注入,或者说它能为我们的软件开发带来什么好处。考虑下列代码中的类和方法:
class UserController extends BaseController
{
public function getIndex()
{
$users = User::all();
return View::make('users.index', compact('users'));
}
}
这段代码看起来很简洁,但是不与数据库打交道的话,我们将无法测试这段代码。也就是说,Eloquent ORM 和该控制器有着紧耦合关系。如果不使用 Eloquent ORM,不连接到实际数据库,我们就没办法运行或者测试这段代码。同时,这段代码也违背了「关注点分离」这个软件设计原则。简单来讲:控制器知道的太多了。控制器不需要去了解数据是从哪儿来的,只要知道如何访问就行。控制器也不需要知道数据在 MySQL 中是否有效,只需要知道它目前是可用的。
关注点分离:每一个类都应该是单一职责的,并且这个职责应该完全被这个类封装。
所以,如果可以完全解耦 Web 控制器层和数据访问层解耦,将会给我们带来诸多便利:这会使得迁移数据存储实现更容易;也会使得代码测试更容易。「Web控制器」的职责就是真实应用的传输层:仅负责收集用户请求数据,然后将其传递给处理方。
假设你有一个类似于监控器的应用程序,该应用有很多线缆接口,你可以通过这些接口来访问监控器的功能,接口包括 HDMI,VGA,DVI 等。把互联网想象成另一个插进应用的线缆接口,显示器的大部分功能都是与线缆接口无关的、互相独立的。线缆接口只是一种传输机制,就像 HTTP 只是你程序的一种传输机制一样。所以,我们不想把传输机制(控制器)和业务逻辑混在一起。这样做的好处是很多其他的传输层比如 API 接口、移动 App 等都可以访问我们的业务逻辑。
因此,以后开发代码就别再将控制器和 Eloquent ORM 耦合在一起了,咱们来注入一个仓库类吧。
建立约定
首先,我们来定义一个接口,然后实现该接口。
interface UserRepositoryInterface
{
public function all(): array;
}
class DbUserRepository implements UserRepositoryInterface
{
public function all(): array
{
return User::all()->toArray();
}
}
然后,我们将该接口的实现注入到我们的控制器。
class UserController extends BaseController
{
public function __construct(UserRepositoryInterface $users)
{
$this->users = $users;
}
public function getIndex()
{
$users=$this->users->all();
return View::make('users.index', compact('users'));
}
}
现在,我们的控制器就完全不知道数据存储在哪了。在这里,无知是福!我们的数据可能来自 MySQL、MongoDB 或者 Redis,我们的控制器不知道也不需要知道到底用的是什么数据库,以及它们是如何存储数据的,在具体实现上有什么区别。仅仅做出了这么小小的改变,我们就可以独立于数据层来测试 Web 层了,将来如果需要的话,切换存储实现也会很容易,两者相互独立,只要调用方法名不改,我们的控制器代码不用做任何改动。
严守边界:始终牢记保持明确的责任边界,控制器和路由是作为 HTTP 和应用程序之间的中介者来提供服务的(用户浏览应用的时候,路由/控制器作为中介将其引导到对应的服务)。当编写大型应用程序时,不要将你的领域逻辑混杂在控制器或路由中。
为了巩固你对这一理念的理解,我们来写一个测试案例。首先,我们要通过 Mockery 动态模拟一个仓库类实例,并将其绑定到应用的 IoC 容器里。然后,发起一个请求,通过断言判定控制器是否正确地调用了这个仓库类:
public function testUserTest()
{
$repository = \Mockery::mock(UserRepositoryInterface::class);
$repository->shouldReceive('all')->once()->andReturn(['学院君']);
$this->instance(UserRepositoryInterface::class, $repository);
$response = $this->get('/users');
$response->assertStatus(200);
$response->assertViewHas('users', ['学院君']);
}
更进一步
让我们考虑另一个例子来巩固理解。当付费会员订阅的某项服务周期快结束了,可能需要去提醒用户该续费了。我们会定义两个接口,或者叫契约(这些契约使我们在更改实际实现时更加灵活),一个是支付接口,一个是通知接口:
interface BillerInterface
{
public function bill(array $user, $amount);
}
interface BillingNotifierInterface
{
public function notify(array $user, $amount);
}
接下来我们要写一个 BillerInterface
接口的实现:
class StripeBiller implements BillerInterface
{
public function __construct(BillingNotifierInterface $notifier)
{
$this->notifier = $notifier;
}
public function bill(array $user, $amount)
{
// Bill the user via Stripe...
$this->notifier->notify($user, $amount);
}
}
通过将责任划分到不同类中,我们现在可以很容易将不同的通知实现类注入到账单类里面。比如,我们可以注入一个 SmsNotifier
或者 EmailNotifier
。账单类只需遵守了自己的契约即可(实现了账单接口方法),不需要考虑如何实现通知功能。只要是遵守账单通知契约(接口)的类,账单类都可以用。这不仅让我们的开发维护更加灵活,而且还可以通过模拟BillingNotifierInterface
实现类来进行账单类的隔离测试,就像我们在上一个测试用例里做的那样。
面向接口开发:编写接口看上去好像要多写一些代码,但是磨刀不误砍柴工,对于大型项目而言实际上反而能提升你的开发效率,这就是软件设计领域经常说的面向接口开发,而不是面向对象开发。从测试角度来说,你不用实现任何接口,就能通过 Mockery 库模拟接口实现实例,进而测试整个后端逻辑!
前面说了这么多,回到我们的主题,我们要如何做依赖注入呢?很简单:
$biller = new StripeBiller(new SmsNotifier);
这就是一个依赖注入。账单类 StripeBiller
不用考虑如何通知用户,我们直接传递给它一个通知实现类 SmsNotifier
的实例。从代码角度来说,这可能只是个微小的变动,但这种设计模式的引入,绝对会使你的整个应用架构焕然一新:因为明确指定了类的职责边界,实现了不同层和服务之间的解耦,你的代码变得更加容易维护;此外,从面向接口编程的角度来看,代码变得更加容易测试,你只需通过模拟注入依赖即可,不同类之间的测试完全可以隔离开来。
那么 IoC 容器呢?难道依赖注入不需要 IoC 容器了么?当然不需要!在接下来的章节里面你会了解到,IoC 容器使得依赖注入更易于管理,但是容器本身不是依赖注入所必须的。只要遵循本章提出的原则,你可以在任何项目里面实现依赖注入,而不必管该项目是否提供了容器。
更多推荐
所有评论(0)