设计模式之美 CrayonQ7

工厂模式

工厂模式一般分为:简单工厂、工厂方法和抽象工厂

简单工厂(Simple Factory)

首先,我们来看,什么是简单工厂模式。我们通过一个例子来解释一下。
在下面这段代码中,我们根据配置文件的后缀(json、xml、yaml、properties),选择不同的解析器(JsonRuleConfigParser、XmlRuleConfigParser……),将存储在文件中的配置解析成内存对象 RuleConfig

RuleConfigSource = class("RuleConfigSource")

function RuleConfigSource:load(ruleConfigFilePath)
    local parser = nil
    -- 这里的funcForJudge是伪代码 指的是一些复杂的if条件判断
    -- 这里的doSthToInit也是伪代码,指的是new出来后还会对对象做一些复杂的处理
    if funcForJudgeJSON(ruleConfigFilePath) then
        parser = JsonRuleConfigParser.new()
        doSthToInitJson(parser)
    elseif funcForJudgeXML(ruleConfigFilePath) then
        parser = XmlRuleConfigParser.new()
        doSthToInitXml(parser)
    elseif funcForJudgeYAML(ruleConfigFilePath) then
        parser = YamlRuleConfigParser.new()
        doSthToInitYaml(parser)
    elseif funcForJudgePROP(ruleConfigFilePath) then
        parser = PropertiesRuleConfigParser.new()
        doSthToInitProp(parser)
    else 
        error("Rule config file format is not supported: " + ruleConfigFilePath))
    end
    -- 从ruleConfigFilePath文件中读取配置文本到configText中
    local configText = ReadContentFrom(ruleConfigFilePath)
    -- 将configText传入解析器中
    local ruleConfig = parser.parse(configText)
    return ruleConfig
end

为了让代码逻辑更加清晰,可读性更好,我们要善于将功能独立的代码块封装成函数。按照这个设计思路,我们可以将代码中涉及 parser 创建的部分逻辑剥离出来,抽象成 createParser() 函数。重构之后的代码如下所示:

function RuleConfigSource:load(ruleConfigFilePath)
    local parser = self:createParser(ruleConfigFilePath)
    if not parser then
        error("Rule config file format is not supported: " + ruleConfigFilePath))
    end
    -- 从ruleConfigFilePath文件中读取配置文本到configText中
    local configText = ReadContentFrom(ruleConfigFilePath)
    -- 将configText传入解析器中
    local ruleConfig = parser.parse(configText)
    return ruleConfig
end

function RuleConfigSource:createParser(path)
    local parser = nil
    if funcForJudgeJSON(path) then
        parser = JsonRuleConfigParser.new()
        doSthToInitJson(parser)
    elseif funcForJudgeXML(path) then
        parser = XmlRuleConfigParser.new()
        doSthToInitXml(parser)
    elseif funcForJudgeYAML(path) then
        parser = YamlRuleConfigParser.new()
        doSthToInitYaml(parser)
    elseif funcForJudgePROP(path) then
        parser = PropertiesRuleConfigParser.new()
        doSthToInitProp(parser)
    end
    return parser
end

为了让类的职责更加单一、代码更加清晰,我们还可以进一步将 createParser() 函数剥离到一个独立的类中,让这个类只负责对象的创建。而这个类就是我们现在要讲的简单工厂模式类。具体的代码如下所示:

function RuleConfigSource:load(ruleConfigFilePath)
    local parser = RuleConfigParserFactory.createParser(ruleConfigFilePath)
    if not parser then
        error("Rule config file format is not supported: " + ruleConfigFilePath))
    end
    -- 从ruleConfigFilePath文件中读取配置文本到configText中
    local configText = ReadContentFrom(ruleConfigFilePath)
    -- 将configText传入解析器中
    local ruleConfig = parser.parse(configText)
    return ruleConfig
end

RuleConfigParserFactory = class("RuleConfigParserFactory")

function RuleConfigParserFactory:createParser(path)
    local parser = nil
    if funcForJudgeJSON(path) then
        parser = JsonRuleConfigParser.new()
        doSthToInitJson(parser)
    elseif funcForJudgeXML(path) then
        parser = XmlRuleConfigParser.new()
        doSthToInitXml(parser)
    elseif funcForJudgeYAML(path) then
        parser = YamlRuleConfigParser.new()
        doSthToInitYaml(parser)
    elseif funcForJudgePROP(path) then
        parser = PropertiesRuleConfigParser.new()
        doSthToInitProp(parser)
    end
    return parser
end

在上面的代码实现中,我们每次调用 RuleConfigParserFactory 的 createParser() 的时候,都要创建一个新的 parser。实际上,如果 parser 可以复用,为了节省内存和对象创建的时间,我们可以将 parser 事先创建好缓存起来。当调用 createParser() 函数的时候,我们从缓存中取出 parser 对象直接使用。
这有点类似单例模式和简单工厂模式的结合,具体的代码实现如下所示。在接下来的讲解中,我们把上一种实现方法叫作简单工厂模式的第一种实现方法,把下面这种实现方法叫作简单工厂模式的第二种实现方法。

