抽象工厂模式-Abstract Factory Pattern

引言

首先我们由一个实际问题来引出抽象工厂模式。

考虑这样一个场景,系统中需要向OSS上传文件,以及通过OSS下载文件。而在系统中有不同的业务在使用这两个功能。如下图:

抽象工厂模式引言类图

伪代码如下

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90

public interface FileUploader {

/**
* 上传文件
*
* @param file 文件
* @return 文件路径
*/
String uploadFile(File file);
}

public interface FileDownloader {

/**
* 下载文件
* @param path 文件路径
* @return 文件流
*/
InputStream download(String path);
}

@Slf4j
public class AliyunFileUploaderImpl implements FileUploader {

@Override
public String uploadFile(File file) {
log.info("向阿里云OSS上传文件");
return "/test/" + file.getName();
}
}

@Slf4j
public class AliyunFileDownloaderImpl implements FileDownloader {
@Override
public InputStream download(String path) {
log.info("通过阿里云下载文件");
// 这里只是模拟文件下载,所以不创建文件流,而是使用空文件流
return ByteArrayInputStream.nullInputStream();
}
}

@Slf4j
public class XxxService1 {

private final FileUploader fileUploader = new AliyunFileUploaderImpl();

public void doService(File file) {
String filePath = this.fileUploader.uploadFile(file);
log.info("文件上传到了:{}", filePath);
log.info("XxxService1 执行其他业务代码");
}
}

@Slf4j
public class XxxService2 {

private final FileUploader fileUploader = new AliyunFileUploaderImpl();

private final FileDownloader fileDownloader = new AliyunFileDownloaderImpl();

public void doService1(File file) {
String filePath = this.fileUploader.uploadFile(file);
log.info("文件上传到了:{}", filePath);
log.info("XxxService2 执行doService1 业务代码");
}

public void doService2(String path) {
InputStream file = this.fileDownloader.download(path);
log.info("文件下载了文件:{}", path);
log.info("XxxService2 执行doService2 业务代码");
}
}

public class Main {

public static void main(String[] args) {

XxxService1 service1 = new XxxService1();
File file = Mockito.mock(File.class);
service1.doService(file);

XxxService2 service2 = new XxxService2();
File file2 = Mockito.mock(File.class);
service2.doService1(file2);

service2.doService2("xxx");
}
}

这时另外一个客户也需要用这套系统,但是客户不想用阿里云的OSS存储文件,想用华为云或者Minio存储文件。

那问题就来了,我们需要新的FileUploaderFileDownloader实现,并且在所有代码中替换掉原来的 AliyunFileUploaderImplAliyunFileDownloaderImpl 配置。而且我们需要修改每一个使用它们的类。这样很麻烦,会使我们的代码可维护性变得很差。而且漏改也会出现系统性BUG。

FileUploaderFileDownloader的创建必定是要依赖一些具体配置的,如阿里云的 accessKeyIdaccessKeySecret。不同的实现,配置也不同。这也是我建议需要通过工厂来创建它们的一个原因。但是在下文中并没有添加相关的配置依赖,在实际的业务中,这些都是必不可少的。请各位同学注意。

下面我们通过抽象工厂模式来解决上述问题。

有没有发现上面的代码中违背了哪个设计原则呢?

上面的代码违背了依赖倒置原则。我们虽然定义的字段类型是 FileUploader 但是我们直接在业务类中实例化了具体类。这样等于间接的与具体类发生了耦合。正确的做法应该是通过 setter 或构造函数将具体类注入到业务类的实例中。

当然直接实例化具体类的做法在很多框架中也是很常见的,这种做法并不是完全不可取。但是一般这样做的目的是简化类实例化时的配置,直接提供依赖接口的默认实现,并提供 serter方法可以替换掉默认实现。

注意

这个例子并不是很恰当,因为现在其实都在用Spring框架。如果将 FileUploaderFileDownloader 改为依赖注入的方式,就不会有这样的问题。通过Spring的依赖注入能够很好的解决这个问题是因为Spring本身就是一个大的工厂。

另外不要认为工厂就必须要产生新的对象,这样想是不正确的。工厂只是把你需要的对象给你,至于是新创建对象还是使用已实例化的对象,根据业务场景去处理即可。

定义及实现

定义

首先我们了解下抽象工厂模式的定义。

