设计模式 - 创建型工厂模式
# 工厂模式概述
工厂模式很重要,后面的很多架构设计,都是工厂模式联合着其它设计模式使用。
一般情况下,工厂模式分为三种更加细分的类型:简单工厂、工厂方法和抽象工厂。不过,在 GOF 的《设计模式》一书中,它将简单工厂模式看作是工厂方法模式的一种特例,所以工厂模式只被分成了工厂方法和抽象工厂两类。实际上,前面一种分类方法更加常见,所以,在今天的讲解中,我们沿用第一种分类方法。
在这三种细分的工厂模式中,简单工厂、工厂方法原理比较简单,在实际的项目中也比较常用。而抽象工厂的原理稍微复杂点,在实际的项目中相对也不常用。
除此之外,本内容讲解的重点也不是原理和实现,因为这些都很简单,重点还是要搞清楚应用场景:什么时候该用工厂模式?相对于直接 new 来创建对象,用工厂模式来创建究竟有什么好处呢?
简单工厂模式
- 用来生产同一等级结构中的任意产品(对于增加新的产品,需要覆盖已有代码)
工厂方法模式
- 用来生产同一等级结构中的固定产品(支持增加任意产品)
抽象工厂模式
- 围绕一个超级工厂创建其他工厂,该超级工厂又称为其他工厂的工厂
# 简单工厂(Simple Factory)
# 简单工厂模式基本介绍
简单工厂模式是属于 创建型模式,是工厂模式的一种。简单工厂模式是由一个工厂对象决定创建出哪一种产品类的实例。简单工厂模式是 工厂模式家族 中最简单实用的模式。
简单工厂模式:定义了一个创建对象的类,由这个类来封装实例化对象的行为(代码)。
在简单工厂模式中创建实例的方法通常为静态(static)方法,因此 简单工厂模式(Simple Factory Pattern)又叫作 静态工厂方法模式(Static Factory Method Pattern)。
在软件开发中,当我们会用到大量的创建某种、某类或者某批对象时,就会使用到工厂模式。
主要优点
工厂类包含必要的逻辑判断,可以决定在什么时候创建哪一个产品的实例。客户端可以免除直接创建产品对象的职责,很方便的创建出相应的产品。工厂和产品的职责区分明确
客户端无需知道所创建具体产品的类名,只需知道参数即可
也可以引入配置文件,在不修改客户端代码的情况下更换和添加新的具体产品类
主要缺点
- 简单工厂模式的工厂类单一,负责所有产品的创建,职责过重,一旦异常,整个系统将受影响。且工厂类代码会非常臃肿,违背高聚合原则
- 使用简单工厂模式会增加系统中类的个数(引入新的工厂类),增加系统的复杂度和理解难度
- 系统扩展困难,一旦增加新产品不得不修改工厂逻辑,在产品类型较多时,可能造成逻辑过于复杂
- 简单工厂模式使用了 static 工厂方法,造成工厂角色无法形成基于继承的等级结构
主要角色
- 简单工厂(SimpleFactory):是简单工厂模式的核心,负责实现创建所有实例的内部逻辑。工厂类的创建产品类的方法可以被外界直接调用,创建所需的产品对象
- 抽象产品(Product):是简单工厂创建的所有对象的父类,负责描述所有实例共有的公共接口
- 具体产品(ConcreteProduct):是简单工厂模式的创建目标
结构图
# 传统方式完成披萨案例
看一个披萨的项目:要便于披萨种类的扩展,要便于维护
- 披萨的种类很多(比如 GreekPizz、CheesePizz 等)
- 披萨的制作有 prepare,bake, cut, box
- 完成披萨店订购功能
思路分析(类图)
编写制作披萨的过程类 Pizza.java
public abstract class Pizza {
protected String name; // 名字
// 准备原材料, 不同的披萨不一样,因此,我们做成抽象方法
public abstract void prepare();
// 省略 setter、getter 方法
}
class CheesePizza extends Pizza {
@Override
public void prepare() {
System.out.println(" 给制作奶酪披萨 准备原材料 ");
}
}
class GreekPizza extends Pizza {
@Override
public void prepare() {
System.out.println(" 给希腊披萨 准备原材料 ");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
编写 OrderPizza.java 去订购需要的各种披萨
public class OrderPizza {
public static void main(String[] args) {
CheesePizza cheesePizza = new CheesePizza();
GreekPizza greekPizza = new GreekPizza();
System.out.println(cheesePizza.getName);
System.out.println(greekPizza.getName);
}
}
2
3
4
5
6
7
8
9
10
需要什么类型的披萨,new 出该对象。
传统的方式的优缺点
- 优点是比较好理解,简单易操作
- 缺点是依赖性太强,不应该直接去 new 一个类
改进的思路分析
分析:修改代码可以接受,但是如果我们在其它的地方也有创建 Pizza 的代码,就意味着,也需要修改,而创建 Pizza 的代码,往往有多处。
思路:把创建 Pizza 对象封装到一个类中,这样我们有新的 Pizza 种类时,只需要修改该类就可,其它有创建到 Pizza 对象的代码就不需要修改了,即 简单工厂模式。
# 简单工厂模式完成案例
简单工厂模式的设计方案: 定义一个可以实例化 Pizaa 对象的类,封装创建对象的代码。
Pizza 类代码保持不变(上面有)。
简单工厂模式代码:
public class SimpleFactory {
// 方法一:if-else,缺点,违反了 OCP 原则
public Pizza createPizza(String orderType) {
Pizza pizza = null;
System.out.println("使用简单工厂模式");
if (orderType.equals("greek")) {
pizza = new GreekPizza();
pizza.setName(" 希腊披萨 ");
} else if (orderType.equals("cheese")) {
pizza = new CheesePizza();
pizza.setName(" 奶酪披萨 ");
}
return pizza;
}
// 方法二:利用方法代替 if-else,虽然也违反了 OCP 原则,但是为了迎合 OCP 原则,会花费大量的代价
public Pizza getGreekPizza() {
Pizza pizza = new GreekPizza();
pizza.setName(" 希腊披萨 ");
return pizza;
}
public Pizza getCheesePizza() {
Pizza pizza = new CheesePizza();
pizza.setName(" 奶酪披萨 ");
return pizza;
}
// 简单工厂模式 也叫 静态工厂模式(全部替换成 static 即可)
public static Pizza createPizza2(String orderType) {
Pizza pizza = null;
System.out.println("使用简单工厂模式2");
if (orderType.equals("greek")) {
pizza = new GreekPizza();
pizza.setName(" 希腊披萨 ");
} else if (orderType.equals("cheese")) {
pizza = new CheesePizza();
pizza.setName(" 奶酪披萨 ");
}
return pizza;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
此时的 OrderPizza.java
文件也要修改:
public class OrderPizza {
public static void main(String[] args) {
SimpleFactory simpleFactory = new SimpleFactory();
Pizza pizza = simpleFactory.createPizza("greek");
// Pizza pizza = SimpleFactory.createPizza("greek"); // 静态工厂模式
System.out.println(pizza.getName);
}
}
2
3
4
5
6
7
8
9
简单工厂模式虽然违反了 OCP 原则,但是花费的代价非常小,所以经常被使用。
# 工厂方法(Factory Method)
# 工厂方法模式介绍
工厂方法模式 是对简单工厂模式的进一步抽象化,其好处是可以使系统在不修改原来代码的情况下引进新的产品,即满足开闭原则。
主要优点
- 用户只需要知道具体工厂的名称就可得到所要的产品,无须知道产品的具体创建过程
- 灵活性增强,对于新产品的创建,只需多写一个相应的工厂类
- 典型的解耦框架。高层模块只需要知道产品的抽象类,无须关心其他实现类,满足迪米特法则、依赖倒置原则和里氏替换原则
主要缺点
- 类的个数容易过多,增加复杂度
- 增加了系统的抽象性和理解难度
- 抽象产品只能生产一种产品,此弊端可使用 抽象工厂模式 解决
工厂方法模式:定义了一个创建对象的抽象方法,由 子类决定要实例化的类。工厂方法模式将 对象的实例化推迟到子类。
主要角色
- 抽象工厂(Abstract Factory):提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法 newProduct() 来创建产品
- 具体工厂(ConcreteFactory):主要是实现抽象工厂中的抽象方法,完成具体产品的创建
- 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能
- 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一一对应
其结构图如下所示:
# 工厂方法模式完成案例
披萨项目新的需求:客户在点披萨时,可以点不同口味的披萨,比如北京的奶酪 pizza、北京的胡椒 pizza 或者是伦敦的奶酪 pizza、伦敦的胡椒 pizza。
思路 1
使用简单工厂模式,创建不同的简单工厂类,比如 BJPizzaSimpleFactory
、LDPizzaSimpleFactory
等等。从当前这个案例来说,也是可以的,但是考虑到项目的规模,以及软件的可维护性、可扩展性并不是特别好。
思路 2
使用工厂模式。
工厂方法模式设计方案:将披萨项目的实例化功能抽象成抽象方法,在不同的口味点餐子类中具体实现。
思路分析图解
代码实现
披萨类代码:
// 将 Pizza 类做成抽象
public abstract class Pizza {
protected String name; //名字
// 准备原材料, 不同的披萨不一样,因此,我们做成抽象方法
public abstract void prepare();
// 省略 setter、getter 方法
}
class BJCheesePizza extends Pizza {
@Override
public void prepare() {
setName("北京的奶酪 pizza");
System.out.println(" 北京的奶酪 pizza 准备原材料");
}
}
class BJPepperPizza extends Pizza {
@Override
public void prepare() {
setName("北京的胡椒 pizza");
System.out.println(" 北京的胡椒 pizza 准备原材料");
}
}
class LDCheesePizza extends Pizza{
@Override
public void prepare() {
setName("伦敦的奶酪 pizza");
System.out.println(" 伦敦的奶酪 pizza 准备原材料");
}
}
class LDPepperPizza extends Pizza{
@Override
public void prepare() {
setName("伦敦的奶酪 pizza");
System.out.println(" 伦敦的奶酪 pizza 准备原材料");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
工厂方法模式代码:
public abstract class OrderPizzaFactory {
// 定义一个抽象方法,createPizza,让各个工厂子类自己实现
abstract Pizza createPizza(String orderType);
// 构造器
public OrderPizza(String orderType) {
Pizza pizza = null;
pizza = createPizza(orderType); // 抽象方法,由工厂子类完成
}
}
2
3
4
5
6
7
8
9
10
11
由工厂方法模式创建的子类:
public class PizzaStore {
public static void main(String[] args) {
// 创建北京口味的各种 Pizza
BJOrderPizzaFactory bJOrderPizzaFactory = new BJOrderPizzaFactory();
Pizza pizza1 = bJOrderPizzaFactory.createPizza("cheese");
Pizza pizza2= bJOrderPizzaFactory.createPizza("pepper");
System.out.print(pizza1.getName());
System.out.print(pizza2.getName());
// 创建伦敦口味的各种 Pizza
LDOrderPizzaFactory lDOrderPizzaFactory = new LDOrderPizzaFactory();
Pizza pizza3 = lDOrderPizzaFactory.createPizza("cheese");
Pizza pizza4 = lDOrderPizzaFactory.createPizza("pepper");
System.out.print(pizza3.getName());
System.out.print(pizza4.getName());
}
}
class BJOrderPizzaFactory extends OrderPizzaFactory {
@Override
Pizza createPizza(String orderType) {
Pizza pizza = null;
if(orderType.equals("cheese")) {
pizza = new BJCheesePizza();
} else if (orderType.equals("pepper")) {
pizza = new BJPepperPizza();
}
return pizza;
}
}
class LDOrderPizzaFactory extends OrderPizzaFactory {
@Override
Pizza createPizza(String orderType) {
Pizza pizza = null;
if(orderType.equals("cheese")) {
pizza = new LDCheesePizza();
} else if (orderType.equals("pepper")) {
pizza = new LDPepperPizza();
}
return pizza;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
如果新增一个新的类如希腊披萨,则只需要新增两个类:希腊披萨类(继承 Pizza 类)、希腊披萨工厂类(继承 OrderPizzaFactory)。不会修改其他的代码,满足了 OCP 开闭原则,但是相比较简单工厂模式,花费的代码比较高。
# 什么时候该用工厂方法模式,而非简单工厂模式呢?
之所以将某个代码块剥离出来,独立为函数或者类,原因是这个代码块的逻辑过于复杂,剥离之后能让代码更加清晰,更加可读、可维护。但是,如果代码块本身并不复杂,就几行代码而已,我们完全没必要将它拆分成单独的函数或者类。
基于这个设计思想,当对象的创建逻辑比较复杂,不只是简单的 new 一下就可以,而是要组合其他类对象,做各种初始化操作的时候,我们推荐使用工厂方法模式,将复杂的创建逻辑拆分到多个工厂类中,让每个工厂类都不至于过于复杂。而使用简单工厂模式,将所有的创建逻辑都放到一个工厂类中,会导致这个工厂类变得很复杂。
除此之外,在某些场景下,如果对象不可复用,那工厂类 每次都要返回不同的对象。如果我们使用简单工厂模式来实现,就只能选择第一种包含 if 分支逻辑的实现方式。如果我们还想避免烦人的 if-else 分支逻辑,这个时候,我们就推荐使用工厂方法模式。
工厂方法模式可以理解为在多个简单工厂模式(子工厂)的基础上再创建一个大的工厂,统一管理多个子工厂。
复杂度 | 优势模式 |
---|---|
结构复杂度 | 简单工厂模式 |
代码复杂度 | 简单工厂模式 |
编程复杂度 | 简单工厂模式 |
管理复杂度 | 简单工厂模式 |
根据实际业务 | 简单工厂模式 |
根据设计原则 | 工厂方法模式 |
应用场景:
- 客户只知道创建产品的工厂名,而不知道具体的产品名。如 TCL 电视工厂、海信电视工厂等
- 创建对象的任务由多个具体子工厂中的某一个完成,而抽象工厂只提供创建产品的接口
- 客户不关心创建产品的细节,只关心产品的品牌
# 抽象工厂(Abstract Factory)
# 基本介绍
- 抽象工厂模式:定义了一个 interface 用于创建相关或有依赖关系的对象簇,而无需指明具体的类
- 抽象工厂模式可以将简单工厂模式和工厂方法模式进行整合,是工厂方法模式的升级版本,工厂方法模式只生产一个等级的产品,而抽象工厂模式可生产多个等级的产品
- 从设计层面看,抽象工厂模式就是对简单工厂模式的改进(或者称为进一步的抽象)
- 将工厂抽象成两层,AbsFactory(抽象工厂)和具体实现的工厂子类。程序员可以根据创建对象类型使用对应的工厂子类。这样将单个的简单工厂类变成了 工厂簇,更利于代码的维护和扩展
优点
除了具有工厂方法模式的优点外,还有:
- 可以在类的内部对产品族中相关联的多等级产品共同管理,而不必专门引入多个新的类来进行管理。
- 当需要产品族时,抽象工厂可以保证客户端始终只使用同一个产品的产品组。
- 抽象工厂增强了程序的可扩展性,当增加一个新的产品族时,不需要修改原代码,满足开闭原则。
主要缺点
当产品族中需要增加一个新的产品时,所有的工厂类都需要进行修改。增加了系统的抽象性和理解难度。
主要角色
- 抽象工厂(Abstract Factory):提供了创建产品的接口,它包含多个创建产品的方法 newProduct(),可以创建多个不同等级的产品
- 具体工厂(Concrete Factory):主要是实现抽象工厂中的多个抽象方法,完成具体产品的创建
- 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能,抽象工厂模式有多个抽象产品
- 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间是多对一的关系
抽象工厂结构图:
上面的披萨案例抽象工厂类图:
# 抽象工厂模式应用实例
换个案例:生产小米手机、小米路由器;华为手机、华为路由器。
小米手机和华为手机称为 产品线,小米手机和小米路由器称为 产品簇。
首先创建手机和路由器的接口类:(为了方便,放在一个代码块里)
// 手机接口
public interface IPhoneProduct {
void start();
void shutdown();
void sendMes();
void call();
}
// 路由器接口
public interface IRouterProduct {
void start();
void shutdown();
void setting();
void link();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
创建小米和华为的手机:(为了方便,放在一个代码块里)
// 小米手机
public class XiaoMiPhone implements IPhoneProduct {
@Override
public void start() {
System.out.println("打开小米手机");
}
@Override
public void shutdown() {
System.out.println("关闭小米手机");
}
@Override
public void sendMes() {
System.out.println("小米手机发送消息");
}
@Override
public void call() {
System.out.println("小米手机打电话");
}
}
// 华为手机
public class HuaWeiPhone implements IPhoneProduct{
@Override
public void start() {
System.out.println("打开华为手机");
}
@Override
public void shutdown() {
System.out.println("关闭华为手机");
}
@Override
public void sendMes() {
System.out.println("华为手机发送消息");
}
@Override
public void call() {
System.out.println("华为手机打电话");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
创建小米和华为的路由器:(为了方便,放在一个代码块里)
// 小米路由器
public class XiaoMiRouter implements IRouterProduct{
@Override
public void start() {
System.out.println("打开小米路由器");
}
@Override
public void shutdown() {
System.out.println("关闭小米路由器");
}
@Override
public void setting() {
System.out.println("设置小米路由器");
}
@Override
public void link() {
System.out.println("连接小米路由器");
}
}
// 华为路由器
public class HuaWeiMiRouter implements IRouterProduct{
@Override
public void start() {
System.out.println("打开华为路由器");
}
@Override
public void shutdown() {
System.out.println("关闭华为路由器");
}
@Override
public void setting() {
System.out.println("设置华为路由器");
}
@Override
public void link() {
System.out.println("连接华为路由器");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
创建抽象工厂和子工厂(小米工厂、华为工厂)(为了方便,放在一个代码块里)
// 抽象工厂
public interface IProductFactory {
IPhoneProduct phoneProduct();
IRouterProduct routerProduct();
}
// 小米工厂
public class XiaoMiFactory implements IProductFactory{
@Override
public IPhoneProduct phoneProduct() {
return new XiaoMiPhone();
}
@Override
public IRouterProduct routerProduct() {
return new XiaoMiRouter();
}
}
// 华为工厂
public class HuaWeiFactory implements IProductFactory{
@Override
public IPhoneProduct phoneProduct() {
return new HuaWeiPhone();
}
@Override
public IRouterProduct routerProduct() {
return new HuaWeiMiRouter();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
测试抽象工厂模式:
public class Client {
public static void main(String[] args) {
System.out.println("-------------- 小米系列工厂 --------------");
XiaoMiFactory xiaoMiFactory = new XiaoMiFactory();
IPhoneProduct iPhoneProduct = xiaoMiFactory.phoneProduct();
iPhoneProduct.start();
iPhoneProduct.call();
iPhoneProduct.sendMes();
iPhoneProduct.shutdown();
IRouterProduct iRouterProduct = xiaoMiFactory.routerProduct();
iRouterProduct.start();
iRouterProduct.link();
iRouterProduct.shutdown();
iRouterProduct.setting();
System.out.println("-------------- 华为系列工厂 --------------");
HuaWeiFactory huaWeiFactory = new HuaWeiFactory();
IPhoneProduct iPhoneProduct1 = huaWeiFactory.phoneProduct();
iPhoneProduct1.start();
iPhoneProduct1.call();
iPhoneProduct1.sendMes();
iPhoneProduct1.shutdown();
IRouterProduct iRouterProduct1 = huaWeiFactory.routerProduct();
iRouterProduct1.start();
iRouterProduct1.link();
iRouterProduct1.shutdown();
iRouterProduct1.setting();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 工厂模式在 JDK-Calendar 应用的源码分析
JDK 中的 Calendar 类中,就使用了简单工厂模式。
public class Factory {
public static void main(String[] args) {
// getInstance 是 Calendar 静态方法
Calendar cal = Calendar.getInstance();
// 注意月份下标从 0 开始,所以取月份要+1
System.out.println("年:" + cal.get(Calendar.YEAR));
System.out.println("月:" + (cal.get(Calendar.MONTH) + 1));
System.out.println("日:" + cal.get(Calendar.DAY_OF_MONTH));
System.out.println("时:" + cal.get(Calendar.HOUR_OF_DAY));
System.out.println("分:" + cal.get(Calendar.MINUTE));
System.out.println("秒:" + cal.get(Calendar.SECOND));
}
}
2
3
4
5
6
7
8
9
10
11
12
13
Calendar.java
public class Calendar {
public static Calendar getInstance() {
return createCalendar(TimeZone.getDefault(), Locale.getDefault(Locale.Category.FORMAT));
}
private static Calendar createCalendar(TimeZone zone,Locale aLocale) { // 根据 TimeZone zone, locale 创建对应的实例
CalendarProvider provider = LocaleProviderAdapter.getAdapter(CalendarProvider.class, aLocale)
.getCalendarProvider();
if (provider != null) {
try {
return provider.getInstance(zone, aLocale);
} catch (IllegalArgumentException iae) {
// fall back to the default instantiation
}
}
Calendar cal = null;
if (aLocale.hasExtensions()) {
String caltype = aLocale.getUnicodeLocaleType("ca");
if (caltype != null) {
switch (caltype) {
case "buddhist":
cal = new BuddhistCalendar(zone, aLocale);
break;
case "japanese":
cal = new JapaneseImperialCalendar(zone, aLocale);
break;
case "gregory":
cal = new GregorianCalendar(zone, aLocale);
break;
}
}
}
if (cal == null) {
// If no known calendar type is explicitly specified
// perform the traditional way to create a Calendar:
// create a BuddhistCalendar for th_TH locale
// a JapaneseImperialCalendar for ja_JP_JP locale, or
// a GregorianCalendar for any other locales
// NOTE: The language, country and variant strings are interned.
if (aLocale.getLanguage() == "th" && aLocale.getCountry() == "TH") {
cal = new BuddhistCalendar(zone, aLocale);
} else if (aLocale.getVariant() == "JP" && aLocale.getLanguage() == "ja" && aLocale.getCountry() == "JP") {
cal = new JapaneseImperialCalendar(zone, aLocale);
} else {
cal = new GregorianCalendar(zone, aLocale);
}
}
return cal;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# 工厂模式小结
- 工厂模式的意义将实例化对象的代码提取出来,放到一个类中统一管理和维护,达到和主项目的依赖关系的解耦。从而提高项目的扩展和维护性
- 三种工厂模式 (简单工厂模式、工厂方法模式、抽象工厂模式)
- 设计模式的依赖抽象原则
- 创建对象实例时,不要直接 new 类, 而是把这个 new 类的动作放在一个工厂的方法中,并返回。有的书上说,变量不要直接持有具体类的引用
- 不要让类继承具体类,而是继承抽象类或者是实现 interface(接口)
- 不要覆盖基类中已经实现的方法
# 如何设计实现一个Dependency Injection框架
当创建对象是一个「大工程」的时候,我们一般会选择使用工厂模式,来封装对象复杂的创建过程,将对象的创建和使用分离,让代码更加清晰。那何为「大工程」呢?上面我们讲了两种情况,一种是创建过程涉及复杂的 if-else 分支判断,另一种是对象创建需要组装多个其他类对象或者需要复杂的初始化过程。
我们来学习一个创建对象的「大工程」,依赖注入框架,或者叫依赖注入容器(Dependency Injection Container),简称 DI 容器。在今天的讲解中,我会带你一块搞清楚这样几个问题:DI 容器跟我们讲的工厂模式又有何区别和联系?DI 容器的核心功能有哪些,以及如何实现一个简单的 DI 容器?
# 工厂模式和 DI 容器有何区别
实际上,DI 容器底层最基本的设计思路就是基于工厂模式的。DI 容器相当于一个大的工厂类,负责在程序启动的时候,根据配置(要创建哪些类对象,每个类对象的创建需要依赖哪些其他类对象)事先创建好对象。当应用程序需要使用某个类对象的时候,直接从容器中获取即可。正是因为它持有一堆对象,所以这个框架才被称为「容器」。
DI 容器相对于我们上面讲的工厂模式的例子来说,它处理的是更大的对象创建工程。上面讲的工厂模式中,一个工厂类只负责某个类对象或者某一组相关类对象(继承自同一抽象类或者接口的子类)的创建,而 DI 容器负责的是整个应用中所有类对象的创建。
除此之外,DI 容器负责的事情要比单纯的工厂模式要多。比如,它还包括配置的解析、对象生命周期的管理。接下来,我们就详细讲讲,一个简单的 DI 容器应该包含哪些核心功能。
# DI 容器的核心功能有哪些
总结一下,一个简单的 DI 容器的核心功能一般有三个:配置解析、对象创建和对象生命周期管理。
首先,我们来看配置解析。
- 在上面讲的工厂模式中,工厂类要创建哪个类对象是事先确定好的,并且是写死在工厂类代码中的。作为一个通用的框架来说,框架代码跟应用代码应该是高度解耦的,DI 容器事先并不知道应用会创建哪些对象,不可能把某个应用要创建的对象写死在框架代码中。所以,我们需要通过一种形式,让应用告知 DI 容器要创建哪些对象。这种形式就是我们要讲的配置
- 我们将需要由 DI 容器来创建的类对象和创建类对象的必要信息(使用哪个构造函数以及对应的构造函数参数都是什么等等),放到配置文件中。容器读取配置文件,根据配置文件提供的信息来创建对象
- 下面是一个典型的 Spring 容器的配置文件。Spring 容器读取这个配置文件,解析出要创建的两个对象:rateLimiter 和 redisCounter,并且得到两者的依赖关系:rateLimiter 依赖 redisCounter
public class RateLimiter {
private RedisCounter redisCounter;
public RateLimiter(RedisCounter redisCounter) {
this.redisCounter = redisCounter;
}
public void test() {
System.out.println("Hello World!");
}
// ...
}
public class RedisCounter {
private String ipAddress;
private int port;
public RedisCounter(String ipAddress, int port) {
this.ipAddress = ipAddress;
this.port = port;
}
// ...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
配置文件 beans.xml:
<beans>
<bean id="rateLimiter" class="cn.youngkbt.RateLimiter">
<constructor-arg ref="redisCounter"/>
</bean>
<bean id="redisCounter" class="cn.youngkbt.redisCounter">
<constructor-arg type="String" value="127.0.0.1"/>
<constructor-arg type="int" value=1234/>
</bean>
</beans>
2
3
4
5
6
7
8
9
10
其次,我们再来看对象创建。
在 DI 容器中,如果我们给每个类都对应创建一个工厂类,那项目中类的个数会成倍增加,这会增加代码的维护成本。要解决这个问题并不难。我们只需要将所有类对象的创建都放到一个工厂类中完成就可以了,比如 BeansFactory。
你可能会说,如果要创建的类对象非常多,BeansFactory 中的代码会不会线性膨胀(代码量跟创建对象的个数成正比)呢?实际上并不会。待会讲到 DI 容器的具体实现的时候,我们会讲「反射」这种机制,它能在程序运行的过程中,动态地加载类、创建对象,不需要事先在代码中写死要创建哪些对象。所以,不管是创建一个对象还是十个对象,BeansFactory 工厂类代码都是一样的。
最后,我们来看对象的生命周期管理。
上面我们讲到,简单工厂模式有两种实现方式,一种是每次都返回新创建的对象,另一种是每次都返回同一个事先创建好的对象,也就是所谓的单例对象。在 Spring 框架中,我们可以通过配置 scope 属性,来区分这两种不同类型的对象。scope=prototype
表示返回新创建的对象,scope=singleton
表示返回单例对象。
除此之外,我们还可以配置对象是否支持懒加载。如果 lazy-init=true
,对象在真正被使用到的时候(比如:BeansFactory.getBean("userService")
)才被被创建;如果 lazy-init=false
,对象在应用启动的时候就事先创建好。
不仅如此,我们还可以配置对象的 init-method
和 destroy-method
方法,比如 init-method=loadProperties()
,destroy-method=updateConfigFile()
。DI 容器在创建好对象之后,会主动调用 init-method
属性指定的方法来初始化对象。在对象被最终销毁之前,DI 容器会主动调用 destroy-method
属性指定的方法来做一些清理工作,比如释放数据库连接池、关闭文件。
# 如何实现一个简单的 DI 容器?
用 Java 语言来实现一个简单的 DI 容器,核心逻辑只需要包括这样两个部分:配置文件解析、根据配置文件通过「反射」语法来创建对象。
# 最小原型设计
因为我们主要是讲解设计模式,所以,在今天的讲解中,我们只实现一个 DI 容器的最小原型。像 Spring 框架这样的 DI 容器,它支持的配置格式非常灵活和复杂。为了简化代码实现,重点讲解原理,在最小原型中,我们只支持下面配置文件中涉及的配置语法。
配置文件 beans.xml
<beans>
<bean id="rateLimiter" class="cn.youngkbt.RateLimiter">
<constructor-arg ref="redisCounter"/>
</bean>
<bean id="redisCounter" class="cn.youngkbt.redisCounter" scope="singleton" lazy-init="true">
<constructor-arg type="String" value="127.0.0.1"/>
<constructor-arg type="int" value=1234/>
</bean>
</beans>
2
3
4
5
6
7
8
9
10
最小原型的使用方式跟 Spring 框架非常类似,示例代码如下所示:
public class Demo {
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");
RateLimiter rateLimiter = (RateLimiter) applicationContext.getBean("rateLimiter");
rateLimiter.test();
// ...
}
}
2
3
4
5
6
7
8
# 提供执行入口
面向对象设计的最后一步是:组装类并提供执行入口。在这里,执行入口就是一组暴露给外部使用的接口和类。通过刚刚的最小原型使用示例代码,我们可以看出,执行入口主要包含两部分:ApplicationContext
和 ClassPathXmlApplicationContext
。其中 ApplicationContext
是接口,ClassPathXmlApplicationContext
是接口的实现类。两个类具体实现如下所示:
public interface ApplicationContext {
Object getBean(String beanId);
}
public class ClassPathXmlApplicationContext implements ApplicationContext {
private BeansFactory beansFactory;
private BeanConfigParser beanConfigParser;
public ClassPathXmlApplicationContext(String configLocation) {
this.beansFactory = new BeansFactory();
this.beanConfigParser = new XmlBeanConfigParser();
loadBeanDefinitions(configLocation);
}
private void loadBeanDefinitions(String configLocation) {
InputStream in = null;
try {
in = this.getClass().getResourceAsStream("/" + configLocation);
if (in == null) {
throw new RuntimeException("Can not find config file: " + configLocation);
}
List<BeanDefinition> beanDefinitions = beanConfigParser.parse(in);
beansFactory.addBeanDefinitions(beanDefinitions);
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
// TODO: log error
}
}
}
}
@Override
public Object getBean(String beanId) {
return beansFactory.getBean(beanId);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
从上面的代码中,我们可以看出,ClassPathXmlApplicationContext 负责组装 BeansFactory 和 BeanConfigParser 两个类,串联执行流程:从 classpath 中加载 XML 格式的配置文件,通过 BeanConfigParser 解析为统一的 BeanDefinition 格式,然后,BeansFactory 根据 BeanDefinition 来创建对象。
# 配置文件解析
配置文件解析主要包含 BeanConfigParser 接口和 XmlBeanConfigParser 实现类,负责将配置文件解析为 BeanDefinition 结构,以便 BeansFactory 根据这个结构来创建对象。配置文件的解析比较繁琐,不涉及我们要讲的理论知识,不是我们讲解的重点,所以这里我只给出两个类的大致设计思路,并未给出具体的实现代码。如果感兴趣的话,你可以自行补充完整。具体的代码框架如下所示:
public interface BeanConfigParser {
List<BeanDefinition> parse(InputStream inputStream);
List<BeanDefinition> parse(String configContent);
}
public class XmlBeanConfigParser implements BeanConfigParser {
@Override
public List<BeanDefinition> parse(InputStream inputStream) {
String content = null;
// TODO:...
return parse(content);
}
@Override
public List<BeanDefinition> parse(String configContent) {
List<BeanDefinition> beanDefinitions = new ArrayList<>();
// TODO:...
return beanDefinitions;
}
}
public class BeanDefinition {
private String id;
private String className;
private List<ConstructorArg> constructorArgs = new ArrayList<>();
private Scope scope = Scope.SINGLETON;
private boolean lazyInit = false;
// 省略必要的 getter/setter/constructors
public boolean isSingleton() {
return scope.equals(Scope.SINGLETON);
}
public static enum Scope {
SINGLETON,
PROTOTYPE
}
public static class ConstructorArg {
private boolean isRef;
private Class type;
private Object arg;
// 省略必要的 getter/setter/constructors
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# 核心工厂类设计
- 最后,我们来看,BeansFactory 是如何设计和实现的。这也是我们这个 DI 容器最核心的一个类了。它负责根据从配置文件解析得到的 BeanDefinition 来创建对象
- 如果对象的 scope 属性是 singleton,那对象创建之后会缓存在 singletonObjects 这样一个 map 中,下次再请求此对象的时候,直接从 map 中取出返回,不需要重新创建。如果对象的 scope 属性是 prototype,那每次请求对象,BeansFactory 都会创建一个新的对象返回
- 实际上,BeansFactory 创建对象用到的主要技术点就是 Java 中的反射语法:一种动态加载类和创建对象的机制。我们知道,JVM 在启动的时候会根据代码自动地加载类、创建对象。至于都要加载哪些类、创建哪些对象,这些都是在代码中写死的,或者说提前写好的。但是,如果某个对象的创建并不是写死在代码中,而是放到配置文件中,我们需要在程序运行期间,动态地根据配置文件来加载类、创建对象,那这部分工作就没法让 JVM 帮我们自动完成了,我们需要利用 Java 提供的反射语法自己去编写代码
搞清楚了反射的原理,BeansFactory 的代码就不难看懂了。具体代码实现如下所示:
public class BeansFactory {
private ConcurrentHashMap<String, Object> singletonObjects = new ConcurrentHashMap<>();
private ConcurrentHashMap<String, BeanDefinition> beanDefinitions = new ConcurrentHashMap<>();
public void addBeanDefinitions(List<BeanDefinition> beanDefinitionList) {
for (BeanDefinition beanDefinition : beanDefinitionList) {
this.beanDefinitions.putIfAbsent(beanDefinition.getId(), beanDefinition);
}
for (BeanDefinition beanDefinition : beanDefinitionList) {
if (beanDefinition.isLazyInit() == false && beanDefinition.isSingleton()) {
createBean(beanDefinition);
}
}
}
public Object getBean(String beanId) {
BeanDefinition beanDefinition = beanDefinitions.get(beanId);
if (beanDefinition == null) {
throw new NoSuchBeanDefinitionException("Bean is not defined: " + beanId);
}
return createBean(beanDefinition);
}
@VisibleForTesting
protected Object createBean(BeanDefinition beanDefinition) {
if (beanDefinition.isSingleton() && singletonObjects.contains(beanDefinition.getId())) {
return singletonObjects.get(beanDefinition.getId());
}
Object bean = null;
try {
Class beanClass = Class.forName(beanDefinition.getClassName());
List<BeanDefinition.ConstructorArg> args = beanDefinition.getConstructorArgs();
if (args.isEmpty()) {
bean = beanClass.newInstance();
} else {
Class[] argClasses = new Class[args.size()];
Object[] argObjects = new Object[args.size()];
for (int i = 0; i < args.size(); ++i) {
BeanDefinition.ConstructorArg arg = args.get(i);
if (!arg.getIsRef()) {
argClasses[i] = arg.getType();
argObjects[i] = arg.getArg();
} else {
BeanDefinition refBeanDefinition = beanDefinitions.get(arg.getArg());
if (refBeanDefinition == null) {
throw new NoSuchBeanDefinitionException("Bean is not defined: " + arg.getArg());
}
argClasses[i] = Class.forName(refBeanDefinition.getClassName());
argObjects[i] = createBean(refBeanDefinition);
}
}
bean = beanClass.getConstructor(argClasses).newInstance(argObjects);
}
} catch (ClassNotFoundException | IllegalAccessException
| InstantiationException | NoSuchMethodException | InvocationTargetException e) {
throw new BeanCreationFailureException("", e);
}
if (bean != null && beanDefinition.isSingleton()) {
singletonObjects.putIfAbsent(beanDefinition.getId(), bean);
return singletonObjects.get(beanDefinition.getId());
}
return bean;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
- 执行入口那里调用
addBeanDefinitions
- 然后
addBeanDefinitions
再调用createBean
利用反射创建对象,如果对象的 scope 属性是 singleton,那对象创建之后会缓存在singletonObjects
这样一个 Map 中 - 最后最小原型设计那里再调用
getBean从singletonObjects
获取对象