抽象工厂方法模式

1 实际场景

1.1 组装电脑

设想一个组装电脑的场景,在组装电脑时,需要选着一些配件,如:CPU、硬盘、内存、主板、电源、机箱等。为降低复杂度,目前只考虑 CPU 和主板。

事实上,我们在选择 CPU 的时候,面临一系列的问题,比如:品牌、型号、针脚数目、主频等问题,只有把这些都确定下来,才能确定具体的 CPU。同样,在选择主板的时候,也有一系列的问题,比如:品牌、芯片组、集成芯片、总线频率等问题,也只有这些都确定了,才能确定具体的主板。

在最终确定这个装机方案之前,还需要整体考虑各个配件之间的兼容性,比如:CPU 和主板,如果 CPU 针脚数和主板提供的 CPU 插口不兼容,是无法组装的。也就是说,装机方案是有整体性的,里面选择的各个配件之间是有关联的

对于装机工程师而言,他只知道组装一台电脑,需要相应的配件,但是具体使用什么样的配件,还得由客户说了算。也就是说装机工程师只是负责组装,而客户负责选择装配所需要的具体的配件。因此,当装机工程师为不同的客户组装电脑时,只需要按照客户的装机方案,去获取相应的配件,然后组装即可。

如何利用代码模拟选择配件进行装机的过程?

1.2 非抽象工厂的解决方案

考虑客户的功能,需要选择自己需要的 CPU 和主板,然后告诉装机工程师自己的选择,接下来就等着装机工程师组装机器了。

对装机工程师而言,只是知道 CPU 和主板的接口,而不知道具体实现,很明显可以用上简单工厂或工厂方法模式,为了简单,这里选用简单工厂。客户告诉装机工程师自己的选择,然后装机工程师会通过相应的工厂去获取相应的实例对象。

1、cpu 和主板的接口定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* @Auther mark
* @Version CpuApi.java v1.0 2023/1/7 22:42 Exp $
* @Description CPU的接口
*/
public interface CpuApi {
/**
* 示意方法,CPU具有运算功能
*/
void calculate();
}

/**
* @Auther mark
* @Version MainboardApi.java v1.0 2023/1/7 22:43 Exp $
* @Description 主板的接口
*/
public interface MainboardApi {
/**
* 示意方法,主板可以对接CPU
*/
void installCpu();
}

2、两个品牌的 cpu 实现

1
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
/**
* @Auther mark
* @Version IntelCpu.java v1.0 2023/1/7 22:45 Exp $
* @Description Intel cpu的实现
*/
public class IntelCpu implements CpuApi {
/**
* cpu针脚数目
*/
private int pins = 0;

public IntelCpu(int pins) {
this.pins = pins;
}

@Override
public void calculate() {
System.out.println("now in intel cpu, pins=" + pins);
}
}

/**
* @Auther mark
* @Version AmdCpu.java v1.0 2023/1/7 22:47 Exp $
* @Description amd cpu的实现
*/
public class AmdCpu implements CpuApi {
/**
* cpu针脚数
*/
private int pins = 0;

public AmdCpu(int pins) {
this.pins = pins;
}

@Override
public void calculate() {
System.out.println("now in amd cpu, pins=" + pins);
}
}

3、两个品牌的主板实现

1
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
/**
* @Auther mark
* @Version GAMainboard.java v1.0 2023/1/7 22:49 Exp $
* @Description 技嘉主板
*/
public class GAMainboard implements MainboardApi {
/**
* cpu 插槽数
*/
private int cpuHoles = 0;

public GAMainboard(int cpuHoles) {
this.cpuHoles = cpuHoles;
}

@Override
public void installCpu() {
System.out.println("now in GAMainboard, couHoles=" + cpuHoles);
}
}

/**
* @Auther mark
* @Version MSIMainboard.java v1.0 2023/1/7 22:50 Exp $
* @Description 微星的主板
*/
public class MSIMainboard implements MainboardApi {
private int cpuHoles = 0;

public MSIMainboard(int cpuHoles) {
this.cpuHoles = cpuHoles;
}

@Override
public void installCpu() {
System.out.println("now in MSIMainboard, cpuHoles=" + cpuHoles);
}
}

