工厂方法一定比简单工厂高级?三种工厂模式到底该怎么选

117 阅读8分钟

「设计模式与范式」系列 Day10

写在前面

简单工厂、工厂方法、抽象工厂,光看名字很容易被带偏——好像后面的一定比前面的"高级",写代码就该一步到位上抽象工厂。这篇用一个规则配置解析器的真实需求走一遍:不同格式(JSON/XML/YAML/Properties)的配置文件要用不同的 Parser 去解析,看看这三种工厂到底是怎么演进出来的,以及为什么"更复杂"不等于"更好"。


一、是什么:三种工厂各自解决的问题

模式解决的问题对象的分类维度
简单工厂一个工厂类,按参数创建对应的实现类一个维度(比如按文件格式)
工厂方法用多态代替 if-else,每种类型对应一个工厂类一个维度,但用继承体系表达
抽象工厂一个工厂负责创建一组相关的对象两个及以上维度

三者共同的目标没有变过:把"根据什么条件创建哪个对象"这件事从业务代码里挪出去。区别只在于对象的分类维度有多少个、以及创建逻辑复杂到什么程度。


二、为什么:从简单工厂演进到抽象工厂,每一步都是被具体问题推着走的

起点:规则配置文件要按格式选择解析器

public class RuleConfigSource {
    public RuleConfig load(String filePath) {
        String extension = getFileExtension(filePath); // json/xml/yaml/properties
        IRuleConfigParser parser = RuleConfigParserFactory.createParser(extension);
        if (parser == null) {
            throw new InvalidRuleConfigException("unsupported: " + filePath);
        }
        String configText = ""; // 从文件读取
        return parser.parse(configText);
    }
}

createParser 该怎么实现,取决于创建逻辑复不复杂:如果只是简单的按格式 new 一个对应的 Parser,用简单工厂就够了;如果创建过程本身很重,才需要往工厂方法升级。

简单工厂:够用就不要多加抽象

public class RuleConfigParserFactory {
    public static IRuleConfigParser createParser(String format) {
        if ("json".equalsIgnoreCase(format)) return new JsonRuleConfigParser();
        if ("xml".equalsIgnoreCase(format)) return new XmlRuleConfigParser();
        // yaml、properties 同理
        return null;
    }
}

这个 if-else 版本每次调用都要重新 new 一个 Parser 对象。如果 Parser 本身没有成员变量、可以安全复用,还有一种更省资源的写法——预先创建好、缓存在 Map 里:

public class RuleConfigParserFactory {
    private static final Map<String, IRuleConfigParser> cachedParsers = new HashMap<>();
    static {
        cachedParsers.put("json", new JsonRuleConfigParser());
        cachedParsers.put("xml", new XmlRuleConfigParser());
        // yaml、properties 同理
    }
    public static IRuleConfigParser createParser(String format) {
        if (format == null || format.isEmpty()) return null;
        return cachedParsers.get(format.toLowerCase());
    }
}

两种写法都是简单工厂,选哪种只取决于 Parser 是否无状态、能不能复用,跟"该不该用简单工厂"这个问题无关。

工厂方法:创建逻辑变复杂时,把工厂本身也拆开

如果某种 Parser 的创建过程不再是一个 new 能打发的——比如要组合别的对象、要做额外的初始化——继续把所有创建逻辑塞进一个工厂类,这个类会越来越臃肿。工厂方法模式用多态把每种类型的创建逻辑拆到各自的工厂类里:

public interface IRuleConfigParserFactory {
    IRuleConfigParser createParser();
}
public class JsonRuleConfigParserFactory implements IRuleConfigParserFactory {
    public IRuleConfigParser createParser() { return new JsonRuleConfigParser(); }
}
// XmlRuleConfigParserFactory、YamlRuleConfigParserFactory 同理

但这样一来,load() 里又多了一段新的 if-else——用来判断该 new 哪个工厂类:

IRuleConfigParserFactory factory =
        "json".equalsIgnoreCase(ext) ? new JsonRuleConfigParserFactory() : /* ... */ null;

创建对象的逻辑被挪走了,创建工厂的逻辑又耦合回了 load()。解法很直接:再套一层简单工厂,去创建这些工厂对象,工厂类本身通常不含状态,用 Map 缓存复用即可:

public class RuleConfigParserFactoryMap { // 工厂的工厂
    private static final Map<String, IRuleConfigParserFactory> cachedFactories = new HashMap<>();
    static {
        cachedFactories.put("json", new JsonRuleConfigParserFactory());
        cachedFactories.put("xml", new XmlRuleConfigParserFactory());
    }
    public static IRuleConfigParserFactory getParserFactory(String type) {
        return type == null ? null : cachedFactories.get(type.toLowerCase());
    }
}

load() 只需要调用 RuleConfigParserFactoryMap.getParserFactory(ext).createParser(),不再关心具体是哪个工厂类。

抽象工厂:当分类维度不止一个

前面的场景里,Parser 只有"文件格式"这一个分类维度。假设现在配置文件不只有规则配置(Rule),还有系统配置(System),两个维度交叉起来,用工厂方法就要写 Json/Xml/Yaml/Properties × Rule/System 一共 8 个工厂类。抽象工厂就是为了应对这种场景:让一个工厂负责创建一组相关的对象,而不是一种。

