引言
首先我们由一个实际问题来引出抽象工厂模式。
考虑这样一个场景,系统中需要向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 {
String uploadFile(File file); }
public interface FileDownloader {
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存储文件。
那问题就来了,我们需要新的FileUploader
和FileDownloader
实现,并且在所有代码中替换掉原来的 AliyunFileUploaderImpl
和 AliyunFileDownloaderImpl
配置。而且我们需要修改每一个使用它们的类。这样很麻烦,会使我们的代码可维护性变得很差。而且漏改也会出现系统性BUG。
FileUploader
和FileDownloader
的创建必定是要依赖一些具体配置的,如阿里云的 accessKeyId
和 accessKeySecret
。不同的实现,配置也不同。这也是我建议需要通过工厂来创建它们的一个原因。但是在下文中并没有添加相关的配置依赖,在实际的业务中,这些都是必不可少的。请各位同学注意。
下面我们通过抽象工厂模式来解决上述问题。
有没有发现上面的代码中违背了哪个设计原则呢?
上面的代码违背了依赖倒置原则。我们虽然定义的字段类型是 FileUploader
但是我们直接在业务类中实例化了具体类。这样等于间接的与具体类发生了耦合。正确的做法应该是通过 setter
或构造函数将具体类注入到业务类的实例中。
当然直接实例化具体类的做法在很多框架中也是很常见的,这种做法并不是完全不可取。但是一般这样做的目的是简化类实例化时的配置,直接提供依赖接口的默认实现,并提供 serter
方法可以替换掉默认实现。
注意
这个例子并不是很恰当,因为现在其实都在用Spring框架。如果将 FileUploader
和FileDownloader
改为依赖注入的方式,就不会有这样的问题。通过Spring的依赖注入能够很好的解决这个问题是因为Spring本身就是一个大的工厂。
另外不要认为工厂就必须要产生新的对象,这样想是不正确的。工厂只是把你需要的对象给你,至于是新创建对象还是使用已实例化的对象,根据业务场景去处理即可。
定义及实现
定义
首先我们了解下抽象工厂模式的定义。
Provide an interface for creating families of related or dependent objects without specifying their concrete classes.
为创建一组相关或相互依赖的对象提供一个接口, 而且无须指定它们的具体类。
这个定义不太容易理解,我们通过UML类图和具体的代码来逐步理解其含义。
结构
我们对上面的结构做下调整,让业务类依赖 AbstractFileManagerFactory
,不再直接依赖FileUploader
和FileDownloader
。而FileUploader
和FileDownloader
的创建则放到抽象工厂的具体实现类中来进行。而业务类中也不再关心抽象工厂的具体实现,只依赖抽象工厂接口。具体实现则通过 setter
或构造函数注入。
上面定义中提到的 “为创建一组相关或相互依赖的对象提供一个接口” 就是抽象工厂。对于我们的这个业务场景来说文件的上传、下载就是一组相关的对象。
- 抽象工厂并并不是必须定义为抽象类,可以是抽象类或接口。
代码实现
原来的 FileUploader
和 FileDownloader
以及 AliyunFileUploaderImpl
和 AliyunFileDownloaderImpl
不动,我们先增加 AbstractFileManagerFactory
和 AliyunFileManagerFactory
并修改两个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) {
AbstractFileManagerFactory factory = new MinioFileManagerFactory();
XxxService1 service1 = new XxxService1(factory); File file = Mockito.mock(File.class); service1.doService(file); } }
|
抽象工厂针对的是一组或一系列相关或相互依赖的接口,而工厂方法和简单工厂则是针对的单一的接口。这一点区别大家要注意。如果只针对一个接口应该根据场景选择简单工厂或工厂方法。
抽象工厂模式有什么问题
从上面的例子中我们可以看出来,抽象工厂的弊端也很大。虽然我们不再依赖FileUploader
和FileDownloader
的具体实现,改为依赖了 AbstractFileManagerFactory
,但是只要我们增加新的文件上传和下载方式,就需要在业务类创建的地方全部修改一遍。这不符合开闭原则。有没有什么方法能解决这个问题呢?当然是有的。
- 通过Spring的依赖注入来解决,业务类依赖
AbstractFileManagerFactory
,而我们使用哪个具体实现就将其创建(或注册)为Spring Bean。让Spring来管理类之间的依赖关系。
- 通过简单工厂来处理。业务层依赖简单工厂,简单工厂通过读取配置决定创建抽象工厂的哪个具体实现。这样修改代码只需要修改简单工厂类即可,无需对业务类做大面积修改。
使用简单工厂处理的源码清单如下:
首先需要定义一个配置,用来指定要使用的工厂类。配置文件放在 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。
请注意以上两者的区别。
总结
抽象工厂模式是一种创建型设计模式,它提供一个接口用于创建一系列相关或依赖对象的家族,而不需要指定具体的类。这有助于实现对象的解耦和灵活性。
适用于需要创建一系列相关对象的场景。它可以提高系统的可维护性和可扩展性。
优点:
- 实现了对象的解耦:客户端使用抽象工厂接口来创建产品对象,而无需关心具体的产品类,从而实现了对象的解耦。
- 易于替换产品系列:由于客户端使用抽象工厂接口来创建产品对象,因此可以轻松地替换整个产品系列,而不需要修改客户端代码。
缺点
- 不够灵活:一旦需要添加新的产品类,就需要修改抽象工厂的接口和所有的具体工厂,这可能会带来一定的工作量。