4、cpu 工厂实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* @Auther mark
* @Version CpuFactory.java v1.0 2023/1/7 22:53 Exp $
* @Description cpu简单工厂
*/
public class CpuFactory {

/**
* 创建CPU接口对象的方法
*
* @param type 选择CPU类型的参数
* @return CPU接口对象的方法
*/
public static CpuApi createCpuApi(int type) {
CpuApi cpu = null;
if (type == 1) {
cpu = new IntelCpu(1156);
} else if (type == 2) {
cpu = new AmdCpu(939);
}
return cpu;
}
}

5、主板工厂实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* @Auther mark
* @Version MainboardFactory.java v1.0 2023/1/7 22:56 Exp $
* @Description 主板的简单工厂
*/
public class MainboardFactory {
/**
* 创建主板接口对象的方法
*
* @param type 选择主板类型的参数
* @return 主板接口对象的方法
*/
public static MainboardApi createMainboardApi(int type) {
MainboardApi mainboard = null;
// 根据参数来选择并创建相应的主板对象
if (type == 1) {
mainboard = new GAMainboard(1156);
} else if (type == 2) {
mainboard = new MSIMainboard(939);
}
return mainboard;
}
}

6、装机工程师的实现

1
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
/**
* @Auther mark
* @Version ComputerEngineer.java v1.0 2023/1/7 23:01 Exp $
* @Description 装机工程师
*/
public class ComputerEngineer {
/**
* 定义组装机器需要的CPU
*/
private CpuApi cpu = null;
/**
* 定义组装机器需要的主板
*/
private MainboardApi mainboard = null;

/**
* 装机过程
*
* @param cpuType 客户选择所需cpu的类型
* @param mainboardType 客户选择所需主板的类型
*/
public void assembleComputer(int cpuType, int mainboardType) {
// 1. 准备好装机所需要的配件
prepareHardwares(cpuType, mainboardType);
// 2. 组装机器
// 3. 测试机器
// 4. 交付客户
}

private void prepareHardwares(int cpuType, int mainboardType) {
// 这里要去准备CPU和主板的具体实现,为了示例简单,这里只准备这两个
// 可是,装机工程师并不知道如何去创建,怎么办呢?

// 直接找相应的工厂获取
this.cpu = CpuFactory.createCpuApi(cpuType);
this.mainboard = MainboardFactory.createMainboardApi(mainboardType);
// 测试配件是否好用
this.cpu.calculate();
this.mainboard.installCpu();
}
}

7、客户端实现

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @Auther mark
* @Version Client.java v1.0 2023/1/7 23:09 Exp $
* @Description
*/
public class Client {
public static void main(String[] args) {
// 实例化装机工程师对象
ComputerEngineer engineer = new ComputerEngineer();
// 客户告诉装机工程师配件方案,装机工程师按要求进行组装
engineer.assembleComputer(1, 1);
}
}

1.3 有何问题

虽然上面的实现,通过简单工厂解决解决了:对于装机工程师无需感知 CPU 和主板的具体实现的问题。但还有一个问题没有解决:没有解决 CPU 对象与主板对象之间的关系匹配问题。而在上面的实现中,并没有维护这种关联关系,若客户随意选择 CPU 和主板的类型,可能导致针脚不配位的问题。

如何维护对象之间的关系?

2 抽象工厂模式

2.1 定义

抽象工厂模式(Abstract Factory Pattern)属于类创建型模式。提供一个创建一系列相关或者相互依赖对象的接口,而无需指定具体的类。

2.2 抽象工厂模式解决问题思路

上述场景中,实际有两个问题:

  1. 对于使用者而言,只需要一系列对象的接口,而无需了解对象的具体实现;
  2. 对于这些对象是相关的或者说相互依赖,即:创建接口的对象还需要约束这些对象之间的关系;