Provide an interface for creating families of related or dependent objects without specifying their concrete classes.

为创建一组相关或相互依赖的对象提供一个接口, 而且无须指定它们的具体类。

这个定义不太容易理解,我们通过UML类图和具体的代码来逐步理解其含义。

结构

抽象工厂模式类图

我们对上面的结构做下调整,让业务类依赖 AbstractFileManagerFactory,不再直接依赖FileUploaderFileDownloader。而FileUploaderFileDownloader的创建则放到抽象工厂的具体实现类中来进行。而业务类中也不再关心抽象工厂的具体实现,只依赖抽象工厂接口。具体实现则通过 setter 或构造函数注入。

上面定义中提到的 “为创建一组相关或相互依赖的对象提供一个接口” 就是抽象工厂。对于我们的这个业务场景来说文件的上传、下载就是一组相关的对象。

  • 抽象工厂并并不是必须定义为抽象类,可以是抽象类或接口。

代码实现

原来的 FileUploaderFileDownloader 以及 AliyunFileUploaderImplAliyunFileDownloaderImpl 不动,我们先增加 AbstractFileManagerFactoryAliyunFileManagerFactory 并修改两个Service。先实现其中一个,然后再实现另一个,看看如何替换,以体会抽象工厂模式解决了什么问题。

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
50
51
52
53
54
55
56
public interface AbstractFileManagerFactory {

FileUploader getFileUploader();

FileDownloader getFileDownloader();
}

public class AliyunFileManagerFactory implements AbstractFileManagerFactory {

private final FileUploader fileUploader;

private final FileDownloader fileDownloader;

public AliyunFileManagerFactory() {
this.fileUploader = new AliyunFileUploaderImpl();
this.fileDownloader = new AliyunFileDownloaderImpl();
}

@Override
public FileUploader getFileUploader() {
return this.fileUploader;
}

@Override
public FileDownloader getFileDownloader() {
return this.fileDownloader;
}
}

@Slf4j
public class XxxService1 {

private final AbstractFileManagerFactory factory;

public XxxService1(AbstractFileManagerFactory factory) {
this.factory = factory;
}

public void doService(File file) {
String filePath = this.factory.getFileUploader().uploadFile(file);
log.info("文件上传到了:{}", filePath);
log.info("XxxService1 执行其他业务代码");
}
}

public class Main {

public static void main(String[] args) {

AbstractFileManagerFactory factory = new AliyunFileManagerFactory();

XxxService1 service1 = new XxxService1(factory);
File file = Mockito.mock(File.class);
service1.doService(file);
}
}

这时候我们再实现一套Minio的逻辑,看看如何替换成Minio

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
@Slf4j
public class MinioFileUploaderImpl implements FileUploader {

@Override
public String uploadFile(File file) {
log.info("向minio上传文件");
return "/test/" + file.getName();
}
}

@Slf4j
public class MinioFileDownloaderImpl implements FileDownloader {
@Override
public InputStream download(String path) {
log.info("通过minio下载文件");
// 这里只是模拟文件下载,所以不创建文件流,而是使用空文件流
return ByteArrayInputStream.nullInputStream();
}
}

public class MinioFileManagerFactory implements AbstractFileManagerFactory {

private final FileUploader fileUploader;

private final FileDownloader fileDownloader;

public MinioFileManagerFactory() {
this.fileUploader = new MinioFileUploaderImpl();
this.fileDownloader = new MinioFileDownloaderImpl();
}

@Override
public FileUploader getFileUploader() {
return this.fileUploader;
}

@Override
public FileDownloader getFileDownloader() {
return this.fileDownloader;
}
}

Service 不用动,修改下入口程序。

1
2
3
4
5
6
7
8
9
10
11
12
public class Main {

public static void main(String[] args) {

// 这里替换成Minio
AbstractFileManagerFactory factory = new MinioFileManagerFactory();

XxxService1 service1 = new XxxService1(factory);
File file = Mockito.mock(File.class);
service1.doService(file);
}
}

抽象工厂针对的是一组或一系列相关或相互依赖的接口,而工厂方法和简单工厂则是针对的单一的接口。这一点区别大家要注意。如果只针对一个接口应该根据场景选择简单工厂或工厂方法。

抽象工厂模式有什么问题

