控制反转(IoC)
大量使用工厂模式引起的问题:
Client 对象需要使用 Service1 的 execute( ) 方法完成特定功能,而 Service1 的实现 Service1Impe类又依赖于 Service2的实现类 Service2Impl,为了减少依赖,我们为 Service1 和 Service2对象的实例化分别提供工厂方法类的实现。
public interface Service1 { void execute(); } public class Service1Impl implements Service1 { private Service2 service2; public Service1Impl(Service2 service2) { this.service2 = service2; } @Override public void execute() { System.out.println("Service1 is doing something."); service2.execute(); } } public interface Service2 { void execute(); } public class Service2Impl implements Service2 { @Override public void execute() { System.out.println("Service2 is doing something."); } } // 工厂类的实现 public class Service2Factory { public static Service2 getService2Instance() { return new Service2Impl(); } } public class Service1Factory { public static Service1 getService1Instance() { Service2 service2 = Service2Factory.getService2Instance(); return new Service1Impl(service2); } } // 测试代码 public class Client { public void doSomething() { Service1 service = Service1Factory.getService1Instance(); service.execute(); } } -- Service1 is doing something. -- Service2 is doing something.
大规模地使用工厂方法模式,会引起诸多问题:
- 工厂类泛滥:在实际应用中,经常出现几百个像这样的服务类,如果使用工厂方法模式的话,就可能出现几百个这样的工厂类。因为不知道将来会产生多少子类扩展系统,所以也不能使用静态工厂模式。
- 依赖关系复杂:往往这几百个 Service对象之间存在复杂的依赖关系,工厂类的装配逻辑也随之变得十分复杂。增加了装配这些服务对象的精力,而不能专注于业务功能的开发。
- 不易进行单元测试。
Inversion of Control(控制反转):
好莱坞原则:不要找我们,我们会找你。
我们经常把控制逻辑写在其他地方而非客户化的代码里,这样就可以更专注与客户化的逻辑,即外部逻辑负责调用客户化的逻辑。在软件开发领域,取名叫:控制反转(IoC)。
应用实例:
-
模板方法模式——父类的模板方法控制子类的方法调用
-
回调的方法也是控制反转的很好应用。
-
在 Java标准库里,查找(binarySearch) 和 排序(sort)这两个方法,他们在执行过程中会调用对象的 compareTo( ) 方法(如果这些对象实现了 java.lang.Comparable 接口的话),或者调用我们所传递的回调接口 java.util.Comparator 对象的 compare( ) 方法来比较大小,最终完成查找/排序操作。
-
框架:框架抽象了通用流程,我们可以通过框架提供的规范(如子类化抽象类或者实现相关的接口,实现相关的交互协议和接口等)就可以把客户化的代码植入流程中,完成各种定制的需求。
框架和工具库(Library)的区别是:如果框架没有实现控制反转,那么框架就会退化为工具库。也就是说,使用工具库,必须撰写调用它们的逻辑,每次调用它们都会完成相应的工作,并把控制权返回给调用者;而框架不需要客户程序撰写控制调用的逻辑,由框架专门负责。
IoC 和 DI:
任何容器/框架都实现了控制反转,它们所说的控制反转指的是对服务/对象/组件的实例化和查找实现了控制反转,这只是控制反转的一种而已。
实现控制反转主要有两种形式:
- Service Locator(服务定位器)
- Dependency Injection(依赖注入,简写为 DI)
Service Locator(服务定位器):
服务定位器封装了查找逻辑,隐藏了服务/对象/组件之间的依赖关系,为它们提供了一个全局的入口。客户对象只要依赖于它就能获取想要的组件/服务/对象。
工厂方法模式和服务定位器的区别是:服务定位器为整个应用组件/服务/对象的获取提供了单一的入口,而一个工厂只提供特定类型的实例化,如果一个工厂能提供所有组件/服务/对象的装配和实例化,那它就被进化为服务定位器。
使用服务定位器时,容器/框架侵入了代码,降低了代码移植性,单元测试也相对比较麻烦。
简单的服务定位器:
//简单的服务定位器 public class ServiceLocator { private static ServiceLocator locator; // 创建 HashMap<String, Object>对象来持有这些服务对象 private Map<String, Object> services = new HashMap<String, Object>(); // 初始化容器 public static void configure() { load(new ServiceLocator()); // Service1、Service2的对象其实是按照单例创建的 Service2 service2 = new Service2Impl(); locator.services.put("service2", service2); Service1 service1 = new Service1Impl(service2); locator.services.put("service1", service1); } private static void load(ServiceLocator serviceLocator) { locator = serviceLocator; } public static Object lookup(String serviceName) { return locator.services.get(serviceName); } public static void registerService(String name, Object service) { locator.services.put(name, service); } }
Client类的实现:
public class Client { public void doSomething() { Service1 service1 = (Service1) ServiceLocator.lookup("service1"); service1.execute(); } // 测试代码 public static void main(String[] args) { // 初始化容器 ServiceLocator.configure(); new Client().doSomething(); } }
Dependency Injection(依赖注入):
外部程序把服务对象通过某种方式注入到客户对象供其使用的方法称之为依赖注入。
根据注入方式的不同,把依赖注入分为 6类:
- Setter 注入:外部程序通过调用 setting方法为客户对象注入所依赖的对象。(Spring框架是使用反射机制调用 setting方法注入依赖的对象)
- Constructor 注入:通过带参数的构造方法注入依赖对象。
- Annotation 注入:把实例化信息和对象之间的依赖关系信息使用 Annotation注解。
- Interface 注入:客户程序通过实现容器/框架所规范的某些特殊接口,在为客户对象返回这些依赖的对象之前,容器回调这些接口的方法,注入所依赖的服务对象。如:Struts2中的 Action实现了接口 ServletRequesetAware、SessionAware等,只要 Action类实现这些接口,Struts框架就会调用这些接口的相应方法,注入 HttpServletRequest、HttpSession对象。
- Parameter 注入 :外部程序可以通过函数参数,给客户程序注入所依赖的服务对象。
- 其它形式的注入
Setter 和 Constructor 是最常见的注入方式,代码往往不会受到容器/框架的入侵,可以在多种轻量级容器上移植,而其它方式或多或少都受到容器/框架代码的入侵。
Interface 注入:(这种方式不够灵活,容器必须预先定义一些接口实现注入,适合实现少数特定类型的对象注入)
// ServiceAware 接口 public interface ServiceAware { void injectService(Service service); } // Service 接口及其实现类 public interface Service { void excute(); } public class ServiceImpl implements Service { @Override public void excute() { System.out.println("Service is doing something..."); } } // 创建简单的接口注入容器 public class InterfaceInjector { private static InterfaceInjector injector; private Map<Class, Object> services = new HashMap<Class, Object>(); // 安装容器 public static void configure() { load(new InterfaceInjector()); } public static <T> T getInstance(Class<T> clazz) { return injector.loadService(clazz); } private static void load(InterfaceInjector container) { InterfaceInjector.injector = container; } private <T> T loadService(Class<T> clazz) { Object service = injector.services.get(clazz); if (service != null) { return (T) service; } try { // 创建 clazz类实例 service = clazz.newInstance(); // 通过接口装配依赖的对象,如果对象是 ServiceAware的实例为真 if (service instanceof ServiceAware) { // 则调用此接口的方法注入 ServiceAware对象 ((ServiceAware) service).injectService(new ServiceImpl()); } injector.services.put(clazz, service); } catch (Exception e) { e.printStackTrace(); } return (T) service; } } // 只要服务类实现这个接口,容器就会注入 Service对象,定义一个 Client类实现该接口 public class Client implements ServiceAware { private Service service; @Override public void injectService(Service service) { this.service = service; } public void doSomething() { service.excute(); } public static void main(String[] args) { InterfaceInjector.configure(); Client client = InterfaceInjector.getInstance(Client.class); client.doSomething(); } } -- Service is doing something...
Parameter 注入 :外部程序可以通过函数参数,给客户程序注入所依赖的服务对象。
//参数注入 public class Client { // 外部程序通过函数参数,给客户程序注入所依赖的服务对象 // 采用外部传递 Service对象方式,至于Service如何实例化,它一无所知 public void dosomething(Service service) { service.excute(); } // 使用方法:外部程序实例化一个 Service实例,传递给 Client的 dosomething(Service service)方法使用 public static void main(String[] args) { Service service = new ServiceImpl(); new Client().dosomething(service); } }
注意:这种形式的注入比较特殊,Client 类的 dosomething(Service service) 方法不光完成了依赖的装配,而且执行了 Service 回调的方法 execute( ),完成了其它逻辑。
Setter 和 Constructor 注入都是一种特殊的参数注入,它规定了只能使用 setting方法注入依赖 或者 只能对构造函数实现参数注入。
一些流行 DI 框架在使用参数注入实例化对象时,往往结合 Annotation注入、Interface 注入等方式一起使用,但是明显的一条,这些实现方法不应包含除依赖装配之外的其它逻辑。
其它形式的注入:
容器/框架提供一些高级用法,主要用来解决一些特殊问题。如:Spring框架的 lookup方法注入方式。使用场景:一个singleton的Bean需要引用一个prototype的Bean; 一个无状态的Bean需要引用一个有状态的Bean; ... ; 等等情景下,一般用的不多。
总结:
- IoC使得客户专注于客户化的逻辑设计,把程序流程的控制交给外部代码,实现高内聚低耦合目标。
- 把实例化的过程交给框架/容器来处理,使得我们更专注于业务逻辑的开发。