引言
在 Java 中,出现了大量轻量级容器,这些容器有助于将来自不同项目的组件组装成一个有凝聚力的应用程序。这些容器的底层是它们如何执行布线的常见模式,它们将这一概念称为“控制反转”。
🏢 本章内容
🏭 IoC服务理念;
🏭 让别人为你服务;
🏭 IoC的附加值;
IoC服务理念
IoC
—Inversion of Control
,即_“控制反转”_,不是什么技术,而是一种设计思想。其核心理念是从传统的程序设计中将控制权逆转,通过外部容器来管理对象的创建、组装和生命周期,从而降低代码之间的耦合度,提高系统的灵活性和可维护性。
在实际应用中,IoC 的实现方式主要包括依赖注入(Dependency Injection,DI)和依赖查找(Dependency Lookup)两种方式。
🗡️ 依赖注入:
是一种常见的实现方式,通过构造函数、方法参数、或者特定的注入接口,将对象所依赖的其他对象交由外部容器负责创建和管理,而不是在对象内部通过 new 关键字主动创建。这样一来,对象的依赖关系由外部容器来维护,对象本身不需要关心依赖对象的创建和生命周期,从而实现了解耦合。
🗡️ 依赖查找:
是另一种IoC的实现方式,它通过容器提供的API,允许对象查询(或查找)所依赖的其他对象。对象在需要依赖对象时,向容器发出查询请求,容器则负责返回相应的依赖对象。这种方式相对于依赖注入来说更加灵活,但也增加了代码的复杂度和耦合度。
理解 Ioc 的要点
🥋 谁控制谁,控制什么:
理解IoC的关键在于颠覆传统程序设计中对象间的关系模式。传统模式中,对象之间的依赖关系由对象自身负责管理和创建,而IoC则是将这种控制权交给外部容器,实现了对象间的松耦合。这种“谁控制谁,控制什么”的转变,使得代码更加清晰、可扩展、易维护。
🥋 为何是反转,哪些方面反转了
为什么称之为“反转”呢?因为在IoC中,传统的对象创建和管理流程被颠倒过来了。传统情况下,对象自身主动创建和管理依赖对象,而在IoC中,对象成为被动的接受者,由外部容器负责创建、管理和注入依赖对象。因此,IoC被称为一种“反转”,它颠覆了传统程序设计中对象间的控制关系。
举个栗子
🥊 **你口渴想喝水吧?**以前,你得自己找到水(依赖对象),然后自己到杯子里,然后再喝水。
而现在,你只要跟你的“另一半” 使个眼色或说一句“Honey,水拿来。” 她就会心领神会地到饮水机那里为你接杯水,然后再给喂你喝下。(此时此刻,你心里肯定窃喜,“有人照顾的感觉真好!”)对你来说,到底哪种场景比较惬意,我想已经不言自明了吧?
🥊 使用前:
🥊 使用后:
让别人为你服务
🔫 “先生,晚上好,欢迎光临XXX,拖鞋手牌拿好,楼上请,男宾一位 。” 当你开着凯迪拉克来到洗浴中心,想要洗澡的时候,通常会直接招呼服务生,让他为你送来拖鞋手牌。同样地,作为被注入对象,要想让IoC为其提供服务,并将所需要的被依赖对象送过来,也需要通过某种方式通知对方
🧟 如果你是洗浴中心的常客,或许你刚进门,服务生已经将你最常使衣柜,手牌放到了你面前;
🧟 如果你是初次或偶尔光顾,也许你坐下之后还要招呼服务生,“给我拿双拖鞋和手牌”;
🧟 还有一种可能,你根本就不知道的时候,这时,你只示意一下告诉服务生你到底想要什么。
不管怎样,你终究会找到一种方式来向服务生表达你的需求,以便他为你提供适当的服务。那么,在IoC模式中,被注入对象又是通过哪些方式来通知IoC Service为其提供适当服务的呢?
🏹 IoC模式最权威的总结和解释,应该是Martin Fowler的那篇文章 Inversion of Control Containers and the Dependency Injection pattern ,其中提到了三种依赖注入的方式,即构造方法注入(constructor injection)、 setter方法注入(setter injection)以及接口注入(interface injection)。
🪓 构造方法注入(Constructor Injection):
这种方式通过在对象实例化时传递依赖项来完成注入。在洗浴中心中,你作为常客,通过注册时填写的偏好信息,告知洗浴中心你需要的沐浴露和洗发水。当你到达时,洗浴中心已经根据你的偏好提前准备好了相应的产品,直接为你提供服务。构造方法注入的优点在于依赖项的一致性和可靠性。
// 构造方法注入public BathCenter(BathProduct bathProduct) {this.bathProduct = bathProduct;}
🪓 Setter方法注入(Setter Injection):
这种方式通过调用对象的setter方法来完成依赖项的注入。对于偶尔光顾的顾客而言,他们在到达洗浴中心后,需要向服务员说明他们的洗浴偏好。服务员根据顾客的需求,调用相应的setter方法设置洗浴服务,使得顾客能够享受到与常客相同的洗浴体验。Setter方法注入的灵活性允许在运行时动态地更改依赖项。
// Setter方法注入public void setBathProduct(BathProduct bathProduct) {this.bathProduct = bathProduct;}
🪓 接口注入(Interface Injection):
这种方式通过定义接口来完成依赖项的注入。对于新来的游客而言,他们可能并不清楚洗浴中心提供的服务项目,也不了解适合自己的沐浴产品。在这种情况下,游客通过与服务员的交流来表达自己的需求,服务员根据游客的需求来为其安排相应的服务。接口注入允许通过抽象的接口定义来解耦组件之间的依赖关系,提高了系统的灵活性和可维护性。
// 沐浴产品接口
interface BathProduct {void use();
}// 沐浴中心类
class BathCenter {private BathProduct bathProduct;// 构造方法注入public BathCenter(BathProduct bathProduct) {this.bathProduct = bathProduct;}// Setter方法注入public void setBathProduct(BathProduct bathProduct) {this.bathProduct = bathProduct;}// 提供洗浴服务public void provideBathService() {System.out.println("欢迎光临洗浴中心!");bathProduct.use(); // 使用沐浴产品System.out.println("感谢您的光临,祝您洗浴愉快!");}
}// 沐浴露类
class ShowerGel implements BathProduct {@Overridepublic void use() {System.out.println("使用沐浴露洗澡");}
}// 洗发水类
class Shampoo implements BathProduct {@Overridepublic void use() {System.out.println("使用洗发水洗头");}
}// 测试类
public class Main {public static void main(String[] args) {// 构造方法注入BathProduct showerGel = new ShowerGel();BathCenter bathCenter1 = new BathCenter(showerGel);bathCenter1.provideBathService();// Setter方法注入BathProduct shampoo = new Shampoo();BathCenter bathCenter2 = new BathCenter(null); // 初始化时不注入沐浴产品bathCenter2.setBathProduct(shampoo);bathCenter2.provideBathService();// 接口注入BathCenter bathCenter3 = new BathCenter(null); // 初始化时不注入沐浴产品bathCenter3.setBathProduct(new BathProduct() {@Overridepublic void use() {System.out.println("使用默认沐浴产品洗澡");}});bathCenter3.provideBathService();}
}
三种注入方式的比较
在IoC模式中,构造方法注入、Setter方法注入和接口注入是三种常见的依赖注入方式。它们在使用场景、灵活性和实现复杂度上有一些区别,以下是它们的比较:
🛡️ 构造方法注入(Constructor Injection):
- 使用场景: 适用于在对象创建时就确定其依赖关系的情况。常用于注入不变的依赖项,例如配置信息、常用服务等。
- 灵活性: 由于依赖项在对象创建时确定,因此一旦对象被创建,其依赖关系就不可更改。这可能会限制一些动态变化的需求。
- 实现复杂度: 相对简单,不需要提供额外的setter方法用于动态修改依赖。
🛡️ Setter方法注入(Setter Injection):
- 使用场景: 适用于依赖项可能在对象的生命周期内发生变化的情况。常用于可变的依赖关系,允许在运行时动态修改。
- 灵活性: 具有较高的灵活性,可以在任何时候通过调用setter方法来修改依赖关系。适用于需要动态切换依赖的情况。
- 实现复杂度: 相对较复杂,需要提供额外的setter方法,并在对象创建后可能需要进行额外的检查来确保正确的依赖关系。
🛡️ 接口注入(Interface Injection):
- 使用场景: 适用于希望通过共享接口来解耦依赖项的情况。可以用于将依赖注入抽象为接口,具体实现由外部注入。
- 灵活性: 具有一定的灵活性,可以通过不同的接口实现来注入不同的依赖。更适合于对接口较为关心的情况。
- 实现复杂度: 需要定义接口并提供不同的实现类。相对于构造方法注入和Setter方法注入,实现上略显繁琐。
IoC的附加值
IoC模式的附加值远不止于简单的依赖注入,它带来的好处包括更低的耦合度、更好的可测试性、可重用性和可扩展性等。让我们通过具体的示例来深入理解IoC模式的附加值。
降低耦合度
在传统的程序设计中,对象之间的依赖关系通常是硬编码的,对象直接实例化并使用其依赖对象。这种做法会导致高耦合度,当一个对象发生变化时,可能需要修改其他多个对象。而使用IoC模式,对象不再负责获取依赖对象,而是通过外部容器进行管理和注入,从而降低了对象之间的耦合度。
提高可测试性
IoC模式使得依赖对象的注入变得灵活,可以通过Mock对象等方式轻松替换真实的依赖对象,从而方便进行单元测试。单元测试可以针对对象的特定行为进行测试,而不受其依赖对象的影响,提高了测试的精度和可靠性。
增强可重用性
通过将依赖对象与实现解耦,使得依赖对象可以被多个对象共享和重用。这样一来,我们可以更轻松地将已经实现和测试过的组件应用到新的场景中,而不必重复开发和测试相同的功能。
支持可扩展性
IoC模式使得系统更容易扩展,新的功能模块可以通过依赖注入的方式加入到系统中,而不必修改现有的代码。这种松耦合的设计使得系统更具弹性,能够更好地适应未来的需求变化和业务扩展。
通过具体的示例,我们可以更直观地感受到IoC模式带来的好处。它不仅仅是一种设计模式,更是一种思想,能够帮助我们构建更灵活、可测试、可维护和可扩展的软件系统。
举个例子
忽然有一天,洗浴中心老板为了能够洗浴更多的顾客到店光顾,他茅塞顿开,想到顾客洗完澡后,可以到二楼休息,饮茶。于是提供了,乘电梯上2楼的服务。
☯️ 如果没有IoC,那么新增的业务是没有办法与顾客进行绑定的,客户并不知道我们提供了这样的服务。功能需要重新实现。
☯️ 如果使用IoC,我们可以主动的告知客户,我们提供了上2楼服务,这样在接口不发生变化的情况,我们可以提供更多的服务方式。
👹 原始提供的服务:
// 洗浴产品接口
interface BathProduct {void use();
}// 沐浴露实现
class ShowerGel implements BathProduct {@Overridepublic void use() {System.out.println("使用沐浴露洗澡");}
}// 洗发水实现
class Shampoo implements BathProduct {@Overridepublic void use() {System.out.println("使用洗发水洗头");}
}// 洗浴中心类
class BathCenter {private BathProduct bathProduct;// 构造方法注入public BathCenter(BathProduct bathProduct) {this.bathProduct = bathProduct;}// 提供洗浴服务public void provideBathService() {System.out.println("欢迎光临洗浴中心!");bathProduct.use(); // 使用洗浴产品System.out.println("感谢您的光临,祝您洗浴愉快!");}
}// 测试类
public class Main {public static void main(String[] args) {// 构造方法注入洗浴露BathProduct showerGel = new ShowerGel();BathCenter bathCenter1 = new BathCenter(showerGel);bathCenter1.provideBathService();// 构造方法注入洗发水BathProduct shampoo = new Shampoo();BathCenter bathCenter2 = new BathCenter(shampoo);bathCenter2.provideBathService();}
}
👻 新增搭乘电梯上2楼服务
// 上2楼服务实现:电梯服务
class ElevatorService implements BathProduct {@Overridepublic void use() {System.out.println("正在搭乘电梯前往2楼");}
}// 测试类
public class Main {public static void main(String[] args) {// 构造方法注入上2楼服务UpstairsService elevatorService = new ElevatorService();BathCenter bathCenter3 = new BathCenter(elevatorService);bathCenter3.provideBathService();}
}
结语
IoC模式不仅仅是一种设计模式,更是一种设计思想,通过它,我们能够构建出更加灵活、可测试和可维护的软件系统。在现实应用中,合理运用IoC模式能够为软件开发带来诸多益处