需要明确简单工厂模式、工厂方法模式无法解决上述问题的关键所在:简单工厂和工厂方法模式关注的单个对象的创建,如:CPU 如何创建,主板如何创建。无法解决对象之间的关联关系。

解决这个问题的一个解决方案就是抽象工厂模式。在这个模式里面,会定义一个抽象工厂,在里面虚拟的创建客户端需要的这一系列对象,所谓虚拟的就是定义创建这些对象的抽象方法,并不去真的实现,然后由具体的抽象工厂的子类来提供这一系列对象的创建。这样一来可以为同一个抽象工厂提供很多不同的实现,那么创建的这一系列对象也就不一样了,也就是说,抽象工厂在这里起到一个约束的作用,并提供所有子类的一个统一外观,来让客户端使用

2.3 结构说明

image-20230107235250666

AbstractFactory:抽象工厂,定义创建一系列产品对象的操作接口;
ConcreteFactory:具体的工厂,实现抽象工厂定义的方法,具体操作一系列产品对象的创建;
AbstractProduct:定义一类产品对象的接口;
ConcreProduct:具体的产品实现对象,通常在具体工厂里面,会选择具体的产品实现对象,来创建符合抽象工厂定义的方法返回的产品类型的对象;
Client:客户端,主要使用抽象工厂来获取一系列所需要的产品对象,然后面向这些产品对象的接口编程,以实现需要的功能;

2.4 示例代码

1、抽象工厂的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* @Auther mark
* @Version AbstractFactory.java v1.0 2023/1/8 00:01 Exp $
* @Description 抽象工厂的接口,声明创建抽象产品对象的操作
*/
public interface AbstractFactory {
/**
* 示例方法,创建抽象产品A的对象
*
* @return 抽象产品A的对象
*/
public AbstractProductA createProductA();

/**
* 示例方法,创建抽象产品B的对象
*
* @return 抽象产品B的对象
*/
public AbstractProductB createProductB();
}

2、产品定义示意代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @Auther mark
* @Version AbstractProductA.java v1.0 2023/1/8 00:01 Exp $
* @Description 抽象产品A的接口
*/
public interface AbstractProductA {
//定义抽象产品A相关的操作
}

/**
* @Auther mark
* @Version AbstractProductB.java v1.0 2023/1/8 00:02 Exp $
* @Description 抽象产品B的接口
*/
public interface AbstractProductB {
//定义抽象产品B相关的操作
}

3、具体产品的实现定义

1
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
/**
* @Auther mark
* @Version ProductA1.java v1.0 2023/1/8 00:03 Exp $
* @Description 产品A的具体实现
*/
public class ProductA1 implements AbstractProductA {
//实现产品A的接口中定义的操作
}

/**
* @Auther mark
* @Version ProductA2.java v1.0 2023/1/8 00:04 Exp $
* @Description 产品A的具体实现
*/
public class ProductA2 implements AbstractProductA {
// //实现产品A的接口中定义的操作
}

/**
* @Auther mark
* @Version ProductB1.java v1.0 2023/1/8 00:05 Exp $
* @Description 产品B的具体实现
*/
public class ProductB1 implements AbstractProductB {
//实现产品B的接口中定义的操作
}

/**
* @Auther mark
* @Version ProductB2.java v1.0 2023/1/8 00:05 Exp $
* @Description 产品B的具体实现
*/
public class ProductB2 implements AbstractProductB {
//实现产品B的接口中定义的操作
}

4、具体工厂的实现 - 维护了不同对象之间的关系

1
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
/**
* @Auther mark
* @Version ConcreteFactory1.java v1.0 2023/1/8 00:06 Exp $
* @Description 具体的工厂实现对象,实现创建具体的产品对象的操作
*/
public class ConcreteFactory1 implements AbstractFactory {
@Override
public AbstractProductA createProductA() {
return new ProductA1();
}

@Override
public AbstractProductB createProductB() {
return new ProductB1();
}
}