从上面的例子中我们可以看出来,抽象工厂的弊端也很大。虽然我们不再依赖FileUploaderFileDownloader的具体实现,改为依赖了 AbstractFileManagerFactory,但是只要我们增加新的文件上传和下载方式,就需要在业务类创建的地方全部修改一遍。这不符合开闭原则。有没有什么方法能解决这个问题呢?当然是有的。

  1. 通过Spring的依赖注入来解决,业务类依赖 AbstractFileManagerFactory,而我们使用哪个具体实现就将其创建(或注册)为Spring Bean。让Spring来管理类之间的依赖关系。
  2. 通过简单工厂来处理。业务层依赖简单工厂,简单工厂通过读取配置决定创建抽象工厂的哪个具体实现。这样修改代码只需要修改简单工厂类即可,无需对业务类做大面积修改。

简单工厂+抽象工厂

使用简单工厂处理的源码清单如下:

首先需要定义一个配置,用来指定要使用的工厂类。配置文件放在 src/main/resources/config.properties

1
factory=org.depsea.designpattern.creation.afp.e02.factory.MinioFileManagerFactory

下面是 SimpleFileManagerFactory 的具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class SimpleFileManagerFactory {

private final AbstractFileManagerFactory factory;

public SimpleFileManagerFactory() throws Exception {
// 在构造函数中实例化工厂类
Class<? extends AbstractFileManagerFactory> factoryClass = this.getFactoryClass();
this.factory = factoryClass.getConstructor().newInstance();
}

public AbstractFileManagerFactory getFactory() {
return factory;
}

@SuppressWarnings("unchecked")
private Class<? extends AbstractFileManagerFactory> getFactoryClass() throws IOException, ClassNotFoundException {
// 读取配置文件并获取到工厂类
Properties properties = new Properties();
properties.load(getClass().getClassLoader().getResourceAsStream("config.properties"));
String clazz = properties.getProperty("factory");
return (Class<? extends AbstractFileManagerFactory>) Class.forName(clazz);
}
}

入口程序则修改为

1
2
3
4
5
6
7
8
9
10
11
12
public class Main {

public static void main(String[] args) throws Exception {

SimpleFileManagerFactory simpleFileManagerFactory = new SimpleFileManagerFactory();
AbstractFileManagerFactory factory = simpleFileManagerFactory.getFactory();

XxxService1 service1 = new XxxService1(factory);
File file = Mockito.mock(File.class);
service1.doService(file);
}
}

至此添加新的文件上传下载支持,只需要修改配置文件即可,无需对业务代码甚至是工厂做任何的修改。

上面的代码同样有弊端,在通过反射做实例化的时候,使用的是默认构造函数,如果工厂没有默认构造函数,则会出现无法实例化的情况,这限制了代码的灵活性。

实际应用

Spring中的 BeanFactory 就是抽象工厂模式的最佳应用。

抽象工厂的实际应用

而Spring给我们提供了一种新的思路,通过不同的方式来创建同一个对象。比如上面的例子中 ClassPathXmlApplicationContext 提供的是一种通过XML来创建Bean的方式;而AnnotationConfigApplicationContext则提供通过注解来创建Bean的方式。

而在一个项目中可以同时支持两种Bean创建方式,这不是本篇文章的重点,有兴趣的同学可以通过学习Spring源码来了解其工作原理。

  • BeanFactory 是抽象工厂模式,创建了一系列的Bean。
  • FactoryBean 是工厂方法模式,只创建单一Bean。

请注意以上两者的区别。

总结

抽象工厂模式是一种创建型设计模式,它提供一个接口用于创建一系列相关或依赖对象的家族,而不需要指定具体的类。这有助于实现对象的解耦和灵活性。

适用于需要创建一系列相关对象的场景。它可以提高系统的可维护性和可扩展性。

优点:

  • 实现了对象的解耦:客户端使用抽象工厂接口来创建产品对象,而无需关心具体的产品类,从而实现了对象的解耦。
  • 易于替换产品系列:由于客户端使用抽象工厂接口来创建产品对象,因此可以轻松地替换整个产品系列,而不需要修改客户端代码。

缺点

  • 不够灵活:一旦需要添加新的产品类,就需要修改抽象工厂的接口和所有的具体工厂,这可能会带来一定的工作量。
作者

大扑棱蛾子(jaune162@126.com)

发布于

2024-02-07

更新于

2024-10-21

许可协议

评论