注射与反转
> Dependency Injection是一种Inversion of Control技术,用于通过Dependency Injection Design Pattern向类提供对象(‘dependencies’).通常通过以下方法之一传递依赖项:
>构造函数
>公共财产或领域
>公共二传手
>依赖倒置原则(DIP)是一个软件设计指南,归结为关于de-coupling a class from its concrete dependencies的两个建议:
>’高级模块不应该依赖于低级模块.两者都应该取决于抽象.
>’抽象不应该依赖于细节.细节应该取决于抽象.
依赖注入
首先,依赖注入与控制反转(IoC)不同.具体来说,IoC是不同解决方案的保护伞,包括依赖注入,服务定位器,工厂模式,策略模式等.
考虑到依赖注入而设计的类可能如下所示:
// Dependency Injection Example... class Foo { // Constructor uses DI to obtain the Meow and Woof dependencies constructor(fred: Meow,barney: Woof) { this.fred = fred; this.barney = barney; } }
在这个例子中,Meow和Woof都是通过Foo构造函数注入的依赖项.
另一方面,没有依赖注入设计的Foo类可能只是创建Meow和Woof实例本身,或者可能使用某种服务定位器/工厂:
// Example without Dependency Injection... class Foo { constructor() { // a 'Meow' instance is created within the Foo constructor this.fred = new Meow(); // a service locator gets a 'WoofFactory' which in-turn // is responsible for creating a 'Woof' instance. // This demonstrates IoC but not Dependency Injection. var factory = TheServiceLocator.GetWoofFactory(); this.barney = factory.CreateWoof(); } }
因此,依赖注入只是意味着一个类已经推迟了获取或提供自己的依赖关系的责任;相反,责任在于任何想要创建实例的东西. (通常是IoC容器)
依赖倒置
依赖性反转通常是通过防止那些具有彼此直接引用的类来解除具体类的分离.
注意:依赖性反转通常在静态类型编程语言(如C#或Java)中更明确,因为这些语言对变量名执行严格的类型检查.另一方面,依赖性反转已经在动态语言(如Python或JavaScript)中被动地可用,因为这些语言中的变量没有任何特定的类型限制.
考虑使用静态类型语言的场景,其中类需要能够从应用程序的数据库中读取记录:
class Foo { reader: sqlRecordReader; constructor(sqlReader: sqlRecordReader) { this.reader = sqlReader; } doSomething() { var records = this.reader.readAll(); // etc. } }
在上面的例子中,类Foo对sqlRecordReader有很强的依赖性,但它唯一关心的是存在一个名为readAll()的方法,它返回一些记录.
考虑将sql数据库查询分成单独的微服务的情况; Foo类需要从删除服务中读取记录.或者,Foo单元测试需要从内存存储或平面文件中读取数据的情况.
如果顾名思义,sqlRecordReader包含数据库和sql逻辑,那么任何迁移到微服务都需要改变Foo类.
依赖性反转指南建议应该用仅提供readAll()方法的抽象替换sqlRecordReader.即:
interface IRecordReader { Records[] getAll(); } class Foo { reader: IRecordReader; constructor(reader: IRecordReader) { this.reader = reader; } }
根据DIP,IRecordReader是一个抽象,并且强制Foo依赖于IRecordReader而不是sqlRecordReader满足DIP指南.
为什么DIP指南很有用
关键字是指南 – 依赖性反转将间接添加到程序的设计中.添加任何类型的间接的明显缺点是复杂性(即,人类理解正在发生的事情所需的认知“负荷”)增加.
在许多情况下,间接可以使代码更容易维护(修复错误,添加增强功能)但是:
在最后一个例子中,Foo可能会收到一个sqlRecordReader,或者一个SoapRecordReader,或者一个FileRecordReader,或者甚至可能用于单元测试一个MockRecordReader – 重点是它不知道或关心IRecordReader的不同可能实现 – 提供当然,这些实现完全符合Liskov Substitution Principle.
此外,它避免了潜在的肮脏场景,即急于开始工作的开发人员可能会考虑通过从基类sqlRecordReader继承SoapRecordReader或FileRecordReader来“捏造”Liskov原则.
更糟糕的是,缺乏经验的开发人员甚至可能会更改sqlRecordReader本身,以便该类不仅具有sql逻辑,还具有SOAP端点,文件系统以及可能需要的任何其他内容. (这种事情在现实世界中经常发生 – 特别是在维护不良的代码中,并且几乎总是Code Smell.)