RuleConfigParserFactory = class("RuleConfigParserFactory")
function RuleConfigParserFactory:ctor()
    self.parserList = {
        ["json"] = JsonRuleConfigParser.new(),
        ["xml"] = XmlRuleConfigParser.new(),
        ["yaml"] = YamlRuleConfigParser.new(),
        ["properties"] = PropertiesRuleConfigParser.new(),
    }
end

function RuleConfigParserFactory:createParser(path)
    if path then
        local parser = nil
        if funcForJudgeJSON(path) then
            parser = self.parserList["json"]
            doSthToInitJson(parser)
        elseif funcForJudgeXML(path) then
            parser = self.parserList["xml"]
            doSthToInitXml(parser)
        elseif funcForJudgeYAML(path) then
            parser = self.parserList["yaml"]
            doSthToInitYaml(parser)
        elseif funcForJudgePROP(path) then
            parser = self.parserList["properties"]
            doSthToInitProp(parser)
        end
        return parser
    end
end

对于上面两种简单工厂模式的实现方法,如果我们要添加新的 parser或者修改初始化处理,那势必要改动到 RuleConfigParserFactory 的代码,这个里面除了有我当前自己想改的 parser 的初始化代码,还有其他的,那这是不是违反开闭原则呢?实际上,如果不是需要频繁地添加新的 parser,只是偶尔修改一下 RuleConfigParserFactory 代码,稍微不符合开闭原则,也是完全可以接受的。
总结一下,尽管简单工厂模式的代码实现中,有多处 if 分支判断逻辑,违背开闭原则,但权衡扩展性和可读性,这样的代码实现在大多数情况下(比如,不需要频繁地添加 parser,也没有太多的 parser)是没有问题的。

工厂方法(Factory Method)

如果在上面的例子中 funcForJudgedoSthToInit 很多且很复杂,那么将这些处理全部放到一个类里面去处理,就会显得很复杂,且新增一个 parser时,就会很麻烦。此时我们可以通过多态的思路将上面的 if 分支进行重构:
我们将上面 if 分支里面的代码块抽离出来,独立为一个工厂类,里面是对象创建以及所有的创建处理,然后再实现一个这些工厂类的工厂,用于管理这些工厂。

BaseFactory = class("BaseFactory")
function BaseFactory:createParser()
    -- 将创建parser的处理,即doSthToInit放到createParser中
end

JsonRuleConfigParserFactory = class("JsonRuleConfigParserFactory", BaseFactory)
function JsonRuleConfigParserFactory:createParser()
    local parser = self.parser or JsonRuleConfigParser.new()
    doSthToInitJson(parser)
    return parser
end

XmlRuleConfigParserFactory = class("XmlRuleConfigParserFactory", BaseFactory)
function XmlRuleConfigParserFactory:createParser()
    local parser = self.parser or XmlRuleConfigParser.new()
    doSthToInitXml(parser)
    return parser
end

YamlRuleConfigParserFactory = class("YamlRuleConfigParserFactory", BaseFactory)
function YamlRuleConfigParserFactory:createParser()
    local parser = self.parser or YamlRuleConfigParser.new()
    doSthToInitYaml(parser)
    return parser
end

PropertiesRuleConfigParserFactory = class("PropertiesRuleConfigParserFactory", BaseFactory)
function PropertiesRuleConfigParserFactory:createParser()
    local parser = self.parser or PropertiesRuleConfigParser.new()
    doSthToInitProp(parser)
    return parser
end

RuleConfigParserFactoryMap = class("RuleConfigParserFactoryMap") -- 工厂的工厂
function RuleConfigParserFactoryMap:ctor()
    self.cachedFactories = {
        ["json"] = JsonRuleConfigParserFactory.new(),
        ["xml"] = XmlRuleConfigParserFactory.new(),
        ["yaml"] = YamlRuleConfigParserFactory.new(),
        ["properties"] = PropertiesRuleConfigParserFactory.new(),
    }
end

function RuleConfigParserFactoryMap:getParserFactory(path)
    if path then
        local parserFactory = nil
        if funcForJudgeJSON(path) then
            parserFactory = self.cachedFactories["json"]
        elseif funcForJudgeXML(path) then
            parserFactory = self.cachedFactories["xml"]
        elseif funcForJudgeYAML(path) then
            parserFactory = self.cachedFactories["yaml"]
        elseif funcForJudgePROP(path) then
            parserFactory = self.cachedFactories["properties"]
        end
        return parserFactory
    end
end