/**
* @Auther mark
* @Version ConcreteFactory2.java v1.0 2023/1/8 00:07 Exp $
* @Description 具体的工厂实现对象,实现创建具体的产品对象的操作
*/
public class ConcreteFactory2 implements AbstractFactory {
@Override
public AbstractProductA createProductA() {
return new ProductA2();
}

@Override
public AbstractProductB createProductB() {
return new ProductB2();
}
}

5、客户端实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @Auther mark
* @Version FactoryClient.java v1.0 2023/1/8 00:08 Exp $
* @Description
*/
public class FactoryClient {
public static void main(String[] args) {
//创建抽象工厂对象
AbstractFactory factory = new ConcreteFactory1();
//通过抽象工厂来获取一系列的对象,如产品A和产品B
factory.createProductA();
factory.createProductB();
}
}

2.5 重写方案

使用抽象工厂模式重写前面的例子:装机工程师要组装电脑对象,需要一系列的产品对象,比如 CPU、主板等,于是创建一个抽象工厂给装机工程师使用,在这个抽象工厂里面定义抽象的创建 CPU 和主板的方法,这个抽象工厂就相当于一个抽象的装机方案,在这个装机方案里面,各个配件是能够相互匹配的

image-20230108001856205

1、CPU 与主板的接口定于以及实现同上

2、抽象工厂定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @Auther mark
* @Version AbstractFactory.java v1.0 2023/1/8 00:20 Exp $
* @Description 抽象工厂的接口,声明创建抽象产品对象的操作
*/
public interface AbstractFactory {
/**
* 创建CPU的对象
* @return CPU的对象
*/
public CpuApi createCpuApi();
/**
* 创建主板的对象
* @return 主板的对象
*/
public MainboardApi createMainboardApi();
}

3、装机方案的实现

1
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
/**
* @Auther mark
* @Version Schema1.java v1.0 2023/1/8 00:21 Exp $
* @Description 装机方案一:Intel 的CPU + 技嘉的主板
* 这里创建CPU和主板对象的时候,是对应的,能匹配上的
*/
public class Schema1 implements AbstractFactory {
@Override
public CpuApi createCpuApi() {
return new IntelCpu(1156);
}

@Override
public MainboardApi createMainboardApi() {
return new GAMainboard(1156);
}
}

/**
* @Auther mark
* @Version Schema2.java v1.0 2023/1/8 00:21 Exp $
* @Description 装机方案二:AMD的CPU + 微星的主板
* 这里创建CPU和主板对象的时候,是对应的,能匹配上的
*/
public class Schema2 implements AbstractFactory {
@Override
public CpuApi createCpuApi() {
return new AmdCpu(939);
}

@Override
public MainboardApi createMainboardApi() {
return new MSIMainboard(939);
}
}

4、装机工程师的实现。

装机工程师相当于使用抽象工厂的客户端,虽然是由真正的客户来选择和创建具体的工厂对象,但是使用抽象工厂的是装机工程师对象。

与前面的实现相比,主要的变化是:从客户端,不再传入选择 CPU 和主板的参数,而是直接传入客户选择并创建好的装机方案对象。这样就避免了单独去选择 CPU 和主板,客户要选就是一套,就是一个系列

1
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
/**
* @Auther mark
* @Version ComputerEngineer.java v1.0 2023/1/8 00:23 Exp $
* @Description 装机工程师
*/
public class ComputerEngineer {
/**
* 定义组装机器需要的CPU
*/
private CpuApi cpu = null;
/**
* 定义组装机器需要的主板
*/
private MainboardApi mainboard = null;

/**
* 装机过程
*
* @param schema 客户选择的装机方案
*/
public void assembleComputer(AbstractFactory schema) {
//1:首先准备好装机所需要的配件
prepareHardwares(schema);
//2:组装机器
//3:测试机器
//4:交付客户
}

/**
* 准备装机所需要的配件
*
* @param schema 客户选择的装机方案
*/
private void prepareHardwares(AbstractFactory schema) {
//这里要去准备CPU和主板的具体实现,为了示例简单,这里只准备这两个
//可是,装机工程师并不知道如何去创建,怎么办呢?

//使用抽象工厂来获取相应的接口对象
this.cpu = schema.createCpuApi();
this.mainboard = schema.createMainboardApi();

//测试一下配件是否好用
this.cpu.calculate();
this.mainboard.installCpu();
}
}