public interface IConfigParserFactory {
    IRuleConfigParser createRuleParser();
    ISystemConfigParser createSystemParser();
}
public class JsonConfigParserFactory implements IConfigParserFactory {
    public IRuleConfigParser createRuleParser() { return new JsonRuleConfigParser(); }
    public ISystemConfigParser createSystemParser() { return new JsonSystemConfigParser(); }
}
// XmlConfigParserFactory 同理,8个工厂类收敛成了4个

三种工厂的调用结构对比:

flowchart LR
    subgraph 简单工厂 一个工厂类按参数创建
        A1[调用方] --> A2[RuleConfigParserFactory]
        A2 --> A3[具体Parser]
    end
    subgraph 工厂方法 每种类型一个工厂类
        B1[调用方] --> B2[对应类型的ParserFactory]
        B2 --> B3[具体Parser]
    end
    subgraph 抽象工厂 一个工厂造一组相关对象
        C1[调用方] --> C2[JsonConfigParserFactory]
        C2 --> C3[RuleParser]
        C2 --> C4[SystemParser]
    end

三、怎么用:判断标准和容易混淆的边界

什么时候该引入工厂模式

对照 Day06 的判断标准,出现下面任意一种情况,才值得把创建逻辑抽到工厂类里,而不是提前套用:

  • 代码里存在 if-else 分支,需要根据条件动态创建不同的对象
  • 单个对象本身的创建过程就很复杂(要组合其他对象、要做额外初始化),哪怕不需要按类型区分

引入之后能拿到的收益,也可以反过来当作判断依据:封装变化(创建逻辑变了,调用方无感知)、代码复用(创建逻辑抽成独立类后能被多处复用)、隔离复杂性(调用方不用了解创建细节)、控制复杂度(原来的函数/类职责更单一了)。四条里一条都不占,多半就是过度设计。

简单工厂 vs 工厂方法:别看名字选,看创建逻辑复不复杂

创建逻辑简单(判断类型、直接 new)用简单工厂;创建逻辑复杂到值得拆分(一个工厂类会因此变得臃肿)才用工厂方法。工厂方法不是简单工厂的"升级版",是简单工厂扛不住的场景下的下一个台阶,两者没有谁比谁更正确。

工厂模式和 DI 容器不是一回事

Spring 这类 DI 容器的底层设计思路确实是基于工厂模式的——本质上是一个"大工厂",在应用启动时根据配置创建好所有需要的对象,用的时候直接从容器里取。但两者管的范围不一样:一个工厂类只负责某个类或某一组相关类的创建;DI 容器负责整个应用所有类对象的创建,还要管配置解析和对象生命周期(比如 Spring 里 scope=singleton/prototype、lazy-init、init-method/destroy-method 这些配置)。工厂类的创建逻辑是写死在代码里的,DI 容器的创建逻辑是从配置动态读出来的,这也是为什么工厂类的代码量会随着类的个数线性增长,而 DI 容器不会——它靠反射机制在运行时动态加载类、创建对象。


四、面试追问

Q1:简单工厂、工厂方法、抽象工厂,本质上的区别是什么?

简单工厂用一个工厂类按参数创建对应的实现类,适合创建逻辑简单的场景;工厂方法用多态把每种类型的创建逻辑拆到各自的工厂类里,适合单个对象创建过程本身就复杂、放一起会让工厂类臃肿的场景;抽象工厂应对的是对象有两个及以上分类维度的场景,让一个工厂负责创建一组相关的对象,避免维度交叉导致工厂类数量爆炸。三者共同的目标都是把"按什么条件创建哪个对象"从业务代码里挪出去。

Q2:工厂方法模式一定比简单工厂模式更好吗?

不是。工厂方法不是简单工厂的升级版,而是简单工厂在某类场景下会失效时的下一个台阶。创建逻辑简单时,硬套工厂方法只会多出一堆只有一个实现的工厂类,属于过度设计;只有当创建逻辑复杂到会让单个工厂类变得臃肿时,才值得拆分成多个工厂类。

Q3:引入工厂方法之后,为什么又出现了 "工厂的工厂"?它解决了什么问题?

工厂方法把对象的创建逻辑拆到了多个工厂类里,但调用方要先决定该 new 哪个工厂类,这段判断逻辑又耦合回了业务代码。"工厂的工厂"是再套一层简单工厂,专门负责创建这些工厂对象——因为工厂类通常不含状态,可以用 Map 缓存好直接复用,调用方就完全不用关心具体是哪个工厂类了。

Q4:什么时候该考虑引入工厂模式?

两种典型信号:一是代码里出现 if-else 分支,需要根据条件动态创建不同的对象;二是即便不需要按类型区分,单个对象本身的创建过程就足够复杂(组合其他对象、做额外初始化)。判断是否真的该用,还可以对照四个收益——封装变化、代码复用、隔离复杂性、控制复杂度——一条都占不上,大概率是过度设计。

Q5:工厂模式和 DI 容器有什么区别?

DI 容器的底层思路确实基于工厂模式,可以理解为一个管理整个应用所有对象的"大工厂",但两者管辖范围不同:工厂类只负责某个类或某一组相关类的创建,创建逻辑写死在代码里;DI 容器负责整个应用所有对象的创建、配置解析和生命周期管理,创建逻辑是从配置动态读取的,并且通过反射在运行时动态加载类,代码量不会随类的个数线性增长。


下一篇预告

Day11 建造者模式:什么时候构造函数该退休,交给 Builder。