依赖倒转原则:代码要依赖于抽象的类,而不要依赖于具体的类;要正对接口或抽象类编程,而不是针对具体类编程。也就是说,在程序代表中传递参数书或在组合聚合关系中,尽量引用层次高的抽象类层,即使用接口和抽象类进行变量类型声明。参数类型声明。方法返回类型声明,以及数据类型的转换等,而不是用具体的类来做这些事情。为了确保该原则的应用,一个具体类应当只实现接口和抽象类中声明过的方法,而不要给出多余的方法,否则将无法调用在子类中增加的新方法。
实现开闭原则的关键是抽象化,并且从抽象化导出具体化实现,如果说开闭原则是面向对象设计的目标的话,那么依赖倒转原则就是面向对象设计的主要手段。有了抽象层,可以使得系统具有很好的灵活性,在程序中尽量使用抽象层进行编程,而将具体类写在配置文件中。
下面简单介绍下依赖倒转原则中经常提到的2个概念:类之间的耦合和依赖注入。
1.类之间的耦合
在oo中,2个类通常通常可以发生3中不同的耦合关系(依赖关系):
1.零耦合关系:如果两个类之间没有耦合关系,称之为零耦合。
2.具体耦合关系:具体耦合发生在2个具体类之间,由一个类对另一个具体类实例的直接引用产生。
3.抽象耦合关系:发生在一个具体类和一个抽象类之间,也可以发生在2个抽象类之间,是2个发生关系的类之间存有最大的灵活性。由于在抽象耦合中至少有一端是抽象的,因此可以通过不同的具体事项来进行扩展。
依赖倒转原则要求客户端依赖于抽象耦合,以抽象方式耦合是依赖倒转的关键。
2.依赖注入
控制反转(Inversion of Control,英文缩写为IoC)是一个重要的面向对象编程的法则来削减计算机程序的耦合问题。 控制反转还有一个名字叫做依赖注入(Dependency Injection)。简称DI。
Martin Fowler在它的这篇文章(中文翻译版http://wenku.baidu.com/view/0fa885e8b8f67c1cfad6b841.html)中提出了这个概念,对象与对象之间的依赖关系式可以传递的,通过依赖传递,在一个对象中可以调用另一个对象的方法,在传递时要做好依赖抽象。简单来说,依赖注入就是将一个类的对象传入另一个类,注入时应该尽量注入父类对象,而在程序运行时再通过子类对象来覆盖父类对象.
依赖在哪里
老马举了一个小例子,是开发一个电影列举器(MovieList),这个电影列举器需要使用一个电影查找器(MovieFinder)提供的服务,伪码如下:
public interface MovieFinder { 3 ArrayList findAll(); 4} 5 6/*服务的消费者*/ 7class MovieLister 8{ 9 public Movie[] moviesDirectedBy(String arg) { 10 List allMovies = finder.findAll(); 11 for (Iterator it = allMovies.iterator(); it.hasNext();) { 12 Movie movie = (Movie) it.next(); 13 if (!movie.getDirector().equals(arg)) it.remove(); 14 } 15 return (Movie[]) allMovies.toArray(new Movie[allMovies.size()]); 16 } 17 18 /*消费者内部包含一个将指向具体服务类型的实体对象*/ 19 private MovieFinder finder; 20 /*消费者需要在某一个时刻去实例化具体的服务。这是我们要解耦的关键所在, 21 *因为这样的处理方式造成了服务消费者和服务提供者的强耦合关系(这种耦合是在编译期就确定下来的)。 22 **/ 23 public MovieLister() { 24 finder = new ColonDelimitedMovieFinder("movies1.txt"); 25 } 26}
从上面代码的注释中可以看到,MovieLister和ColonDelimitedMovieFinder(这可以使任意一个实现了MovieFinder接口的类型)之间存在强耦合关系,如下图所示:
这使得MovieList很难作为一个成熟的组件去发布,因为在不同的应用环境中(包括同一套软件系统被不同用户使用的时候),它所要依赖的电影查找器可能是千差万别的。所以,为了能实现真正的基于组件的开发,必须有一种机制能同时满足下面两个要求:
(1)解除MovieList对具体MoveFinder类型的强依赖(编译期依赖)。
(2)在运行的时候为MovieList提供正确的MovieFinder类型的实例。
换句话说,就是在运行的时候才产生MovieList和MovieFinder之间的依赖关系(把这种依赖关系在一个合适的时候“注入”运行时),这恐怕就是Dependency Injection这个术语的由来。再换句话说,我们提到过解除强依赖,这并不是说MovieList和MovieFinder之间的依赖关系不存在了,事实上MovieList无论如何也需要某类MovieFinder提供的服务,我们只是把这种依赖的建立时间推后了,从编译器推迟到运行时了。
依赖关系在OO程序中是广泛存在的,只要A类型中用到了B类型实例,A就依赖于B。前面笔者谈到的内容是把概念抽象到了服务使用者和服务提供者的角度,这也符合现在SOA的设计思路。从另一种抽象方式上来看,可以把MovieList看成我们要构建的主系统,而MovieFinder是系统中的plugin,主系统并不强依赖于任何一个插件,但一旦插件被加载,主系统就应该可以准确调用适当插件的功能。
其实不管是面向服务的编程模式,还是基于插件的框架式编程,为了实现松耦合(服务调用者和提供者之间的or框架和插件之间的),都需要在必要的位置实现面向接口编程,在此基础之上,还应该有一种方便的机制实现具体类型之间的运行时绑定,这就是DI所要解决的问题。
和上面的图1对应的是,如果我们的系统实现了依赖注入,组件间的依赖关系就变成了图2:
说白了,就是要提供一个容器,由容器来完成(1)具体ServiceProvider的创建(2)ServiceUser和ServiceProvider的运行时绑定。下面我们就依次来看一下三种典型的依赖注入方式的实现。特别要说明的是,要理解依赖注入的机制,关键是理解容器的实现方式。本文后面给出的容器参考实现,均为黄忠成老师的代码,笔者仅在其中加上了一些关键注释而已。
依赖注入有以下3中方式:
1.构造注入(constructor injection)
public interface AbstractBook { public void view(); } public interface AbstractReader { public void read(AbstractBook book); } public class concreteBook implements AbstractBook { public void view() { ....... } } public class ConcreteReader implements AbstractReader { private AbstractBook book; public ConcreteReader(AbstractBook book) { this.book=book; } public void read() { book.view(); } }
通过构造函数注入实例变量,代码如下:
我们可以看到,在整个依赖注入的数据结构中,涉及到的重要的类型就是ServiceUser, ServiceProvider和Assembler三者,而这里所说的构造器,指的是ServiceUser的构造器。也就是说,在构造ServiceUser实例的时候,才把真正的ServiceProvider传给他:
class MovieLister 2{ 3 //其他内容,省略 4 5 public MovieLister(MovieFinder finder) 6 { 7 this.finder = finder; 8 } 9}
接下来我们看看Assembler应该如何构建:
private MutablePicoContainer configureContainer() { 2 MutablePicoContainer pico = new DefaultPicoContainer(); 3 4 //下面就是把ServiceProvider和ServiceUser都放入容器的过程,以后就由容器来提供ServiceUser的已完成依赖注入实例, 5 //其中用到的实例参数和类型参数一般是从配置档中读取的,这里是个简单的写法。 6 //所有的依赖注入方法都会有类似的容器初始化过程,本文在后面的小节中就不再重复这一段代码了。 7 Parameter[] finderParams = {new ConstantParameter("movies1.txt")}; 8 pico.registerComponentImplementation(MovieFinder.class, ColonMovieFinder.class, finderParams); 9 pico.registerComponentImplementation(MovieLister.class); 10 //至此,容器里面装入了两个类型,其中没给出构造参数的那一个(MovieLister)将依靠其在构造器中定义的传入参数类型,在容器中 11 //进行查找,找到一个类型匹配项即可进行构造初始化。 12 return pico; 13}
需要在强调一下的是,依赖并未消失,只是延后到了容器被构建的时刻。所以正如图2中您已经看到的,容器本身(更准确的说,是一个容器运行实例的构建过程)对ServiceUser和ServiceProvoder都是存在依赖关系的。所以,在这样的体系结构里,ServiceUser、ServiceProvider和容器都是稳定的,互相之间也没有任何依赖关系;所有的依赖关系、所有的变化都被封装进了容器实例的创建过程里,符合我们对服务应用的理解。而且,在实际开发中我们一般会采用配置文件来辅助容器实例的创建,将这种变化性排斥到编译期之外。
即使还没给出后面的代码,你也一定猜得到,这个container类一定有一个GetInstance(Type t)这样的方法,这个方法会为我们返回一个已经注入完毕的MovieLister。 一个简单的应用如下:
public void testWithPico()
2{
3 MutablePicoContainer pico = configureContainer();
4 MovieLister lister = (MovieLister) pico.getComponentInstance(MovieLister.class);
5 Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
6 assertEquals("Once Upon a Time in the West", movies[0].getTitle());
7}
上面最关键的就是对pico.getComponentInstance的调用。Assembler会在这个时候调用MovieLister的构造器,构造器的参数就是当时通过pico.registerComponentImplementation(MovieFinder.class, ColonMovieFinder.class, finderParams)设置进去的实际的ServiceProvider--ColonMovieFinder。下面请看这个容器的参考代码:
public static class Container 2{ 3 private static Dictionary<Type, object> _stores = null; 4 5 private static Dictionary<Type, object> Stores 6 { 7 get 8 { 9 if (_stores == null) 10 _stores = new Dictionary<Type, object>(); 11 return _stores; 12 } 13 } 14 15 private static Dictionary<string, object> CreateConstructorParameter(Type targetType) 16 { 17 Dictionary<string, object> paramArray = new Dictionary<string, object>(); 18 19 ConstructorInfo[] cis = targetType.GetConstructors(); 20 if (cis.Length > 1) 21 throw new Exception("target object has more then one constructor,container can't peek one for you."); 22 23 foreach (ParameterInfo pi in cis[0].GetParameters()) 24 { 25 if (Stores.ContainsKey(pi.ParameterType)) 26 paramArray.Add(pi.Name, GetInstance(pi.ParameterType)); 27 } 28 return paramArray; 29 } 30 31 public static object GetInstance(Type t) 32 { 33 if (Stores.ContainsKey(t)) 34 { 35 ConstructorInfo[] cis = t.GetConstructors(); 36 if (cis.Length != 0) 37 { 38 Dictionary<string, object> paramArray = CreateConstructorParameter(t); 39 List<object> cArray = new List<object>(); 40 foreach (ParameterInfo pi in cis[0].GetParameters()) 41 { 42 if (paramArray.ContainsKey(pi.Name)) 43 cArray.Add(paramArray[pi.Name]); 44 else 45 cArray.Add(null); 46 } 47 //在这里完成了对构造函数的调用,而构造函数的传入参数是通过在容器中查找匹配类型的实例得到的, 48 //所以被称为构造器注入。 49 return cis[0].Invoke(cArray.ToArray()); 50 } 51 else if (Stores[t] != null) 52 return Stores[t]; 53 else 54 return Activator.CreateInstance(t, false); 55 } 56 return Activator.CreateInstance(t, false); 57 } 58 59 //向容器中加入ServiceProvider的实例 60 public static void RegisterImplement(Type t, object impl) 61 { 62 if (Stores.ContainsKey(t)) 63 Stores[t] = impl; 64 else 65 Stores.Add(t, impl); 66 } 67 68 //向容器中加入ServiceUser的类型,类型的构造器将在容器中被调用 69 public static void RegisterImplement(Type t) 70 { 71 if (!Stores.ContainsKey(t)) 72 Stores.Add(t, null); 73 } 74}
2 Setter Injection(设值注入)
设值注入是通过setter方法注入实例变量,代码如下:
public interface AbstractBook { public void view(); } public interface AbstractReader { public void seBook(AbstarctBook book); public void read(); } public class ConcreteReader implements AbstractReader { private AbstractBook book; public void setBook(AbstractBook book) { this.book=book; } public void read() { book.view(); } }
这种注入方式和构造注入实在很类似,唯一的区别就是前者在构造函数的调用过程中进行注入,而它是通过给属性赋值来进行注入。无怪乎PicoContainer和Spring都是同时支持这两种注入方式。Spring对通过XML进行配置有比较好的支持,也使得Spring中更常使用设值注入的方式.
<beans>
2 <bean id="MovieLister" class="spring.MovieLister">
3 <property name="finder">
4 <ref local="MovieFinder"/>
5 </property>
6 </bean>
7 <bean id="MovieFinder" class="spring.ColonMovieFinder">
8 <property name="filename">
9 <value>movies1.txt</value>
10 </property>
11 </bean>
12</beans>
下面也给出支持设值注入的容器参考实现,大家可以和构造器注入的容器对照起来看,里面的差别很小,主要的差别就在于,在获取对象实例(GetInstance)的时候,前者是通过反射得到待创建类型的构造器信息,然后根据构造器传入参数的类型在容器中进行查找,并构造出合适的实例;而后者是通过反射得到待创建类型的所有属性,然后根据属性的类型在容器中查找相应类型的实例。
public static class Container 2{ 3 private static Dictionary<Type, object> _stores = null; 4 5 private static Dictionary<Type, object> Stores 6 { 7 get 8 { 9 if (_stores == null) 10 _stores = new Dictionary<Type, object>(); 11 return _stores; 12 } 13 } 14 15 public static object GetInstance(Type t) 16 { 17 if (Stores.ContainsKey(t)) 18 { 19 if (Stores[t] == null) 20 { 21 object target = Activator.CreateInstance(t, false); 22 foreach (PropertyDescriptor pd in TypeDescriptor.GetProperties(target)) 23 { 24 if (Stores.ContainsKey(pd.PropertyType)) 25 //在此处为待创建实例设置属性,完成依赖注入过程。属性值是在容器中通过类型匹配的方式查找出来的。 26 pd.SetValue(target, GetInstance(pd.PropertyType)); 27 } 28 return target; 29 } 30 else 31 return Stores[t]; 32 } 33 return Activator.CreateInstance(t, false); 34 } 35 36 public static void RegisterImplement(Type t, object impl) 37 { 38 if (Stores.ContainsKey(t)) 39 Stores[t] = impl; 40 else 41 Stores.Add(t, impl); 42 } 43 44 public static void RegisterImplement(Type t) 45 { 46 if (!Stores.ContainsKey(t)) 47 Stores.Add(t, null); 48 } 49}
3Interface Injection (接口注入)
是通过接口方法注入实例变量,代码如下:
public interface AbstractBook { public void view(); } public interface AbstractReader { public void read(AbstractBook book); } public class concreteBook implements AbstractBook { public void view() { ....... } } public class ConcreteReader implements AbstractReader { public void read(AbstractBook book) { book.view(); } }
这是笔者认为最不够优雅的一种依赖注入方式。要实现接口注入,首先ServiceProvider要给出一个接口定义:
public interface InjectFinder {
2 void injectFinder(MovieFinder finder);
3}
接下来,ServiceUser必须实现这个接口:
2{
3 public void injectFinder(MovieFinder finder) {
4 this.finder = finder;
5 }
6}
容器所要做的,就是根据接口定义调用其中的inject方法完成注入过程,这里就不在赘述了,总的原理和上面两种依赖注入模式没有太多区别。
我们可以看到,构造器注入和设值注入在类里面都定义了一个变量,而接口注入没有,只是在方法的参数里出现。
除了DI,还有Service Locator
上面提到的依赖注入只是消除ServiceUser和ServiceProvider之间的依赖关系的一种方法,还有另一种方法:服务定位器(Service Locator)。也就是说,由ServiceLocator来专门负责提供具体的ServiceProvider。当然,这样的话ServiceUser不仅要依赖于服务的接口,还依赖于ServiceContract。仍然是最早提到过的电影列举器的例子,如果使用Service Locator来解除依赖的话,整个依赖关系应当如下图所示:
图3
用起来也很简单,在一个适当的位置(比如在一组相关服务即将被调用之前)对ServiceLocator进行初始化,用到的时候就直接用ServiceLocator返回ServiceProvider实例:
2ServiceLocator locator = new ServiceLocator();
3locator.loadService("MovieFinder", new ColonMovieFinder("movies1.txt"));
4ServiceLocator.load(locator);
5//服务定义器的使用
6//其实这个使用方式体现了服务定位器和依赖注入模式的最大差别:ServiceUser需要显示的调用ServiceLocator,从而获取自己需要的服务对象;
7//而依赖注入则是隐式的由容器完成了这一切。
8MovieFinder finder = (MovieFinder) ServiceLocator.getService("MovieFinder");
9
正因为上面提到过的ServiceUser对ServiceLocator的依赖性,从提高模块的独立性(比如说,你可能把你构造的ServiceUser或者ServiceProvider给第三方使用)上来说,依赖注入可能更好一些,这恐怕也是为什么大多数的IOC框架都选用了DI的原因。ServiceLocator最大的优点可能在于实现起来非常简单,如果您开发的应用没有复杂到需要采用一个IOC框架的程度,也许您可以试着采用它。
3.广义的服务
文中很多地方提到服务使用者(ServiceUser)和服务提供者(ServiceProvider)的概念,这里的“服务”是一种非常广义的概念,在语法层面就是指最普通的依赖关系(类型A中有一个B类型的变量,则A依赖于B)。如果您把服务理解为WCF或者Web Service中的那种服务概念,您会发现上面所说的所有技术手段都是没有意义的。以WCF而论,其客户端和服务器端本就是依赖于Contract的松耦合关系,其实这也从另一个角度说明了SOA应用的优势所在。
参考
http://www.cnblogs.com/xingyukun/archive/2007/10/20/931331.html
Object Builder Application Block