5、客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @Auther mark
* @Version Client.java v1.0 2023/1/8 00:25 Exp $
* @Description
*/
public class Client {
public static void main(String[] args) {
//创建装机工程师对象
ComputerEngineer engineer = new ComputerEngineer();
//客户选择并创建需要使用的装机方案对象
AbstractFactory schema = new Schema1();
//告诉装机工程师自己选择的装机方案,让装机工程师组装电脑
engineer.assembleComputer(schema);
}
}

如同前面的示例,定义了一个抽象工厂 AbstractFactory,在里面定义了创建 CPU 和主板对象的接口的方法,但是在抽象工厂里面,并没有指定具体的 CPU 和主板的实现,也就是无须指定它们具体的实现类。

CPU 和主板是相关的对象,是构建电脑的一系列相关配件,这个抽象工厂就相当于一个装机方案,客户选择装机方案的时候,一选就是一套,CPU 和主板是确定好的,不让客户分开选择,这就避免了出现不匹配的错误。

3 模式讲解

3.1 认识抽象工厂模式

3.1.1 功能

抽象工厂的功能是为一系列相关对象或相互依赖的对象创建一个接口,一定要注意,这个接口内的方法不是任意堆砌的,而是一系列相关或相互依赖的方法,比如上面例子中的 CPU 和主板,都是为了组装一台电脑的相关对象。

从某种意义上看,抽象工厂其实是一个产品系列,或者是产品簇。上面例子中的抽象工厂就可以看成是电脑簇,每个不同的装机方案,代表一种具体的电脑系列。

3.1.2 抽象工厂为接口

AbstractFactoryJava 中通常实现成为接口,不要被名称所误导,以为是实现成为抽象类,当然,如果需要为这个产品簇提供公共的功能,也不是不可以把 AbstractFactory 实现成为抽象类,但一般不这么做。

关于抽象类与接口选择,可以参考文章

3.1.3 使用工厂方法

AbstractFactory 定义了创建产品所需要的接口,具体的实现是在实现类里面,通常在实现类里面就需要选择多种更具体的实现,所以 AbstractFactory 定义的创建产品的方法可以看成是工厂方法,而这些工厂方法的具体实现就延迟到了具体的工厂里面。也就是说使用工厂方法来实现抽象工厂

3.1.4 产品簇

由于抽象工厂定义的一系列对象,通常是相关或者相依赖的,这些产品对象就构成了一个产品簇,也就是抽象工厂定义了一个产品簇。这就带来非常大的灵活性,切换一个产品簇的时候,只要提供不同的抽象工厂实现就好了,也就是说现在是以产品簇做为一个整体被切换

3.2 定义可扩展的工厂

前面组装电脑的示例中,抽象工厂为每一种对象,如 CPU、主板都定义了相应的方法。这有个不灵活的地方,若产品簇中要增加一个对象,所有的具体实现工厂都需要随之改变。

可以有一种不安全(类型强制转换)但是相对灵活的改进方法:将抽象工厂的方法设置为一个带参数的方法,通过参数来判断具体创建何种产品对象;由于只有一个方法,在返回类型上就不能是具体的某个产品类型,只能是某个产品簇的父类或者接口,又或者为 Object 类型。示例代码如下:

1、抽象工厂方法改造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @Auther mark
* @Version AbstractFactory.java v1.0 2023/1/8 21:10 Exp $
* @Description 可扩展的抽象工厂的接口
*/
public interface AbstractFactory {
/**
* 一个通用的创建产品对象的方法,为了简单,直接返回Object
* 也可以为所有被创建的产品定义一个公共的接口
*
* @param type 具体创建的产品类型标识
* @return 创建出的产品对象
*/
public Object createProduct(int type);
}

