工厂模式:把"创建对象"收进一个入口
从一个真实的痛点出发:业务代码里的 if-else 分发,以及新增渠道时为什么总要改核心代码。
一、痛点:new 和 if-else 把代码焊死了
假设我们有一个文件上传模块,支持三种存储后端:本地磁盘、阿里云 OSS、AWS S3。最直白的写法是这样:
publicStringupload(Stringfile,StringstorageType){if("local".equals(storageType)){returnnewLocalStorage().save(file);}elseif("oss".equals(storageType)){returnnewOssStorage().save(file);}elseif("s3".equals(storageType)){returnnewS3Storage().save(file);}thrownewIllegalArgumentException("未知存储类型: "+storageType);}这段代码的问题不是"不能用",而是"经不起变化":
- 开闭原则被违反:每新增一种存储后端(比如七牛云),就要回来改这段核心代码——对修改不封闭;
- 创建逻辑散落:调用方必须知道每个实现类的名字和构造细节;
- 调用方被绑定:业务代码与具体实现类强耦合,换实现、做 Mock 都很痛苦。
工厂模式的回答是:把"创建谁"这个决策收口到一个专门的工厂类里,调用方只面向接口。
二、简单工厂:一个入口,屏蔽创建细节
第一步改造:抽出统一接口,用工厂类承接创建逻辑。
// 统一存储接口publicinterfaceStorageService{Stringsave(Stringfile);}publicclassLocalStorageimplementsStorageService{@OverridepublicStringsave(Stringfile){return"local://"+file;}}// OssStorage、S3Storage 实现同构,略importjava.util.HashMap;importjava.util.Map;importjava.util.function.Supplier;/** * 简单工厂:唯一的创建入口 * 用注册表替代 if-else 链,新增渠道时只动这一个类 */publicclassStorageFactory{privatestaticfinalMap<String,Supplier<StorageService>>REGISTRY=newHashMap<>();static{REGISTRY.put("local",LocalStorage::new);REGISTRY.put("oss",OssStorage::new);REGISTRY.put("s3",S3Storage::new);}publicstaticStorageServicecreate(Stringtype){Supplier<StorageService>supplier=REGISTRY.get(type);if(supplier==null){thrownewIllegalArgumentException("不支持的存储类型: "+type);}returnsupplier.get();}}业务层彻底与实现类解耦:
publicclassFileService{publicStringupload(Stringfile,StringstorageType){StorageServicestorage=StorageFactory.create(storageType);returnstorage.save(file);}}现在FileService不知道、也不需要知道 LocalStorage 和 OssStorage 的存在。它只依赖两个抽象:StorageService接口和工厂的 create 方法。
三、简单工厂的边界
注意:简单工厂把 if-else 从业务代码挪进了工厂类,没有消灭 if-else,只是把它集中了。这带来两个效果:
- 好处:创建逻辑单点管理,业务代码干净了;
- 代价:新增渠道仍需修改工厂类(加一行注册),严格说没有完全满足开闭原则。
如果"新增渠道不改任何现有类"是硬要求,就需要工厂方法模式了。
四、工厂方法:一个产品,一个工厂
工厂方法的思路是:把"创建"也抽象成接口,每种产品对应一个独立工厂类。
// 工厂接口(GoF 术语中称为 Creator)publicinterfaceStorageCreator{StorageServicecreate();}publicclassLocalStorageCreatorimplementsStorageCreator{@OverridepublicStorageServicecreate(){returnnewLocalStorage();}}// OssStorageCreator、S3StorageCreator 同构,略调用方持有工厂接口,新增 S3 时写一个S3StorageCreator即可,现有类零改动:
publicclassFileUploader{privatefinalStorageCreatorcreator;publicFileUploader(StorageCreatorcreator){this.creator=creator;}publicStringupload(Stringfile){returncreator.create().save(file);}}代价也很明显:类的数量翻倍——每个产品多一个工厂类。产品类型少时显得繁琐。
五、两种工厂怎么选
| 维度 | 简单工厂 | 工厂方法 |
|---|---|---|
| 创建逻辑的位置 | 集中在唯一工厂类 | 分散到各产品专属工厂 |
| 新增产品 | 改工厂类(加一行注册) | 新增工厂类,现有代码零修改 |
| 类数量 | 少 | 每个产品多一个类 |
| 开闭原则 | 部分满足 | 完全满足 |
| 适用场景 | 产品类型稳定、集中管理 | 产品类型持续扩张、强调扩展性 |
工程上的务实结论:多数场景用简单工厂 + 注册表就够;当产品线明显会持续扩张、且团队对"改现有代码"很敏感时,升级到工厂方法。
六、工厂模式没回答的问题
工厂模式解决的是"创建哪个对象",它不解决"创建完之后,不同对象的行为差异怎么办"。
回到存储例子:三种存储后端的 save 逻辑完全不同——这个差异由多态解决,和工厂无关。如果业务里还有一类需求是"根据参数选择执行哪套行为、并随时可切换",那是策略模式的领地(见本系列第 3 篇)。
这也是工厂和策略经常被一起提问的原因:工厂管"造",策略管"用"。
小结
工厂模式的价值一句话:创建入口收口,调用方面向接口。简单工厂集中管理,工厂方法彻底开闭。选哪个,取决于产品类型的扩张预期和团队对类数量的容忍度。