面向对象编程(OOP)不仅是一种设计编程语言的新方式,也是一种完全不同的程序设计方式。
本文解释了如何使用Smalltalk设计系统。由于软件重用是面向对象编程的主要动机之一,本文还阐述了如何开发可重用的类。
什么是IoC?
控制反转(Inversion of Control, IoC)是一种软件设计模式,程序员编写的程序会受到可重用库的流程控制。
在传统的编程中,程序的流程是由程序员编写的代码调用外部库来实现的。然而,在应用了控制反转的结构中,外部库的代码会反过来调用程序员编写的代码。
IoC的概念最早出现在1988年Ralph E. Johnson和Brian Foote撰写的经典论文《Designing Reusable Classes》中。这篇论文旨在提高代码的可重用性,并提出了OOP的几个核心原则:
- 多态性(Polymorphism): 不同对象可以通过相同的消息进行交互的特性。
- 协议(Protocol): 对象需要提供的一组服务或方法集,即接口的明确定义。
- 继承(Inheritance): 新类可以继承现有类的特性和行为,并对其进行扩展或修改的机制。
- 抽象类(Abstract Classes): 不提供具体实现,而是定义子类必须实现的接口或基本结构的类。
(需要注意的是,这些定义与我们现在使用的OOP概念有很大不同。这反映了OOP从Alan Kay的概念到后来Booch的概念演变的过程。)
框架的定义:
在这篇论文中,作者将框架定义为以下公式:
框架 = 组件 + 控制反转(IoC)
这里,IoC被解释为框架控制应用程序流程的方式。这篇论文被认为是现代框架概念的奠基之作,也是首次系统化地定义IoC的论文。
IoC的核心思想是,与传统程序由用户直接控制流程不同,IoC允许外部控制流程,而用户只需实现必要的部分。(论文中将这个“外部”称为框架。)
(尽管IoC一词通常被认为起源于1988年的《Designing Reusable Classes》,但在那之前已经有人在使用类似的概念。这篇论文只是正式确立了这一术语。)
// 传统方式 - 直接在类 B 内部创建 A 的实例
class B {
public void doSomethingWithA() {
A a = new A(); // B 直接创建 A 实例
a.action();
}
}
// A 类(依赖提供者)
class A {
public void action() {
System.out.println("A 的动作");
}
}
// 测试代码
public class Main {
public static void main(String[] args) {
B b = new B(); // 创建 B 的实例
b.doSomethingWithA(); // 直接调用方法
}
}
简单示例:
在传统的编程方式中,对象在需要时自行创建或生成其他对象。也就是说,程序的流程由对象自身主导。
而在应用了IoC的情况下,对象不会自行创建或查找所需的对象。相反,外部容器负责创建所需的对象并设置对象之间的依赖关系。
通过这种方式,对象可以专注于其核心逻辑。
一个简单的例子是IoC中最基础的形式之一:构造函数注入。
// A 类(依赖提供者)
class A {
public void action() {
System.out.println("A 的动作");
}
}
// B 类(依赖消费者) - 通过构造函数注入 A
class B {
private A a; // 依赖项
// 通过构造函数注入 A
public B(A a) {
this.a = a;
}
public void doSomethingWithA() {
a.action(); // 使用注入的 A 实例
}
}
// 外部容器
public class Main {
public static void main(String[] args) {
A a = new A(); // 由容器创建 A 实例
B b = new B(a); // 由容器创建 B 实例,并注入 A
b.doSomethingWithA(); // 执行方法
}
}
IoC的核心概念:
为了更好地理解IoC,我们先介绍其核心概念。
“别打电话给我们,我们会打给你。” —— Sugarloaf, 1974
IoC的核心概念可以总结如下:
好莱坞原则(Hollywood Principle):
这一原则最早出现在1983年Richard E. Sweet撰写的《The Mesa Programming Environment》论文中,后来由现代OOP之父之一John Vlissides在《C++ Report》中引用,并由Martin Fowler推广而广为人知。
这一原则源自好莱坞电影公司的做法:演员不需要主动联系导演,而是留下联系方式等待导演的“回电”。
简单来说:
- 演员(调用者)不会主动联系导演(框架)请求出演机会,而是由导演根据需要选择合适的演员并发出“选角电话”。这强调了控制权不在演员手中,而是在导演手中。
总之,IoC意味着控制的反转,即将程序的流程控制从开发者转移到框架。开发者遵循框架提供的规则和API编写代码,而框架则管理对象的生命周期和交互。
IoC的实现模式:
IoC的实现模式可以从两个主要方面来看:
- 回调(Callback): 在事件处理、GUI框架中常见。
- 模板方法模式(Template Method Pattern): 上层类定义整个算法流程,下层类可以覆盖部分步骤。
- 发布-订阅模式(Publisher-Subscriber Pattern): 当主题(Subject)触发某个事件时,通知注册的发布者。
IoC容器(IoC Container):
实际实现和管理IoC的角色是IoC容器。IoC容器的主要功能包括:
- 对象的创建与管理: 创建应用程序所需的对象并管理其生命周期(创建、初始化、销毁等)。
- 依赖注入(Dependency Injection, DI): 分析对象之间的依赖关系,并将所需的对象注入到其他对象中。
- 对象的检索与提供: 根据需要检索并提供容器中注册的对象。
IoC的优点包括:
- 降低耦合度: 减少对象之间的依赖,提高代码的可重用性。
- 增强灵活性: 可以通过配置文件或其他机制轻松更改依赖关系。
- 简化代码: 减少了对象创建和依赖管理的代码,使代码更加简洁。
-
DI(依赖注入)是什么?
读者可能对突然出现的“依赖注入(DI)”感到困惑。接下来我们将解释DI的概念。
由于IoC的范围过于广泛,可能会让人难以理解。因此,伟大的程序员Martin Fowler在一篇经典文章《Inversion of Control Containers and the Dependency Injection pattern》(2004)中整理了IoC的各种形式,并定义了依赖注入(DI)作为IoC的一种特定模式。
控制反转(IoC)"这一术语过于宽泛,容易让人感到困惑。
因此,经过与众多IoC倡导者的广泛讨论,最终决定采用“依赖注入(Dependency Injection, DI)”这一名称
接着,Martin Fowler在文章中举例说明了DI的三种具体形式:
依赖注入的三种形式:
// A 类(依赖提供者)
class A {
public void action() {
System.out.println("A 的动作");
}
}
// B 类(依赖消费者) - 通过构造函数注入 A
class B {
private A a; // 依赖项
// 通过构造函数注入 A
public B(A a) {
this.a = a;
}
public void doSomethingWithA() {
a.action(); // 使用注入的 A 实例
}
}
// 外部容器
public class Main {
public static void main(String[] args) {
A a = new A(); // 由容器创建 A 实例
B b = new B(a); // 由容器创建 B 实例,并注入 A
b.doSomethingWithA(); // 执行方法
}
}
- 构造函数注入(Constructor Injection): 在对象创建时通过构造函数注入依赖(Spring和DI容器中常用)
// A 类(依赖提供者) class A { public void action() { System.out.println("A 的动作"); } } // B 类(依赖消费者) - 通过 Setter 方法注入 A class B { private A a; // 依赖项 // Setter 方法用于注入 A public void setA(A a) { this.a = a; } public void doSomethingWithA() { if (a != null) { a.action(); // 使用注入的 A } else { System.out.println("A 未被注入"); } } } // 外部容器 public class Main { public static void main(String[] args) { A a = new A(); // 容器创建 A 实例 B b = new B(); // 容器创建 B 实例 b.setA(a); // 通过 Setter 方法注入 A b.doSomethingWithA(); // 执行方法 } }
- Setter注入(Setter Injection): 通过Setter方法注入依赖(常用于XML配置)。
// 依赖注入接口 interface InjectableA { void injectA(A a); } // A 类(依赖提供者) class A { public void action() { System.out.println("A 的动作"); } } // B 类(依赖消费者) - 通过接口方法注入 A class B implements InjectableA { private A a; // 通过接口方法注入 A @Override public void injectA(A a) { this.a = a; } public void doSomethingWithA() { if (a != null) { a.action(); // 使用注入的 A } else { System.out.println("A 未被注入"); } } } // 外部容器 public class Main { public static void main(String[] args) { A a = new A(); // 容器创建 A 实例 B b = new B(); // 容器创建 B 实例 b.injectA(a); // 通过接口方法注入 A b.doSomethingWithA(); // 执行方法 } }
- 接口注入(Interface Injection): 通过接口注入依赖(作者常用)。
注入方式 | 优点 | 缺点 | 使用场景 |
---|---|---|---|
构造函数注入 | 保证不可变性,对象完全初始化 | 循环依赖难以处理 | 必须依赖的情况 |
Setter注入 | 支持可选依赖,易于解决循环依赖 | 难以保证不可变性,对象状态可能改变 | 非必要依赖的情况 |
接口注入 | 限制特定对象的注入 | 维护困难,代码可能变得复杂 | 强制特定注入模式 |
总结:
- IoC(控制反转): 将控制流委托给外部的上层概念。可以通过框架、回调、事件等方式实现。
- DI(依赖注入): 实现IoC的下层概念,通过外部注入对象之间的依赖关系。
最后,IoC和DI不仅仅是技术工具,更是反映软件设计哲学的概念。通过降低对象之间的耦合度,提高代码的灵活性和测试便利性,它们可以帮助我们设计和开发更优秀的软件。