这里要特别注意传入 createProduct 的参数所代表的含义,这个参数只是用来标识现在是在创建什么类型的产品,比如标识现在是创建 CPU 还是创建主板,一般这个 type 的含义到此就结束了,不再进一步表示具体是什么样的 CPU 或具体什么样的主板,也就是说 type 不再表示具体是创建 Intel 的 CPU 还是创建 AMD 的 CPU,这就是一个参数所代表的含义的深度的问题,要注意。虽然也可以延伸参数的含义到具体的实现上,但这不是可扩展工厂这种设计方式的本意,一般也不这么做。

2、CPU 和主板的实现与前面实例一致:CPU 分为 Intel 和 Amd,主板分为技嘉和微星

3、具体工厂实现

1
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
/**
* @Auther mark
* @Version Schema1.java v1.0 2023/1/8 21:14 Exp $
* @Description 装机方案一:Intel 的CPU + 技嘉的主板
* 这里创建CPU和主板对象的时候,是对应的,能匹配上的
*/
public class Schema1 implements AbstractFactory {
@Override
public Object createProduct(int type) {
Object retObj = null;
//type为1表示创建CPU,type为2表示创建主板
if(type==1){
retObj = new IntelCpu(1156);
}else if(type==2){
retObj = new GAMainboard(1156);
}
return retObj;
}
}

/**
* @Auther mark
* @Version Schema2.java v1.0 2023/1/8 21:15 Exp $
* @Description 装机方案二:AMD的CPU + 微星的主板
* 这里创建CPU和主板对象的时候,是对应的,能匹配上的
*/
public class Schema2 implements AbstractFactory {
@Override
public Object createProduct(int type) {
Object retObj = null;
//type为1表示创建CPU,type为2表示创建主板
if (type == 1) {
retObj = new AmdCpu(939);
} else if (type == 2) {
retObj = new MSIMainboard(939);
}
return retObj;
}
}

4、装机工程师的实现,拓展的基本实现与之前有所不同

1
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
/**
* @Auther mark
* @Version ComputerEngineer.java v1.0 2023/1/8 21:18 Exp $
* @Description 装机工程师
*/
public class ComputerEngineer {
/**
* 定义组装机器需要的CPU
*/
private CpuApi cpu = null;
/**
* 定义组装机器需要的主板
*/
private MainboardApi mainboard = null;

/**
* 装机过程
*
* @param schema 客户选择的装机方案
*/
public void assembleComputer(AbstractFactory schema) {
//1:首先准备好装机所需要的配件
prepareHardwares(schema);
//2:组装机器
//3:测试机器
//4:交付客户
}

/**
* 准备装机所需要的配件
*
* @param schema 客户选择的装机方案
*/
private void prepareHardwares(AbstractFactory schema) {
//这里要去准备CPU和主板的具体实现,为了示例简单,这里只准备这两个
//可是,装机工程师并不知道如何去创建,怎么办呢?

//使用抽象工厂来获取相应的接口对象
this.cpu = (CpuApi) schema.createProduct(1);
this.mainboard = (MainboardApi) schema.createProduct(2);

//测试一下配件是否好用
this.cpu.calculate();
this.mainboard.installCpu();
}
}

5、产品簇新增内存接口,可拓展性体现在已有代码无需改动

接口定义

1
2
3
4
5
6
7
8
9
10
11
/**
* @Auther mark
* @Version MemoryApi.java v1.0 2023/1/8 21:23 Exp $
* @Description 内存的接口
*/
public interface MemoryApi {
/**
* 示意方法,内存具有缓存数据的能力
*/
public void cacheData();
}

接口实现

1
2
3
4
5
6
7
8
9
10
11
/**
* @Auther mark
* @Version HyMemory.java v1.0 2023/1/8 21:24 Exp $
* @Description 现代内存的类
*/
public class HyMemory implements MemoryApi {
@Override
public void cacheData() {
System.out.println("现在正在使用现代内存");
}
}

