声明:本文并非博主原创,而是来自对《Laravel 4 From Apprentice to Artisan》阅读的翻译和理解,当然也不是原汁原味的翻译,能保证90%的原汁性,另外因为是理解翻译,肯定会有错误的地方,欢迎指正。
欢迎转载,转载请注明出处,谢谢!
依赖注入
问题所在
Laravel框架的基础在于其IoC容器。要想真正了解框架的核心,需要对容器有一定的概念。然而,我们需要注意的是IoC仅是软件设计模式:依赖注入的一种便利的实现形式。容器本身不是依赖注入的必要条件,在框架他只是让其变得更加简便。
首先,让我们探索下为什么依赖注入是有益的。考虑到如下代码中的类和方法:
class UserController extends BaseController { public function getIndex() { $users = User::all(); return View::make('users.index',compact('users')); } }
代码简洁易懂,但是在没有连接到数据库的情况下,我们是无法进行测试的。换句话说, Eloquent ORM 被紧密耦合到控制器中了。在未连接数据库的情况下,我们无法测试当前引用了Eloquent ORM的控制器的方法。这段代码同样违背了软件设计原则 关注点分离(SoC) 。简言之:控制器知道的太多。控制器无需知道数据_从何而来_,只需关注如何接入;无需关心数据库在MysqL中是否可用,而只关心数据在_某处_可用。
关注点分离(Separation Of Concerns):
每个类都应有其单一的职责,并且这个职责由这个类完全封装
所以,将web层(controller)从数据层解耦分离出来会是有益的。这在我们对数据进行存储迁移时是有利的,也会使代码的测试更为简单。将“Web”认为是到“真正”应用的传输层。
想象一下,应用是一台有着多种电缆接口的显示器。我们能通过HDMI,VGA或者DVI接入显示功能。也可将应用比喻成你接入互联网的电缆。显示器的主要功能大部分依赖着电缆。而电缆仅仅是一种类似HTTP接入你应用的传输部件。所以我们不想将这部分内容(控制器)和应用逻辑糅合在一块。这种做法可以允许任何传输层,比如API或者移动应用程序来接入我们的应用逻辑。
所以,我们再次注入一个存储类,来代替现有将控制器和Eloquent ORM糅合在一块的做法。
契约式设计
首先,我们定义一个接口和相应的实现:
interface UserRepositoryInterface { public function all(); } class DbUserRepository implements UserRepositoryInterface { public function all() { 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和程序交互的中简介,在大型程序中,不能将他们糅合到你的主要逻辑中。
为了巩固上面的知识,我们从一个测试案例开始。首先,模拟一个库并绑定到IoC容器中,然后确保控制器正确的调用了该库:
public function testIndexActionBindsUsersFromRepository() { // Arrange... $repository = Mockery::mock('UserRepositoryInterface'); $repository->shouldReceive('all')->once()->andReturn(array('foo')); App::instance('UserRepositoryInterface',$repository); // Act... $response = $this->action('GET','UserController@getIndex'); // Assert... $this->assertResponSEOk(); $this->assertViewHas('users',array('foo')); }
你在模仿我么
示例中,我们使用了
Mockery
模拟库,它提供了一套表述简洁的方法来模仿你的程序。Mockery可通过Composer进行安装。
继续深入
让我们通过另一个示例来加深对依赖注入的理解。有这样一个场景,我们需要对用户账户中发生的财务变更用进行通知。这里我们定义两个接口,或者叫约定。这些约定将会使需求变更变的很便捷。
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); } }
由于各个类之间已经进行了职责分离,为财务账单(billing)类注入不同的通知程序将会很方便。比如注入短信通知类SmsNotifier
或者邮件通知类EmailNotifier
。我们的账单系统不需要考虑账单通知的实现,只需要根据约定执加载通知即可。凡是遵守约定的账单,都能实现对用户财务变更的通知。此外,不光我们添加方便,我们也可以单独模拟BillingNotifierInterface
接口,来测试账单系统。
善用接口
接口写起来看似添加了很多额外的东西,实际是在加速我们的开发。我们可以在不实现接口的情况下,模拟已开发的接口,来对整个底层逻辑进行测试。
那问题来了,怎么实现依赖注入呢?
$biller = new StripeBiller(new SmsNotifier);
如上,简单吧,这就是依赖注入。只需要将通知器传入到账单系统,而不用担心通知器的使用。微小的改动就能是代码很清晰,这种清晰的职责界定设计,让我们使代码维护简单,当然也方便模拟测试。
那IoC容器是怎么一回事?依赖注入必须要用到他么?这里当然不是!在以后的章节中,我们会看到IoC容器只是为了更好的组织管理依赖注入,但它并非必须。只要遵循本章中介绍的设计原则,你可以在任何项目中实现依赖注入,也不用管是否有这样一个容器可用。
太多JAVA了吧
很多人指责,在PHP中使用接口把代码变的太过冗长,太象“JAVA”。你必须定义一个接口并实现一个类,这得多敲多少代码。
在小而简的项目中,我承认这种批判。这样的项目中,接口是不必要的,因为就你自己用,以后也不会去改。即使架构上牛逼的架构师也会说“需求永远不会确定”,但是需要承认的是,总有tm那么一些地方就是改不着。
接口在大型项目中是非常有用的,这样额外的代码是为了保证未来你代码的灵活性和可测试性。当你快速切换代码实现的时候,一定会闪瞎某些人的狗眼。当然我们的目的是为了让代码能够适应各种操蛋需求的变更。
总之,我们一直提倡“简洁”架构。如果如果你的项目很小,不需要遵循这么多规范,也别不好意思。代码敲的怎么爽怎么来。如果不写接口,也行,以后再说呗,又不是结婚买房,都tm逼的。