1.5 实现依赖注入1.5.1 背景介绍 设计模式中,尤其是结构型模式很多时候解决的就是对象间的依赖关系,变依赖具体为依赖抽象。平时开发中如果发现客户程序依赖某个(或某类)对象,我们常常会对它们进行一次抽象,形成抽象的抽象类、接口,这样客户程序就可以摆脱所依赖的具体类型。 这个过程中有个环节被忽略了——谁来选择客户程序需要的满足抽象类型的具体类型呢?通过后面的介绍你会发现很多时候创建型模式可以比较优雅地解决这个问题。但另一问题出现了,如果您设计的不是具体业务逻辑,而是公共库或框架程序,这时候您是一个“服务方”,不是您调用那些构造类型,而是它们把抽象类型传给您,怎么松散地把加工好的抽象类型传递给客户程序就是另一回事了。 参考Martin Fowler在《Inversion of Control Containers and the Dependency Injection pattern》一文,我们可以采用“依赖注入”的方式将加工好的抽象类型实例“注入”到客户程序中,本书的示例也将大量采用这种方式将各种依赖项“注入”到模式实现的外部——客户程序。下面我们结合一个具体的示例看看为什么需要依赖注入,以及Martin Fowler文中提到的三种经典方式,然后依据C#语言的特质,再扩展出一个基于Attribter方式注入(参考原有的Setter命名,这里将基于Attribute的方法称为Attributer)。 1.5.2 示例情景 客户程序需要一个提供System.DateTime类型当前系统时间的对象,然后根据需要仅仅把其中的年份部分提取出来,因此最初的实现代码如下: C# using System; using System.Diagnostics; namespace MarvellousWorks.PracticalPattern.Concept.DependencyInjection.Example1 { } 后来因为某种原因,发现使用.NET Framework自带的日期类型精度不够,需要提供其他来源的TimeProvider,确保在不同精度要求的功能模块中使用不同的TimeProvider。这样问题集中在TimeProvider的变化会影响客户程序,但其实客户程序仅需要抽象地使用其获取当前时间的方法。为此,增加一个抽象接口,确保客户程序仅依赖这个接口ITimeProvider,由于这部分客户程序仅需要精确到年,因此它可以使用一个名为SystemTimeProvider (:ITimeProvider)的类型。新的实现代码如下: C# using System; namespace { } 这样看上去客户程序后续处理权都依赖于抽象的ITimeProvider,问题似乎解决了?没有,它还要知道具体的SystemTimeProvider。因此,需要增加一个对象,由它选择某种方式把ITimeProvider实例传递给客户程序,这个对象被称为Assembler。新的结构如图1-19所示。 图1-19 增建装配对象后新的依赖关系 其中,Assembler的职责如下: l l l 下面是一个Assembler的示例实现: C# public class Assembler { } 1.5.3 Constructor注入 构造函数注入,顾名思义,就是在构造函数的时候,通过Assembler或其他机制把抽象类型作为参数传递给客户类型。这种方式虽然相对其他方式有些粗糙,而且仅在构造过程中通过“一锤子”方式设置好,但很多时候我们设计上正好就需要这种Read Only的注入方式。其实现方式如下: C# /// 在构造函数中注入 class Client { } Unit Test [TestClass] public class TestClient { } 1.5.4 Setter注入 Setter注入是通过属性赋值的办法解决的,由于Java等很多语言中没有真正的属性,所以Setter注入一般通过一个set()方法实现,C#语言由于本身就有可写属性,所以实现起来更简洁,更像Setter。相比较Constructor方式而言,Setter给了客户类型后续修改的机会,它比较适应于客户类型实例存活时间较长,但Assembler修改抽象类型指定的具体实体类型相对较快的情景;不过也可以由客户程序根据需要动态设置所需的类型。其实现方式如下: C# /// 通过Setter实现注入 class Client { } Unit Test [TestClass] public class TestClient { } 1.5.5 接口注入 接口注入是将包括抽象类型注入的入口以方法的形式定义在一个接口里,如果客户类型需要实现这个注入过程,则实现这个接口,客户类型自己考虑如何把抽象类型“引入”内部。实际上接口注入有很强的侵入性,除了要求客户类型增加需要的Setter或Constructor注入的代码外,还显式地定义了一个新的接口并要求客户类型实现它。除非还有更外层容器使用的要求,或者有完善的配置系统,可以通过反射动态实现接口方式注入,否则笔者并不建议采用接口注入方式。 既然Martin Fowler文中提到了这个实现方式,就给出如下示例: C# /// 定义需要注入ITimeProvider的类型 interface IObjectWithTimeProvider { } /// 通过接口方式注入 class Client : IObjectWithTimeProvider { } Unit Test [TestClass] public class TestClient { } 1.5.6 基于Attribute实现注入——Attributer 如果做个归纳,Martin Fowler之前所介绍的三种模式都是在对象部分进行扩展的,随着语言的发展(.NET从1.0开始,Java从5开始),很多在类元数据层次扩展的机制相继出现,比如C#可以通过Attribute将附加的内容注入到对象上。直观上的客户对象有可能在使用上做出让步以适应这种变化,但这违背了依赖注入的初衷,三个角色(客户对象、Assembler、抽象类型)之中两个不能变,那只好在Assembler上下功夫,谁叫它的职责就是负责组装呢? 为了实现上的简洁,上面三个经典实现方式实际将抽象对象注入到客户类型都是在客户程序中(也就是那三个Unit Test部分)完成的,其实同样可以把这个工作交给Assembler完成;而对于Attribute方式注入,最简单的方式则是直接把实现了抽象类型的Attribute定义在客户类型上,例如: C# (错误的实现情况) 相信您也发现了,这样虽然把客户类型需要的ITimeProvider通过“贴标签”的方式告诉它了,但事实上又把客户程序与SystemTimeAttribute“绑”上了,它们紧密地耦合在一起。参考上面的三个实现,当抽象类型与客户对象耦合的时候我们引入了Assembler,当Attribute方式出现类似的情况时,我们写个AttributeAssembler不就行了么?还不行。设计上要把Attribute设计成一个“通道”,考虑到扩展和通用性,它本身要协助AttributeAssembler完成ITimeProvider的装配,最好还可以同时装载其他抽象类型来修饰客户类型。示例代码如下: C# [AttributeUsage(AttributeTargets.Class, AllowMultiple=true)] sealed class DecoratorAttribute : Attribute { } /// 帮助客户类型和客户程序获取其Attribute定义中需要的抽象类型实例的工具类 static class AttributeHelper { } [Decorator(typeof(ITimeProvider))] // 应用Attribute,定义需要将ITimeProvider通过它注入 class Client { } Unit Test 1.5.7 小结 依赖注入虽然被Martin Fowler称为一个模式,但平时使用中,它更多地作为一项实现技巧出现,开发中很多时候需要借助这项技巧把各个设计模式所加工的成果传递给客户程序。各种实现方式虽然最终目标一致,但在使用特性上有很多区别。 l l l l |
1.5 实现依赖注入1.5.1 背景介绍 设计模式中,尤其是结构型模式很多时候解决的就是对象间的依赖关系,变依赖具体为依赖抽象。平时开发中如果发现客户程序依赖某个(或某类)对象,我们常常会对它们进行一次抽象,形成抽象的抽象类、接口,这样客户程序就可以摆脱所依赖的具体类型。 这个过程中有个环节被忽略了——谁来选择客户程序需要的满足抽象类型的具体类型呢?通过后面的介绍你会发现很多时候创建型模式可以比较优雅地解决这个问题。但另一问题出现了,如果您设计的不是具体业务逻辑,而是公共库或框架程序,这时候您是一个“服务方”,不是您调用那些构造类型,而是它们把抽象类型传给您,怎么松散地把加工好的抽象类型传递给客户程序就是另一回事了。 参考Martin Fowler在《Inversion of Control Containers and the Dependency Injection pattern》一文,我们可以采用“依赖注入”的方式将加工好的抽象类型实例“注入”到客户程序中,本书的示例也将大量采用这种方式将各种依赖项“注入”到模式实现的外部——客户程序。下面我们结合一个具体的示例看看为什么需要依赖注入,以及Martin Fowler文中提到的三种经典方式,然后依据C#语言的特质,再扩展出一个基于Attribter方式注入(参考原有的Setter命名,这里将基于Attribute的方法称为Attributer)。 1.5.2 示例情景 客户程序需要一个提供System.DateTime类型当前系统时间的对象,然后根据需要仅仅把其中的年份部分提取出来,因此最初的实现代码如下: C# using System; using System.Diagnostics; namespace MarvellousWorks.PracticalPattern.Concept.DependencyInjection.Example1 { } 后来因为某种原因,发现使用.NET Framework自带的日期类型精度不够,需要提供其他来源的TimeProvider,确保在不同精度要求的功能模块中使用不同的TimeProvider。这样问题集中在TimeProvider的变化会影响客户程序,但其实客户程序仅需要抽象地使用其获取当前时间的方法。为此,增加一个抽象接口,确保客户程序仅依赖这个接口ITimeProvider,由于这部分客户程序仅需要精确到年,因此它可以使用一个名为SystemTimeProvider (:ITimeProvider)的类型。新的实现代码如下: C# using System; namespace { } 这样看上去客户程序后续处理权都依赖于抽象的ITimeProvider,问题似乎解决了?没有,它还要知道具体的SystemTimeProvider。因此,需要增加一个对象,由它选择某种方式把ITimeProvider实例传递给客户程序,这个对象被称为Assembler。新的结构如图1-19所示。 图1-19 增建装配对象后新的依赖关系 其中,Assembler的职责如下: l l l 下面是一个Assembler的示例实现: C# public class Assembler { } 1.5.3 Constructor注入 构造函数注入,顾名思义,就是在构造函数的时候,通过Assembler或其他机制把抽象类型作为参数传递给客户类型。这种方式虽然相对其他方式有些粗糙,而且仅在构造过程中通过“一锤子”方式设置好,但很多时候我们设计上正好就需要这种Read Only的注入方式。其实现方式如下: C# /// 在构造函数中注入 class Client { } Unit Test [TestClass] public class TestClient { } 1.5.4 Setter注入 Setter注入是通过属性赋值的办法解决的,由于Java等很多语言中没有真正的属性,所以Setter注入一般通过一个set()方法实现,C#语言由于本身就有可写属性,所以实现起来更简洁,更像Setter。相比较Constructor方式而言,Setter给了客户类型后续修改的机会,它比较适应于客户类型实例存活时间较长,但Assembler修改抽象类型指定的具体实体类型相对较快的情景;不过也可以由客户程序根据需要动态设置所需的类型。其实现方式如下: C# /// 通过Setter实现注入 class Client { } Unit Test [TestClass] public class TestClient { } 1.5.5 接口注入 接口注入是将包括抽象类型注入的入口以方法的形式定义在一个接口里,如果客户类型需要实现这个注入过程,则实现这个接口,客户类型自己考虑如何把抽象类型“引入”内部。实际上接口注入有很强的侵入性,除了要求客户类型增加需要的Setter或Constructor注入的代码外,还显式地定义了一个新的接口并要求客户类型实现它。除非还有更外层容器使用的要求,或者有完善的配置系统,可以通过反射动态实现接口方式注入,否则笔者并不建议采用接口注入方式。 既然Martin Fowler文中提到了这个实现方式,就给出如下示例: C# /// 定义需要注入ITimeProvider的类型 interface IObjectWithTimeProvider { } /// 通过接口方式注入 class Client : IObjectWithTimeProvider { } Unit Test [TestClass] public class TestClient { } 1.5.6 基于Attribute实现注入——Attributer 如果做个归纳,Martin Fowler之前所介绍的三种模式都是在对象部分进行扩展的,随着语言的发展(.NET从1.0开始,Java从5开始),很多在类元数据层次扩展的机制相继出现,比如C#可以通过Attribute将附加的内容注入到对象上。直观上的客户对象有可能在使用上做出让步以适应这种变化,但这违背了依赖注入的初衷,三个角色(客户对象、Assembler、抽象类型)之中两个不能变,那只好在Assembler上下功夫,谁叫它的职责就是负责组装呢? 为了实现上的简洁,上面三个经典实现方式实际将抽象对象注入到客户类型都是在客户程序中(也就是那三个Unit Test部分)完成的,其实同样可以把这个工作交给Assembler完成;而对于Attribute方式注入,最简单的方式则是直接把实现了抽象类型的Attribute定义在客户类型上,例如: C# (错误的实现情况) 相信您也发现了,这样虽然把客户类型需要的ITimeProvider通过“贴标签”的方式告诉它了,但事实上又把客户程序与SystemTimeAttribute“绑”上了,它们紧密地耦合在一起。参考上面的三个实现,当抽象类型与客户对象耦合的时候我们引入了Assembler,当Attribute方式出现类似的情况时,我们写个AttributeAssembler不就行了么?还不行。设计上要把Attribute设计成一个“通道”,考虑到扩展和通用性,它本身要协助AttributeAssembler完成ITimeProvider的装配,最好还可以同时装载其他抽象类型来修饰客户类型。示例代码如下: C# [AttributeUsage(AttributeTargets.Class, AllowMultiple=true)] sealed class DecoratorAttribute : Attribute { } /// 帮助客户类型和客户程序获取其Attribute定义中需要的抽象类型实例的工具类 static class AttributeHelper { } [Decorator(typeof(ITimeProvider))] // 应用Attribute,定义需要将ITimeProvider通过它注入 class Client { } Unit Test 1.5.7 小结 依赖注入虽然被Martin Fowler称为一个模式,但平时使用中,它更多地作为一项实现技巧出现,开发中很多时候需要借助这项技巧把各个设计模式所加工的成果传递给客户程序。各种实现方式虽然最终目标一致,但在使用特性上有很多区别。 l l l l |
1.5 实现依赖注入1.5.1 背景介绍 设计模式中,尤其是结构型模式很多时候解决的就是对象间的依赖关系,变依赖具体为依赖抽象。平时开发中如果发现客户程序依赖某个(或某类)对象,我们常常会对它们进行一次抽象,形成抽象的抽象类、接口,这样客户程序就可以摆脱所依赖的具体类型。 这个过程中有个环节被忽略了——谁来选择客户程序需要的满足抽象类型的具体类型呢?通过后面的介绍你会发现很多时候创建型模式可以比较优雅地解决这个问题。但另一问题出现了,如果您设计的不是具体业务逻辑,而是公共库或框架程序,这时候您是一个“服务方”,不是您调用那些构造类型,而是它们把抽象类型传给您,怎么松散地把加工好的抽象类型传递给客户程序就是另一回事了。 参考Martin Fowler在《Inversion of Control Containers and the Dependency Injection pattern》一文,我们可以采用“依赖注入”的方式将加工好的抽象类型实例“注入”到客户程序中,本书的示例也将大量采用这种方式将各种依赖项“注入”到模式实现的外部——客户程序。下面我们结合一个具体的示例看看为什么需要依赖注入,以及Martin Fowler文中提到的三种经典方式,然后依据C#语言的特质,再扩展出一个基于Attribter方式注入(参考原有的Setter命名,这里将基于Attribute的方法称为Attributer)。 1.5.2 示例情景 客户程序需要一个提供System.DateTime类型当前系统时间的对象,然后根据需要仅仅把其中的年份部分提取出来,因此最初的实现代码如下: C# using System; using System.Diagnostics; namespace MarvellousWorks.PracticalPattern.Concept.DependencyInjection.Example1 { } 后来因为某种原因,发现使用.NET Framework自带的日期类型精度不够,需要提供其他来源的TimeProvider,确保在不同精度要求的功能模块中使用不同的TimeProvider。这样问题集中在TimeProvider的变化会影响客户程序,但其实客户程序仅需要抽象地使用其获取当前时间的方法。为此,增加一个抽象接口,确保客户程序仅依赖这个接口ITimeProvider,由于这部分客户程序仅需要精确到年,因此它可以使用一个名为SystemTimeProvider (:ITimeProvider)的类型。新的实现代码如下: C# using System; namespace { } 这样看上去客户程序后续处理权都依赖于抽象的ITimeProvider,问题似乎解决了?没有,它还要知道具体的SystemTimeProvider。因此,需要增加一个对象,由它选择某种方式把ITimeProvider实例传递给客户程序,这个对象被称为Assembler。新的结构如图1-19所示。 图1-19 增建装配对象后新的依赖关系 其中,Assembler的职责如下: l l l 下面是一个Assembler的示例实现: C# public class Assembler { } 1.5.3 Constructor注入 构造函数注入,顾名思义,就是在构造函数的时候,通过Assembler或其他机制把抽象类型作为参数传递给客户类型。这种方式虽然相对其他方式有些粗糙,而且仅在构造过程中通过“一锤子”方式设置好,但很多时候我们设计上正好就需要这种Read Only的注入方式。其实现方式如下: C# /// 在构造函数中注入 class Client { } Unit Test [TestClass] public class TestClient { } 1.5.4 Setter注入 Setter注入是通过属性赋值的办法解决的,由于Java等很多语言中没有真正的属性,所以Setter注入一般通过一个set()方法实现,C#语言由于本身就有可写属性,所以实现起来更简洁,更像Setter。相比较Constructor方式而言,Setter给了客户类型后续修改的机会,它比较适应于客户类型实例存活时间较长,但Assembler修改抽象类型指定的具体实体类型相对较快的情景;不过也可以由客户程序根据需要动态设置所需的类型。其实现方式如下: C# /// 通过Setter实现注入 class Client { } Unit Test [TestClass] public class TestClient { } 1.5.5 接口注入 接口注入是将包括抽象类型注入的入口以方法的形式定义在一个接口里,如果客户类型需要实现这个注入过程,则实现这个接口,客户类型自己考虑如何把抽象类型“引入”内部。实际上接口注入有很强的侵入性,除了要求客户类型增加需要的Setter或Constructor注入的代码外,还显式地定义了一个新的接口并要求客户类型实现它。除非还有更外层容器使用的要求,或者有完善的配置系统,可以通过反射动态实现接口方式注入,否则笔者并不建议采用接口注入方式。 既然Martin Fowler文中提到了这个实现方式,就给出如下示例: C# /// 定义需要注入ITimeProvider的类型 interface IObjectWithTimeProvider { } /// 通过接口方式注入 class Client : IObjectWithTimeProvider { } Unit Test [TestClass] public class TestClient { } 1.5.6 基于Attribute实现注入——Attributer 如果做个归纳,Martin Fowler之前所介绍的三种模式都是在对象部分进行扩展的,随着语言的发展(.NET从1.0开始,Java从5开始),很多在类元数据层次扩展的机制相继出现,比如C#可以通过Attribute将附加的内容注入到对象上。直观上的客户对象有可能在使用上做出让步以适应这种变化,但这违背了依赖注入的初衷,三个角色(客户对象、Assembler、抽象类型)之中两个不能变,那只好在Assembler上下功夫,谁叫它的职责就是负责组装呢? 为了实现上的简洁,上面三个经典实现方式实际将抽象对象注入到客户类型都是在客户程序中(也就是那三个Unit Test部分)完成的,其实同样可以把这个工作交给Assembler完成;而对于Attribute方式注入,最简单的方式则是直接把实现了抽象类型的Attribute定义在客户类型上,例如: C# (错误的实现情况) 相信您也发现了,这样虽然把客户类型需要的ITimeProvider通过“贴标签”的方式告诉它了,但事实上又把客户程序与SystemTimeAttribute“绑”上了,它们紧密地耦合在一起。参考上面的三个实现,当抽象类型与客户对象耦合的时候我们引入了Assembler,当Attribute方式出现类似的情况时,我们写个AttributeAssembler不就行了么?还不行。设计上要把Attribute设计成一个“通道”,考虑到扩展和通用性,它本身要协助AttributeAssembler完成ITimeProvider的装配,最好还可以同时装载其他抽象类型来修饰客户类型。示例代码如下: C# [AttributeUsage(AttributeTargets.Class, AllowMultiple=true)] sealed class DecoratorAttribute : Attribute { } /// 帮助客户类型和客户程序获取其Attribute定义中需要的抽象类型实例的工具类 static class AttributeHelper { } [Decorator(typeof(ITimeProvider))] // 应用Attribute,定义需要将ITimeProvider通过它注入 class Client { } Unit Test 1.5.7 小结 依赖注入虽然被Martin Fowler称为一个模式,但平时使用中,它更多地作为一项实现技巧出现,开发中很多时候需要借助这项技巧把各个设计模式所加工的成果传递给客户程序。各种实现方式虽然最终目标一致,但在使用特性上有很多区别。 l l l l |
原文来自:http://www.escdns.com/html/article/201212/20121215084906527.html
1.5 实现依赖注入1.5.1 背景介绍
设计模式中,尤其是结构型模式很多时候解决的就是对象间的依赖关系,变依赖具体为依赖抽象。平时开发中如果发现客户程序依赖某个(或某类)对象,我们常常会对它们进行一次抽象,形成抽象的抽象类、接口,这样客户程序就可以摆脱所依赖的具体类型。
这个过程中有个环节被忽略了——谁来选择客户程序需要的满足抽象类型的具体类型呢?通过后面的介绍你会发现很多时候创建型模式可以比较优雅地解决这个问题。但另一问题出现了,如果您设计的不是具体业务逻辑,而是公共库或框架程序,这时候您是一个“服务方”,不是您调用那些构造类型,而是它们把抽象类型传给您,怎么松散地把加工好的抽象类型传递给客户程序就是另一回事了。
参考Martin Fowler在《Inversion of Control Containers and the Dependency Injection pattern》一文,我们可以采用“依赖注入”的方式将加工好的抽象类型实例“注入”到客户程序中,本书的示例也将大量采用这种方式将各种依赖项“注入”到模式实现的外部——客户程序。下面我们结合一个具体的示例看看为什么需要依赖注入,以及Martin Fowler文中提到的三种经典方式,然后依据C#语言的特质,再扩展出一个基于Attribter方式注入(参考原有的Setter命名,这里将基于Attribute的方法称为Attributer)。
1.5.2 示例情景
客户程序需要一个提供System.DateTime类型当前系统时间的对象,然后根据需要仅仅把其中的年份部分提取出来,因此最初的实现代码如下:
C#
using System;
using System.Diagnostics;
namespace MarvellousWorks.PracticalPattern.Concept.DependencyInjection.Example1
{
}
后来因为某种原因,发现使用.NET Framework自带的日期类型精度不够,需要提供其他来源的TimeProvider,确保在不同精度要求的功能模块中使用不同的TimeProvider。这样问题集中在TimeProvider的变化会影响客户程序,但其实客户程序仅需要抽象地使用其获取当前时间的方法。为此,增加一个抽象接口,确保客户程序仅依赖这个接口ITimeProvider,由于这部分客户程序仅需要精确到年,因此它可以使用一个名为SystemTimeProvider (:ITimeProvider)的类型。新的实现代码如下:
C#
using System;
namespace
{
}
这样看上去客户程序后续处理权都依赖于抽象的ITimeProvider,问题似乎解决了?没有,它还要知道具体的SystemTimeProvider。因此,需要增加一个对象,由它选择某种方式把ITimeProvider实例传递给客户程序,这个对象被称为Assembler。新的结构如图1-19所示。
图1-19 增建装配对象后新的依赖关系
其中,Assembler的职责如下:
l
l
l
下面是一个Assembler的示例实现:
C#
public class Assembler
{
}
1.5.3 Constructor注入
构造函数注入,顾名思义,就是在构造函数的时候,通过Assembler或其他机制把抽象类型作为参数传递给客户类型。这种方式虽然相对其他方式有些粗糙,而且仅在构造过程中通过“一锤子”方式设置好,但很多时候我们设计上正好就需要这种Read Only的注入方式。其实现方式如下:
C#
/// 在构造函数中注入
class Client
{
}
Unit Test
[TestClass]
public class TestClient
{
}
1.5.4 Setter注入
Setter注入是通过属性赋值的办法解决的,由于Java等很多语言中没有真正的属性,所以Setter注入一般通过一个set()方法实现,C#语言由于本身就有可写属性,所以实现起来更简洁,更像Setter。相比较Constructor方式而言,Setter给了客户类型后续修改的机会,它比较适应于客户类型实例存活时间较长,但Assembler修改抽象类型指定的具体实体类型相对较快的情景;不过也可以由客户程序根据需要动态设置所需的类型。其实现方式如下:
C#
/// 通过Setter实现注入
class Client
{
}
Unit Test
[TestClass]
public class TestClient
{
}
1.5.5 接口注入
接口注入是将包括抽象类型注入的入口以方法的形式定义在一个接口里,如果客户类型需要实现这个注入过程,则实现这个接口,客户类型自己考虑如何把抽象类型“引入”内部。实际上接口注入有很强的侵入性,除了要求客户类型增加需要的Setter或Constructor注入的代码外,还显式地定义了一个新的接口并要求客户类型实现它。除非还有更外层容器使用的要求,或者有完善的配置系统,可以通过反射动态实现接口方式注入,否则笔者并不建议采用接口注入方式。
既然Martin Fowler文中提到了这个实现方式,就给出如下示例:
C#
/// 定义需要注入ITimeProvider的类型
interface IObjectWithTimeProvider
{
}
/// 通过接口方式注入
class Client : IObjectWithTimeProvider
{
}
Unit Test
[TestClass]
public class TestClient
{
}
1.5.6 基于Attribute实现注入——Attributer
如果做个归纳,Martin Fowler之前所介绍的三种模式都是在对象部分进行扩展的,随着语言的发展(.NET从1.0开始,Java从5开始),很多在类元数据层次扩展的机制相继出现,比如C#可以通过Attribute将附加的内容注入到对象上。直观上的客户对象有可能在使用上做出让步以适应这种变化,但这违背了依赖注入的初衷,三个角色(客户对象、Assembler、抽象类型)之中两个不能变,那只好在Assembler上下功夫,谁叫它的职责就是负责组装呢?
为了实现上的简洁,上面三个经典实现方式实际将抽象对象注入到客户类型都是在客户程序中(也就是那三个Unit Test部分)完成的,其实同样可以把这个工作交给Assembler完成;而对于Attribute方式注入,最简单的方式则是直接把实现了抽象类型的Attribute定义在客户类型上,例如:
C#
(错误的实现情况)
相信您也发现了,这样虽然把客户类型需要的ITimeProvider通过“贴标签”的方式告诉它了,但事实上又把客户程序与SystemTimeAttribute“绑”上了,它们紧密地耦合在一起。参考上面的三个实现,当抽象类型与客户对象耦合的时候我们引入了Assembler,当Attribute方式出现类似的情况时,我们写个AttributeAssembler不就行了么?还不行。设计上要把Attribute设计成一个“通道”,考虑到扩展和通用性,它本身要协助AttributeAssembler完成ITimeProvider的装配,最好还可以同时装载其他抽象类型来修饰客户类型。示例代码如下:
C#
[AttributeUsage(AttributeTargets.Class, AllowMultiple=true)]
sealed class DecoratorAttribute : Attribute
{
}
/// 帮助客户类型和客户程序获取其Attribute定义中需要的抽象类型实例的工具类
static class AttributeHelper
{
}
[Decorator(typeof(ITimeProvider))]
// 应用Attribute,定义需要将ITimeProvider通过它注入
class Client
{
}
Unit Test
1.5.7 小结
依赖注入虽然被Martin Fowler称为一个模式,但平时使用中,它更多地作为一项实现技巧出现,开发中很多时候需要借助这项技巧把各个设计模式所加工的成果传递给客户程序。各种实现方式虽然最终目标一致,但在使用特性上有很多区别。
l
l
l
l