新增产品簇

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* @Auther mark
* @Version Schema3.java v1.0 2023/1/8 21:25 Exp $
* @Description 装机方案三:Intel 的CPU + 技嘉的主板 + 现代的内存
*/
public class Schema3 implements AbstractFactory {
@Override
public Object createProduct(int type) {
Object retObj = null;
//type为1表示创建CPU,type为2表示创建主板,type为3表示创建内存
if(type==1){
retObj = new IntelCpu(1156);
}else if(type==2){
retObj = new GAMainboard(1156);
}
//创建新添加的产品
else if(type==3){
retObj = new HyMemory();
}
return retObj;
}
}

装机工程师实现改动

1
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
public class ComputerEngineer {
/**
* 定义组装机器需要的CPU
*/
private CpuApi cpu = null;
/**
* 定义组装机器需要的主板
*/
private MainboardApi mainboard = null;
/**
* 定义组装机器需要的内存
*/
private MemoryApi memory = null;

/**
* 装机过程
*
* @param schema 客户选择的装机方案
*/
public void assembleComputer(AbstractFactory schema) {
//1:首先准备好装机所需要的配件
prepareHardwares(schema);
//2:组装机器
//3:测试机器
//4:交付客户
}

/**
* 准备装机所需要的配件
*
* @param schema 客户选择的装机方案
*/
private void prepareHardwares(AbstractFactory schema) {
//这里要去准备CPU和主板的具体实现,为了示例简单,这里只准备这两个
//可是,装机工程师并不知道如何去创建,怎么办呢?

//使用抽象工厂来获取相应的接口对象
this.cpu = (CpuApi) schema.createProduct(1);
this.mainboard = (MainboardApi) schema.createProduct(2);
this.memory = (MemoryApi) schema.createProduct(3);

//测试一下配件是否好用
this.cpu.calculate();
this.mainboard.installCpu();
if (memory != null) {
this.memory.cacheData();
}
}
}

3.3 抽象工厂模式和 DAO

3.3.1 什么是 DAO

DAO(Data Access Object):数据访问对象

DAO 是 JEE(也称 JavaEE,原 J2EE)中的一个标准模式,通过它来解决访问数据对象所面临的一系列问题,比如:数据源不同、存储类型不同、访问方式不同、供应商不同、版本不同等等,这些不同会造成访问数据的实现上差别很大。

  • 数据源的不同:比如存放于数据库的数据源,存放于 LDAP(轻型目录访问协议)的数据源;又比如存放于本地的数据源和远程服务器上的数据源等等

  • 存储类型的不同:比如关系型数据库(RDBMS)、面向对象数据库(ODBMS)、纯文件、XML 等等

  • 访问方式的不同:比如访问关系型数据库,可以用 JDBC、EntityBean、JPA 等来实现,当然也可以采用一些流行的框架,如 Hibernate、IBatis 等等

  • 供应商的不同:比如关系型数据库,流行如 Oracel、DB2、SqlServer、MySql 等等,它们的供应商是不同的

  • 版本不同:比如关系型数据库,不同的版本,实现的功能是有差异的,就算是对标准的 SQL 的支持,也是有差异的

image-20230108222247457

DAO 需要抽象和封装所有对数据的访问,DAO 承担和数据仓库交互的职责,意味着,访问数据所面临的所有问题,都需要 DAO 在内部来自行解决。

3.3.2 DAO 与抽象工厂的关系

事实上,在实现 DAO 模式的时候,最常见的实现策略就是使用工厂的策略,而且多是通过抽象工厂模式来实现,当然在使用抽象工厂模式来实现的时候,可以结合工厂方法模式。因此 DAO 模式和抽象工厂模式有很大的联系。

3.3.3 DAO 的工厂实现策略

3.3.3.1 工厂方法模式

假设需要实现一个订单处理模块,其中订单一般分为两部分,一是订单主记录或者订单主表,另一个是订单明细记录或者订单子表。业务逻辑一般会对两者都进行操作。

如果业务逻辑比较简单,且对数据的操作是固定的,操作数据库,不管订单的业务如何变化,底层数据存储都一样,此时可以选用工厂方法模式,系统结构为:

