什么是 IoC ?
早在2004年,Martin Fowler就提出了“哪些方面的控制被反转了?”这个问题。他总结出是依赖对象的获得被反转了,因为大多数应用程序都是由两个或是更多的类通过彼此的合作来实现业务逻辑,这使得每个对象都需要获取与其合作的对象(也就是它所依赖的对象)的引用。如果这个获取过程要靠自身实现,那么这将导致代码高度耦合并且难以维护和调试。
技术描述
Class A中用到了Class B的对象b,一般情况下,需要在A的代码中显式地用 new 创建 B 的对象。
采用依赖注入技术之后,A 的代码只需要定义一个 private 的B对象,不需要直接 new 来获得这个对象,而是通过相关的容器控制程序来将B对象在外部new出来并注入到A类里的引用中。而具体获取的方法、对象被获取时的状态由配置文件(如XML)来指定。
IoC也可以理解为把流程的控制从应用程序转移到框架之中。以前,应用程序掌握整个处理流程;现在,控制权转移到了框架,框架利用一个引擎驱动整个流程的执行,框架会以相应的形式提供一系列的扩展点,应用程序则通过定义扩展的方式实现对流程某个环节的定制,“框架Call应用”。基于MVC的web应用程序就是如此。
实现方法
实现控制反转主要有两种方式:依赖注入和依赖查找。两者的区别在于,前者是被动的接收对象,在类A的实例创建过程中即创建了依赖的B对象,通过类型或名称来判断将不同的对象注入到不同的属性中,而后者是主动索取相应类型的对象,获得依赖对象的时间也可以在代码中自由控制。
依赖注入
在软件工程中,依赖注入(dependency injection,缩写为 DI)是一种软件设计模式,也是实现控制反转(IOC)的中的一种技术。这种模式能让一个物件接收它所依赖的其他组件。“依赖”是指接收方所需的对象。“注入”是指将“依赖”传递给接收方的过程。在“注入”之后,接收方才可以调用该“依赖”中的方法。
注:编程语言层次下,“接收方”为对象和 class,“依赖”为变量。在提供服务的角度下,“接收方”为客户端,“依赖”为服务。
该设计的目的是为了分离关注点,分离接收方和依赖,从而提供松耦合以及代码重用性。
传统编程方式,客户对象自己创建一个服务实例并使用它。这带来的缺点和问题是:
- 如果使用不同类型的服务对象,就需要修改、重新编译客户类。
- 客户类需要通过配置来适配服务类及服务类的依赖。如果程序有多个类都使用同一个服务类,这些配置就会变得复杂并分散在程序各处。
- 难以单元测试。本来需要使用服务类的 mock 或 stub,在这种方式下不太可行。
依赖注入可以解决上述问题:
- 使用接口或抽象基类,来抽象化依赖实现。
- 依赖在一个服务容器中注册。客户类构造函数被注入服务实例。框架负责创建依赖实例。
依赖注入有如下实现方式:
- 基于接口。实现特定接口以供外部容器注入所依赖类型的对象。
- 基于 set 方法。实现特定属性的public set方法,来让外部容器调用传入所依赖类型的对象。
- 基于构造函数。实现特定参数的构造函数,在新建对象时传入所依赖类型的对象。
- 基于注解。基于Java的注解功能,在私有变量前加“@Autowired”等注解,不需要显式的定义以上三种代码,便可以让外部容器传入对应的对象。该方案相当于定义了public的set方法,但是因为没有真正的set方法,从而不会为了实现依赖注入导致暴露了不该暴露的接口(因为set方法只想让容器访问来注入而并不希望其他依赖此类的对象访问)。
意思是自身对象中的内置对象是通过注入的方式进行创建。依赖注入有两种实现方式:Setter方式(传值方式)和构造器方式(引用方式)。
容器全权负责组件的装配,它会把符合依赖关系的对象通过属性(JavaBean中的setter)或者是构造子传递给需要的对象。
相对于IoC而言,依赖注入(DI)更加准确地描述了IoC的设计理念。所谓依赖注入,即组件之间的依赖关系由容器在应用系统运行期来决定,也就是由容器动态地将某种依赖关系的目标对象实例注入到应用系统中的各个关联的组件之中。
依赖查找
依赖查找更加主动,在需要的时候通过调用框架提供的方法来获取对象,获取时需要提供相关的配置文件路径、key等信息来确定获取对象的状态
IoC控制反转的本质就是把类的依赖对象的控制权交给了容器。
为什么要用IOC?
- 第一:对象的实例化不是一件简单的事情,比如对象的关系比较复杂,依赖关系往往需要程序员去维护,这是一件非常头疼的事。
- 第二:解耦,由容器去维护具体的对象
- 第三:托管了类的产生过程,比如我们需要在类的产生过程中做一些处理,最直接的例子就是代理,如果有容器程序可以把这部分过程交给容器,应用程序则无需去关心类是如何完成代理的
对依赖查找和依赖注入的总结
| 类型 | 依赖处理 | 实现便利性实现 | 代码侵入性 | API依赖性 | 可读性 |
|---|---|---|---|---|---|
| 依赖查找 | 主动获取 | 相对繁琐 | 侵入业务逻辑 | 依赖容器 API | 良好 |
| 依赖注入 | 被动提供 | 相对便利 | 低侵入性 | 不依赖容器 API | 一般 |
构造器注入 VS. Setter 注入
构造器注入VS Setter 注入
Spring Framework 对构造器注入与Setter 的论点:
来自“Constructor-based or setter-based DI”
“The Spring team generally advocates constructor injection, as it lets you implement application components as immutable objects and ensures that required dependencies are not null.
Spring团队提倡构造函数注入,因为它允许您将应用程序组件实现为不可变对象,并确保所需的依赖项不为空。
这个讲法忽略了一个细节, 其实如果依赖的对象为空或者不存在的话, 可以用另外的方式, 也可以通过构造器方式进行注入, 使用 ObjectProvider 类, 它是一种类型安全的方式, 如果你的依赖注入或依赖查找的方式需要的是一个单一类型的依赖, 这时可以调用它的 getIfAvailable() ,来进行示范性的返回, 等于空的时候就会返回空, 这种方式在 springBoot的场景中经常被用到。
Furthermore, constructor-injected components are always returned to the client (calling) code in a fully initialized state. As a side note, a large number of constructor arguments is a bad code smell, implying that the class likely has too many responsibilities and should be refactored to better address proper separation of concerns.
大量的构造函数参数是一种糟糕的代码气味,这意味着类可能有太多的责任,应该进行重构以更好地解决适当的关注点分离问题。
构造器注入的组件通常返回给客户端代码的时候, 是一个完整的初始化状态. 不过呢, 如果构造器的参数过多, 代码看起来就不是十分的良好, 意味着类包含了太多的职责, 不能有太多的输入, 最好进行一下重构(refactored)
Setter injection should primarily only be used for optional dependencies that can be assigned reasonable default values within the class. Otherwise, not-null checks must be performed everywhere the code uses the dependency. One benefit of setter injection is that setter methods make objects of that class amenable to reconfiguration or re-injection later.
Setter注入应该主要仅用于我们的可选性的注入, 另一方面, 如果代码中有用到相关的依赖, 非空检查就是必须的, 我们都知道@Autowired注解是有非空检查的参数的, 而默认可以注入空的对象进去,当然 Setter 注入可以让依赖进行延迟注入, 让对象变得更可配, 这是一个优点。
构造器注入VS Setter 注入的优缺点
以下来自:《Expert One-on-One™ J2EE™ Development without EJB™》
Setter 注入的优点:
Setter注入的优点:
- JavaBean properties are well supported in IDEs.(JavaBean的属性在IDE方面支持良好)
- JavaBean properties are self-documenting.(JavaBean的属性是 self-documenting 的方式, 通常不需要过多文档说明)
- JavaBean properties are inherited by subclasses without the need for any code.
JavaBean的属性通过继承的方式呢, 通常不需要任何的修改, 但是这个通常不应该是个优点, 只是作为面向对象的一个特点
- It’s possible to use the standard JavaBeans property-editor machinery for type conversions if necessary.(因为有 propertyEditor 这个机制, setter中可以进行一些类型转换)
- Many existing JavaBeans can be used within a JavaBean-oriented IoC container without modification.(大量存在的JavaBeans可以在JavaBean原生的IoC容器中使用, 不需要做太大的修改.这里就是指的原生的 BeanContext 这个容器, 这个上下文其实和 spring的 ApplicationContext 有异曲同工之处.)
- If there is a corresponding getter for each setter (making the property readable, as well as writable), it is possible to ask the component for its current configuration state. This is particularly useful if we want to persist that state: for example, in an XML form or in a database. With Constructor Injection, there’s no way to find the current state.(如果每一个Setter方法都有对应的Getter方法的情况下, 可以比较方便的在任何时候获取到组件的当前状态.这个特点在我们需要保存这个状态的时候特别有用, 而构造器注入就做不到这点. 这样就容易对Bean进行一些管理.)
- Setter Injection works well for objects that have default values, meaning that not all properties need to be supplied at runtime.(Setter方法可以为属性设置一个默认值, 并不是所有的属性都需要在运行时才进行设值.)
Setter 注入的缺点:
- The order in which setters are called is not expressed in any contract. Thus, we sometimes need to invoke a method after the last setter has been called to initialize the component. Spring provides the org.springframework.beans.factory.InitializingBean interface for his; it also provides the ability to invoke an arbitrary init method. However, this contract must be documented to ensure correct use outside a container.
setter的顺序无法保证, 而构造器注入因为构造器方法的参数顺序是一定的, 天然决定了设值的顺序是有序的, 不会因为外部调用的顺序而出现问题. 这里提到了一个spring的接口 InitializingBean ,这个接口能够帮助我们实现一个特性就是当我的属性已经设置好了之后, 我们再做相应的操作. 但是这个接口并不会强制的保证正确性, 只是一个鼓励.
- Not all the necessary setters may have been called before use. The object can thus be left partially configured.”
在调用前并不是每一个Setter方法都需要被调用的.
构造器注入的优点:
- Each managed object is guaranteed to be in a consistent state—fully configured—before it can be invoked in any business methods. This is the primary motivation of Constructor Injection. (However, it is possible to achieve the same result with JavaBeans via dependency checking, as Spring can optionally perform.) There’s no need for initialization methods.
能够确保每个被管理的对象有一致的状态. 完全配置好的, 且一旦初始化完成就不再进行变更的. 正如前面所说, 也因此组件对应的对象通常也被声明为 final 的.
- There may be slightly less code than results from the use of multiple JavaBean methods, although will be no difference in complexity.”(减少过多的Setter和Getter方法的代码量. 这个因为现在有建造模式, 似乎也没啥必要)
构造器注入的缺点:
这里就能看到, 作者一开始书里的观点, 和后来在spring官方网站的观点, 有一些冲突, 这里反而构造器注入的缺点列了一大堆.
• Although also a Java-language feature, multi-argument constructors are probably less common in existing code than use of JavaBean properties.(尽管多参数构造函数也是Java语言的一个特性,但在现有代码中可能不如使用JavaBean属性更容易使用)
• Java constructor arguments don’t have names visible by introspection.(构造器参数没有一个名称是可见的来提供给外部进行外省或内省.)
• Constructor argument lists are less well supported by IDEs than JavaBean setter methods.(构造函数参数列表在IDE中的支持不如JavaBean setter方法好)
• Long constructor argument lists and large constructor bodies can become unwieldy.(长的构造函数参数列表和大的构造函数体可能会变得笨拙)
• Concrete inheritance can become problematic. (继承可能会成为问题)
• Poor support for optional properties, compared to JavaBeans(与JavaBean相比,对可选属性的支持较差)
• Unit testing can be slightly more difficult(单元测试可能稍微困难一些)
• When collaborators are passed in on object construction, it becomes impossible to change the reference held in the object. (当协作者被传入到对象的构造器中后,就不能更改对象中保存的引用了。)