7. 依赖注入
来自 Wikipedia:
依赖注入是这样一种软件设计模式,它允许你避免硬编码依赖项,并可以在运行或者编译时修改这些依赖项。
这个引用让这个概念比它实际更难理解了。
依赖注入通过构造器注入、调用方法、设置属性来提供组件。就是这么简单。
基础概念
我们可以通过一个简单、朴实的例子来展示这个概念。
这里我们有一个 Database
类, 它需要一个适配器来连接数据库。我们在这个类的构造方法中通过硬编码实例化了适配器。这样使得测试变得困难,同样意味着 Database
这个类和它的适配器强耦合。
<?php
namespace Database;
class Database
{
protected $adapter;
public function __construct()
{
$this->adapter = new MySqlAdapter;
}
}
class MysqlAdapter {}
这段代码可以改成使用依赖注入来摆脱这种依赖。
<?php
namespace Database;
class Database
{
protected $adapter;
public function __construct(MySqlAdapter $adapter)
{
$this->adapter = $adapter;
}
}
class MysqlAdapter {}
现在我们可以传递给 Database
它的依赖项,而不是让它自己创建。我们甚至可以创建一个方法,用参数的形式将依赖项传递给它,或者设置 $adapter
为公共属性,这样我们可以直接设置它。
复杂的问题
假如你了解过依赖注入,那么你大概率看到过 “控制反转” 或者 “依赖反转原则”。 这些复杂的问题都是通过依赖注入解决的。
控制反转
控制反转正如字面意思,将对一个系统的组织控制反转过来,完全和我们的对象剥离开。
就依赖注入而言,这意味着在系统其它地方控制和实例化依赖项来减少耦合。
多年来,PHP框架一直在致力于实现依赖反转。然而,问题变成了,我们需要反转哪一部分的控制,以及在哪里做控制?例如,MVC框架通常会提供一个超级对象或者基类控制器,由其他控制器继承并获得访问权限。这种控制反转,其实是简单移开了依赖,而不是解耦依赖。
依赖注入允许我们优雅地注入依赖当且仅当我们需要它们时,而且不需要硬编码这些依赖项。
S.O.L.I.D.
单一功能原则
单一功能原则是关于动作和高层次架构的。它被描述为“一个类应当只有一个原因被改变”。这意味着每个类应当 仅 负责软件提供的一个功能。这样做的最大好处是提升了代码的 可复用性 。通过把我们的类设计成只做一件事,我们可以使用(或者复用)它在其他程序中而不需要修改它。
开闭原则
开闭原则是关于类的设计和功能扩展的。它被描述为“软件实例(类、模块、函数等等)应该对扩展开放,但是拒绝修改”。这意味着我们应该这样设计我们的模块、类或者函数,当新功能被提出时,我们不应该修改已有的代码,而是写新的代码给已有的代码使用。实际就是说,我们应该写那些实现并遵守了接口的类,然后引用接口而不是特定的类。
这样做的最大好处是我们可以容易的扩展我们的代码来支持新的功能而不需要修改已经存在的代码。这意味着我们可以减少提问回答的时间,而且应用的负面影响的风险也实质上降低了。我们可以更快地,更自信地部署我们的代码。
里氏替换原则
里氏替换原则是关于子类和继承的。它被描述为“子类永远不应该破坏父类的定义”,或者用 Robert C. Martin 的话说,“子类必须可以被父类替代”
例如,我们有个 FileInterface
接口定义了 embed()
方法,并且有 Audio
和 Video
两个类都实现了 FileInterface
接口,那么我们可以预期 embed()
总是按我们期待的那样做。假如我们稍后又创建了实现了 FileInterface
接口的一个 PDF
和 一个 Gist
类,我们也会明白 embed()
方法是干什么的。这么做最大的好处是我们可以建立灵活且易配置的程序,因为当我们改变了一个对象的类型(比如 FileInterface
)为另一个,我们不需要修改程序里的其它任何地方。
接口隔离原则
接口隔离原则(ISP)是关于 业务逻辑到客户 的沟通的。它被描述为“客户不应被迫使用对其而言无用的方法或功能”。这意味着不应该用一个单一整体的接口适应所有符合的类,而是应该提供一系列更小的、概念明确的接口给符合的类实现其中的一个或多个。
例如,Car
或 Bus
类对 steeringWheel()
方法感兴趣,而 Motorcycle
或 Tricycle
类则对其不感兴趣。相反,Motorcycle
或 Tricycle
类对 handlebars()
方法感兴趣,而 Car
或 Bus
类则对其不感兴趣。没必要让不同类型的车同时实现对 steeringWheel()
和 handlebars()
的支持,我们应该分离源接口。
依赖倒置原则
依赖倒置原则用于解除离散的类之间的硬链接,通过传递不同的类来提供不同的新功能。它倡导应该 “依赖抽象,而不是依赖具体实现。”。简单地说,这意味着依赖关系应该是接口/约定或抽象类,而不是具体的实现。我们可以很简单地重构上述示例以遵循这一原则。
<?php
namespace Database;
class Database
{
protected $adapter;
public function __construct(AdapterInterface $adapter)
{
$this->adapter = $adapter;
}
}
interface AdapterInterface {}
class MysqlAdapter implements AdapterInterface {}
现在 Database
类依赖于接口,相比依赖于具体实现有更多的优势。
假设我们现在进行团队开发,一位同事负责设计开发适配器。在第一个例子中,我们必须等待适配器完成后,才能正确的模拟它进行单元测试。现在由于依赖的是一个接口/约定,我们能轻松地模拟接口测试,因为我们知道同事会基于约定实现那个适配器
这种方法一个更大的好处是,代码的扩展性变得更高。如果一年后我们决定迁移到另一种数据库,我们只需要编写一个实现相应接口的适配器并且注入进去。由于适配器遵循接口的约定,我们无须再进行额外的重构。
容器
你应该明白的第一件事是,依赖注入容器和依赖注入是不相同的概念。容器是帮助我们更方便地实现依赖注入的工具,但是它们通常被误用来实现反模式设计:服务定位(Service Location)。把依赖注入容器作为服务定位器( Service Locator )注入进类中,对容器的依赖性比你原想要替换的依赖性更强,而且还会让你的代码变得更不透明,最终更难进行测试。
大多数现代的框架都有自己的依赖注入容器,允许你通过配置将依赖绑定在一起。这实际上意味着,你可以写出和框架层同样干净、解耦的应用程序代码。
延伸阅读
本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。