function RuleConfigSource:load(ruleConfigFilePath)
    local parserFactory = RuleConfigParserFactoryMap:getParserFactory(ruleConfigFilePath)
    if not parserFactory then
        error("Rule config file format is not supported: " + ruleConfigFilePath))
    end
    local parser = parserFactory:createParser()
    -- 从ruleConfigFilePath文件中读取配置文本到configText中
    local configText = ReadContentFrom(ruleConfigFilePath)
    -- 将configText传入解析器中
    local ruleConfig = parser.parse(configText)
    return ruleConfig
end

当我们需要添加新的规则配置解析器的时候,我们只需要创建新的 parser 类和 parser factory 类,并且在 RuleConfigParserFactoryMap 类中,将新的 parser factory 对象添加到 cachedFactories 中即可。代码的改动非常少,基本上符合开闭原则。如果我们要修改某个 parser 的初始化处理,也只需要找到其自己对应的 Factory 即可

那什么时候该用工厂方法模式,而非简单工厂模式呢?

我们前面提到,之所以将某个代码块剥离出来,独立为函数或者类,原因是这个代码块的逻辑过于复杂,剥离之后能让代码更加清晰,更加可读、可维护。但是,如果代码块本身并不复杂,就几行代码而已,我们完全没必要将它拆分成单独的函数或者类。
当对象的创建逻辑比较复杂,不只是简单的 new 一下就可以,而是要组合其他类对象,做各种初始化操作的时候,我们推荐使用工厂方法模式,将复杂的创建逻辑拆分到多个工厂类中,让每个工厂类都不至于过于复杂。而使用简单工厂模式,将所有的创建逻辑都放到一个工厂类中,会导致这个工厂类变得很复杂。
除此之外,在某些场景下,如果对象不可复用,那工厂类每次都要返回不同的对象。如果我们使用简单工厂模式来实现,就只能选择第一种包含 if 分支逻辑的实现方式。如果我们还想避免烦人的 if-else 分支逻辑,这个时候,我们就推荐使用工厂方法模式。

抽象工厂(Abstract Factory)

在简单工厂和工厂方法中,类只有一种分类方式。比如,在规则配置解析那个例子中,解析器类只会根据配置文件格式(Json、Xml、Yaml……)来分类。但是,如果类有两种分类方式,比如,我们既可以按照配置文件格式来分类,也可以按照解析的对象(Rule 规则配置还是 System 系统配置)来分类,如果还是继续用工厂方法来实现的话,我们要针对每个 parser 都编写一个工厂类,也就是要编写 8 个工厂类。如果我们未来还需要增加针对业务配置的解析器(比如 IBizConfigParser),那就要再对应地增加 4 个工厂类。而我们知道,过多的类也会让系统难维护。这个问题该怎么解决呢?
抽象工厂就是针对这种非常特殊的场景而诞生的。我们可以让一个工厂负责创建多个不同类型的对象(IRuleConfigParser、ISystemConfigParser 等),而不是只创建一种 parser 对象。这样就可以有效地减少工厂类的个数:

BaseFactory = class("BaseFactory")
function BaseFactory:createRuleParser()
    -- 将创建parser的处理,即doSthToInit放到createParser中
end
function BaseFactory:createSystemParser()
    -- 将创建parser的处理,即doSthToInit放到createParser中
end

JsonConfigParserFactory = class("JsonConfigParserFactory", BaseFactory)
function JsonConfigParserFactory:createRuleParser()
    local parser = self.parser or JsonRuleConfigParser.new()
    doSthToInitRuleJson(parser)
    return parser
end

function JsonConfigParserFactory:createSystemParser()
    local parser = self.parser or JsonSystemConfigParser.new()
    doSthToInitSystemJson(parser)
    return parser
end

总结

工厂方法模式:定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法让类把实例化推迟到子类
抽象工厂模式:提供一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类

  1. 简单工厂(Simple Factory 当每个对象的创建逻辑都比较简单的时候,将多个对象的创建逻辑放到一个工厂类中。
  2. 工厂方法(Factory Method) 当每个对象的创建逻辑都比较复杂的时候,为了避免设计一个过于庞大的简单工厂类时,将创建逻辑拆分得更细,每个对象的创建逻辑独立到各自的工厂类中。并设计一个工厂的工厂来作为对外的创建接口。
  3. 抽象工厂(Abstract Factory)- 不常用 让一个工厂负责创建多个不同类型的对象

应用

我们项目里面的场景对象类就使用了抽象工厂的方式: 场景类工厂
可以看到我们外部通过使用 SceneFactory 这个单例的接口去获取不同的工厂,然后再由工厂生成自己的对象。对于 MonsterFactory ,它能生产至少5种不同的对象。
当我们要新增一个场景类对象时,我们只需要为其新增一个工厂,并将该工厂添加到 SceneFactory 中即可,创建该场景类的所有操作都在其自己的工厂中进行,符合开闭原则。