策略模式 + 工厂模式:让文件上传支持多种存储方案
在对象存储业务中,文件上传看起来只是一个简单功能:
上传文件
但真正落地时,文件可能存储到:
Minio
阿里云 OSS
七牛云
腾讯云 COS
华为云 OBS
这些存储产品完成的事情都是一样的:
上传文件。
但是底层 SDK、配置、上传逻辑却完全不同。
如果把所有逻辑都写到一个 Service 中,随着存储平台越来越多,代码会越来越难维护。
这时候就非常适合使用:
策略模式 + 工厂模式
教程中的业务背景也是如此:当前计划支持 Minio 和阿里云 OSS,同时考虑未来可能继续引入其他对象存储,因此需要提前考虑代码的扩展性。
一、如果不用设计模式,会有什么问题?
最简单的做法可能是:
如果 storage.type = minio
执行 Minio 上传
否则如果 storage.type = aliyun
执行阿里云上传
一开始只有两个平台,看起来没什么问题。
但是后面可能变成:
if minio
else if aliyun
else if qiniu
else if tencent
else if huawei
最终一个文件上传 Service 中会同时存在:
Minio 上传逻辑
阿里云 OSS 上传逻辑
七牛云上传逻辑
腾讯云上传逻辑
华为云上传逻辑
这时候 Service 的职责就太多了。
而且以后增加新的存储平台,还要继续修改原来的业务代码。
所以真正的问题不是:
文件怎么上传?
而是:
如何让文件上传支持多种实现,并且以后新增实现时尽量少修改原来的代码?
二、策略模式解决什么问题?
策略模式可以简单理解成:
同一件事情存在多种实现方案,就把每一种方案单独封装起来。
例如文件上传统一定义一个能力:
FileStrategy
uploadFile()
然后不同存储平台分别实现:
FileStrategy
↑
├── MinioFileStrategy
│
├── AliyunOSSFileStrategy
│
└── QiniuFileStrategy
也就是说:
MinioFileStrategy
只负责 Minio。
AliyunOSSFileStrategy
只负责阿里云。
以后增加七牛云:
QiniuFileStrategy
也只负责七牛云。
所有策略都遵守相同的 FileStrategy 接口,因此上层业务可以统一通过接口调用。教程中也是先定义 FileStrategy,再分别创建 Minio 和 Aliyun 两套实现。
三、为什么还需要工厂模式?
策略模式解决了:
有哪些上传方案?
但是还有一个问题:
系统现在到底应该使用哪一个方案?
这就是工厂模式负责的事情。
例如配置:
storage:
type: aliyun
表示:
当前使用阿里云 OSS
如果修改成:
storage:
type: minio
表示:
当前使用 Minio
然后交给工厂判断:
storage.type
↓
FileStrategyFactory
↓
选择具体策略
例如:
aliyun
↓
AliyunOSSFileStrategy
或者:
minio
↓
MinioFileStrategy
教程中的 FileStrategyFactory 就是读取 storage.type,然后决定创建 Minio 还是 Aliyun 的策略实现。
所以可以记成:
策略模式负责提供方案,工厂模式负责选择方案。
四、Service 为什么只依赖接口?
真正的业务层不会直接写:
MinioFileStrategy
也不会直接写:
AliyunOSSFileStrategy
而是依赖:
FileStrategy
也就是说:
FileService
↓
FileStrategy
Service 的想法很简单:
我只管上传文件,至于具体使用哪家存储,我不关心。
所以业务代码只需要调用:
fileStrategy.uploadFile()
真正执行 Minio 还是 Aliyun,由前面的工厂决定。教程中的 FileServiceImpl 正是只注入 FileStrategy 接口,而不是依赖具体实现类。
这样就实现了:
业务层和具体存储平台解耦。
五、完整业务流程
假设当前配置:
storage.type = aliyun
项目启动时:
读取配置
↓
storage.type = aliyun
↓
FileStrategyFactory
↓
创建 AliyunOSSFileStrategy
↓
交给 Spring 容器
然后:
FileService
↓
注入 FileStrategy
实际上得到的是:
AliyunOSSFileStrategy
当用户上传文件:
客户端
↓
FileController
↓
FileService
↓
fileStrategy.uploadFile()
↓
AliyunOSSFileStrategy
↓
上传到阿里云 OSS
如果把配置修改为:
storage.type = minio
流程就会变成:
客户端
↓
Controller
↓
Service
↓
fileStrategy.uploadFile()
↓
MinioFileStrategy
↓
上传到 Minio
Controller 不需要修改。
Service 也不需要修改。
真正变化的只是底层策略。
教程最终也是通过修改 storage.type,验证了 Minio 和 Aliyun 两种实现可以切换,而 Service 层无需变化。
六、为什么文件上传特别适合这种设计?
因为文件上传具有一个非常典型的特点:
业务目标固定,但是实现方式很多。
业务层看到的永远都是:
上传文件
但是底层可能是:
Minio SDK
阿里云 OSS SDK
七牛云 SDK
腾讯云 COS SDK
这正是策略模式最典型的应用场景。
类似的业务还有很多,例如:
支付
├── 微信支付
├── 支付宝
└── 银联
或者:
消息通知
├── 短信
├── 邮件
└── 微信消息
或者:
登录方式
├── 手机号登录
├── 微信登录
└── GitHub 登录
它们都有一个共同特点:
同一个业务目标,存在多个可以互相替换的实现方案。
七、以后新增存储平台怎么办?
假设以后增加:
七牛云
原来的结构:
FileStrategy
↑
├── MinioFileStrategy
└── AliyunOSSFileStrategy
只需要增加:
FileStrategy
↑
├── MinioFileStrategy
├── AliyunOSSFileStrategy
└── QiniuFileStrategy
然后让工厂能够识别:
qiniu
↓
QiniuFileStrategy
Service 仍然还是:
fileStrategy.uploadFile()
不用修改。
这就是这种设计最大的价值:
新增一种实现,尽量通过增加新代码完成,而不是不断修改旧业务代码。
八、什么时候适合使用策略模式?
比较适合下面几类场景。
第一种:
同一业务存在多种实现
例如:
文件存储
支付
登录
消息通知
算法选择
第二种:
实现方式可能经常切换
例如:
开发环境用 Minio
生产环境用阿里云 OSS
第三种:
未来可能继续增加新的实现
例如现在:
Minio + Aliyun
未来:
Minio + Aliyun + Qiniu + COS
这时候策略模式会非常适合。
九、什么时候没必要用?
设计模式不是越多越好。
如果你的系统明确:
永远只使用 Minio
而且几乎没有切换存储平台的可能,那么直接写:
MinioFileService
完全没有问题。
没必要为了一个实现,强行设计:
接口
策略
工厂
多个配置
否则只会增加代码复杂度。
所以是否使用策略模式,要看业务是否真的存在:
多种可替换实现。
如果只有一种实现,简单写通常更合适。
十、策略模式和工厂模式分别负责什么?
最后把两者彻底分清。
策略模式
解决:
有哪些实现方案?
例如:
FileStrategy
↑
├── Minio
├── Aliyun
└── Qiniu
核心思想:
把不同实现拆开。
工厂模式
解决:
当前到底选择哪个实现?
例如:
storage.type
↓
Factory
↓
选择 Strategy
核心思想:
把对象的选择和创建集中起来。
两者结合:
配置
↓
工厂模式
↓
选择具体策略
↓
策略模式
↓
Minio / Aliyun / Qiniu
↓
Service 使用
十一、总结
文件上传使用“策略模式 + 工厂模式”,并不是因为上传文件本身很复杂。
真正的原因是:
底层存储方式可能变化。
策略模式负责:
Minio
Aliyun
Qiniu
这些不同上传方式的隔离。
工厂模式负责:
当前到底使用哪一个?
而 Service 永远只需要:
上传文件
最终形成:
Controller
↓
FileService
↓
FileStrategy
↓
Factory 选择具体实现
↓
Minio / Aliyun / 其他对象存储
可以简单记成一句话:
策略模式负责“有哪些方案”,工厂模式负责“选择哪个方案”,业务层只负责“调用方案”。
这种设计真正适合的,是:
同一个业务动作存在多种实现,而且这些实现未来可能切换或继续增加的场景。