理解DI框架的必要性
理解DI框架的必要性
这可能是一个天真的问题。我目前正在学习Spring框架和依赖注入。虽然DI的基本原理相对容易理解,但为什么需要一个复杂的框架来实现它并不是立即显而易见的。
考虑以下示例:
public abstract class Saw { public abstract void cut(String wood); } public class HandSaw extends Saw { public void cut(String wood) { // chop it up } } public class ChainSaw extends Saw { public void cut(String wood) { // chop it a lot faster } } public class SawMill { private Saw saw; public void setSaw(Saw saw) { this.saw = saw; } public void run(String wood) { saw.cut("some wood"); } }
然后你可以简单地这样做:
Saw saw = new HandSaw(); SawMill sawMill = new SawMill(); sawMill.setSaw(saw); sawMill.run();
这相当于:
再加上:
ApplicationContext context = new ClassPathXmlApplicationContext("sawmill.xml"); SawMill springSawMill = (SawMill)context.getBean("sawMill"); springSawMill.run();
诚然,这是一个假设的例子,对于更复杂的对象关系,将其存储在XML文件中可能比以编程方式编写更高效,但肯定还有更多需要考虑的地方吧?
(我知道Spring框架不仅仅是这样,但我在考虑DI容器的需求。)
在第一个例子中,在中途更改依赖关系也很简单:
// 必须更快地砍 saw = new ChainSaw(); sawMill.setSaw(saw); sawMill.run();
Spring是一个非常重要的框架,它有三个同等重要的特性:依赖注入、面向切面编程和提供了一系列的框架类来帮助处理持久化、远程调用、Web MVC等问题。当我们将依赖注入与简单的一个new关键字进行比较时,很难看出依赖注入的优势。因为相比之下,使用new只需要一行代码,看起来更简单。而Spring的配置总是会增加代码的行数,所以这并不是一个胜出的论点。
但是当我们将一个横切关注点(cross-cutting concern)例如事务从我们的类中剥离出来,并使用面向切面编程的方式来设置它们时,情况就变得不一样了。与单个的"new"调用相比,Spring的优势就会显现出来。这也不是Spring被创建的初衷。
也许使用Spring最好的结果就是它推荐的编码范式使用了接口、分层和DRY等良好原则。它实际上是Rod Johnson在他的咨询工作中使用的面向对象最佳实践的精华。他发现,随着时间的推移,他建立的代码帮助他为客户交付更好的软件,他将自己的经验总结在了《Expert 1:1 J2EE》中,并将代码开源为Spring。
我想说的是,你应该根据自己的需求来选择使用该框架的程度。我认为只有将这三个特性结合起来,才能真正发挥Spring的价值。
理解DI框架的需求
在软件开发中,依赖注入(Dependency Injection,DI)是一个重要的设计模式,它可以解决类与类之间的耦合问题。通过使用DI框架,可以实现依赖的解耦和灵活性。然而,有些人对DI框架的需求产生了疑问,并提出了解决方案。
有人认为,在类A中直接实例化HandSaw或者从SawMill获取所有依赖项会导致类A与这些依赖项之间产生耦合。为什么类A需要与HandSaw耦合,或者更现实的情况是,为什么我的业务逻辑需要与DAO层所需的JDBC连接实现耦合呢?
为了解决这个问题,有人提出了将依赖项进一步移动的解决方案。通过使用DI框架,可以通过配置XML或JavaConfig来获取所需的服务。你不需要关心它是如何初始化的,它需要什么来工作,你只需要获取服务对象并激活它。
此外,对于“plus:”部分的理解也存在误区。你不需要从上下文中获取sawMill bean,sawMill bean应该已经被DI框架注入到你的对象(类A)中。所以,你只需要调用“sawMill.run()”,不需要关心它来自何处,由谁初始化,以及如何。对你来说,它可能直接进入/dev/null,或者是测试输出,或者是真实的CnC引擎...关键是,你不在乎。你只关心你那个小小的类A应该按照合同的要求去激活一个锯木厂。
然而,有人认为这个答案与依赖注入模式的“官方”定义相矛盾。只有在需要在运行时选择实现“Saw”抽象类的实际类时,类A与类HandSaw的耦合才是一个问题。否则,在客户端代码中直接实例化一个Saw实现类是完全可以接受的。DI的真正目的是在确实需要的情况下,“将配置与使用分离”,而不是默认情况下的任何地方。
然而,这个观点在测试时并不成立。在测试时,你希望能够将一个只进行断言的“TestSawImpl”放入A中,而不是一个需要一堆木材、电源和经过认证的锯操作员(可能还需要一个医生)的真实“HandSaw”。
有人认为,在单元测试中使用模拟工具可以轻松实现,不需要创建一个“TestSawImpl”。即使在编译时不知道具体的类,使用模拟工具也可以用一行代码模拟任何Saw实现类。
然而,引入另一层编织/动态类加载/其他技巧来使测试工作,使它们更加脱离实际模拟的现实世界。虽然这是可行的,但我更倾向于简单的解决方案-在测试中设置一个setter,以不同的方式进行设置。
因此,对于单元测试,你根本不使用任何模拟API吗?正如我所描述的,使用模拟工具并不比更“传统”的工具复杂(事实上恰恰相反)。
实际上,我更喜欢在可能的情况下提供自己的模拟。我尽量少使用“魔术”(编织/类加载器替换)。因为它往往会干扰产品的“魔术”。
好吧,如果你更喜欢手工模拟,那对我来说也没问题。然而,其他人认为使用模拟API是更简单的解决方案。