image-20230108224002210

从结构示意图可以看出,如果底层存储固定的时候,DAOFactory 就相当于工厂方法模式中的 Creator,在里面定义两个工厂方法,分别创建订单主记录的 DAO 对象和创建订单子记录的 DAO 对象,因为固定是数据库实现,因此提供一个具体的工厂 RdbDAOFactory(Rdb:关系型数据库),来实现对象的创建。也就是说 DAO 可以采用工厂方法模式来实现。

采用工厂方法模式的情况,要求 DAO 底层存储实现方式是固定的,这种多用在一些简单的小项目开发上。

3.3.3.2 抽象工方法模式

实际上更多的时候,DAO 底层存储实现方式是不固定的,DAO 通常会支持多种存储实现方式,具体使用哪一种存储方式可能是由应用动态决定,或者是通过配置来指定。这种情况多见于产品开发、或者是稍复杂的应用、或者是较大的项目中。

对于底层存储方式不固定的时候,一般是采用抽象工厂模式来实现 DAO。比如现在的实现除了 RDB 的实现,还会有 Xml 的实现,它们会被应用动态的选择,此时系统结构如图所示:

image-20230108224840673

从结构示意图可以看出,采用抽象工厂模式来实现 DAO 的时候,DAOFactory 就相当于抽象工厂,里面定义一系列创建相关对象的方法,分别是创建订单主记录的 DAO 对象和创建订单子记录的 DAO 对象,此时 OrderMainDAO 和 OrderDetailDAO 就相当于被创建的产品,RdbDAOFactory 和 XmlDAOFactory 就相当于抽象工厂的具体实现,在它们里面会选择相应的具体的产品实现来创建对象。

3.4 抽象工厂的优缺点

3.4.1 分离接口和实现

客户端使用抽象工厂来创建需要的对象,而客户端根本就不知道具体的实现是谁,客户端只是面向产品的接口编程而已,也就是说,客户端从具体的产品实现中解耦。

3.4.2 产品簇切换

可以定义产品簇的概念,给出对象之间的关联关系

3.4.3 不容易扩展新产品

前面也提到这个问题,如果需要给整个产品簇添加一个新的产品,那么就需要修改抽象工厂,这样就会导致修改所有的工厂实现类。在前面提供了一个可以扩展工厂的方式来解决这个问题,但是又不够安全。如何选择,需要根据实际应用来权衡。

3.4.4 容易造成类层次复杂

在使用抽象工厂模式的时候,如果需要选择的层次过多,那么会造成整个类层次变得复杂。

那么客户端怎么选呢?不会把所有可能的实现情况全部都做到一个层次上吧,这个时候客户端就需要一层一层选择,也就是整个抽象工厂的实现也需要分出层次来,每一层负责一种选择,也就是一层屏蔽一种变化,这样很容易造成复杂的类层次结构

3.5 思考

3.5.1 抽象工厂模式的本质

本质:选择产品簇的实现

3.5.2 何时选用

  • 若希望一个系统独立于它的产品的创建、组合和表现时,即:希望一个系统只知道产品的接口,而不需要关心具体的实现。
  • 若系统需要由多个产品系列中的一个来配置时,即:可以动态切换产品簇时。
  • 若需要强调一系列产品的接口,方便联合使用的时候。

3.6 相关模式

3.6.1 抽象工厂模式和工厂方法模式

工厂方法模式一般针对单独的产品对象的创建,而抽象工厂模式注重产品簇对象的创建。

若将抽象工厂创建的产品簇简化,产品簇只有一个产品,这是抽象工厂和工厂方法是差不多的,即:抽象工厂可以退化成工厂方法,工厂方法可以退化成简单工厂。

在抽象工厂的实现中,可以使用工厂方法提供抽象工厂的具体实现,可以组合使用。

3.6.2 抽象工厂和单例模式

可以组合使用。在抽象工厂模式例,具体的工厂实现可以实现成单例模式。