工厂模式

设计模式中的工厂模式是我们编写代码时常用的一种建造型模式,用于创建指定类的实例。

工厂模式解决了下面这几个问题:

  • 封装变化:创建逻辑有可能变化,封装成工厂类之后,创建逻辑的变更对调用者透明。
  • 代码复用:创建代码抽离到独立的工厂类之后可以复用。
  • 隔离复杂性:封装复杂的创建逻辑,调用者无需了解如何创建对象。
  • 控制复杂度:将创建代码抽离出来,让原本的函数或类职责更单一,代码更简洁。

一般情况下,工厂模式分为三种更加细分的类型:简单工厂、工厂方法和抽象工厂。

在这三种细分的工厂模式中,简单工厂、工厂方法原理比较简单,在实际的项目中也比较常用。而抽象工厂的原理稍微复杂点,在实际的项目中相对也不常用。

简单工厂

简单工厂就是用于返回一个对象的方法。在Go语言中,没有构造函数这一说法,所以定义一个结构体的时候一般会定义NewXXX函数来初始化结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type Animal interface {
Eat()
}

type Cat struct {
// 各种属性
}

func (c *Cat) Eat() {
fmt.Println("猫吃东西")
}

func NewCat() Animal {
return Cat{}
}

下面通过一个例子去讲解简单工厂模式。

根据配置文件的后缀(json、xml、yaml、properties),选择不同的解析器(JsonRuleConfigParser、XmlRuleConfigParser……),将存储在文件中的配置解析成内存对象 RuleConfig。

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
type RuleConfigParser interface {
Parser(b []byte) RuleConfig
}

// 以下是RuleConfigParser接口的各种实现,包括json、xml、yaml等
type JsonRuleConfigParser struct {}
func (j *JsonRuleConfigParser) Parser(b []byte) RuleConfig {
// json的解析逻辑...
return RuleConfig{}
}
func NewJsonParser() JsonRuleConfigParser {
return JsonRuleConfigParser{}
}

type XmlRuleConfigParser struct {}
func (j *XmlRuleConfigParser) Parser(b []byte) RuleConfig {
// xml的解析逻辑...
return RuleConfig{}
}
func NewXmlParser() XmlRuleConfigParser {
return XmlRuleConfigParser{}
}

// yaml、propertion也是类似代码,忽略
.....

func Load(f string) RuleConfig {
ex := getFileExtension(f)
var r RuleConfig
if ex == "json" {
parser = NewJsonParser()
} else if ex == "xml" {
parser = NewXmlParser()
} else if ex == "yaml" {
parser == NewYamlParser()
} else if ex == "properties" {
parser == NewProperties()
}
b = getFileContent(f)
return parser.Parser(b)
}


func getFileExtension(f string) string {
// 解析文件获取拓展名
return "json"
}

func getFileContent(f string) []byte {
// 模拟读取文件内容
return []byte(".....")
}

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

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
func Load(f string) RuleConfig {
ex := getFileExtension(f)
// 重构调用部分
parser := createParser(f)
if parser == nil {
panic("invalid parser")
}
b = getFileContent(f)
return parser.Parser(b)
}

// 重构的部分
func createParser(f string) RuleConfigParser {
if ex == "json" {
reutrn NewJsonParser()
} else if ex == "xml" {
reutrn NewXmlParser()
} else if ex == "yaml" {
reutrn NewYamlParser()
} else if ex == "properties" {
reutrn NewProperties()
} else {
return nil
}
}


func getFileExtension(f string) string {
// 解析文件获取拓展名
return "json"
}

func getFileContent(f string) []byte {
// 模拟读取文件内容
return []byte(".....")
}

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

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
func Load(f string) RuleConfig {
ex := getFileExtension(f)
// 重构调用部分
parser := factory.createParser(ex)
if parser == nil {
panic("invalid parser")
}
b = getFileContent(f)
return parser.Parser(b)
}


// 重构的部分
var factory RuleConfigParserFactory

type RuleConfigParserFactory struct {}

func (f *RuleConfigParserFactory) createParser(ex string) RuleConfigParser {
if ex == "json" {
reutrn NewJsonParser()
} else if ex == "xml" {
reutrn NewXmlParser()
} else if ex == "yaml" {
reutrn NewYamlParser()
} else if ex == "properties" {
reutrn NewProperties()
} else {
return nil
}
}

func getFileExtension(f string) string {
// 解析文件获取拓展名
return "json"
}

func getFileContent(f string) []byte {
// 模拟读取文件内容
return []byte(".....")
}

在上面的代码实现中,就实现了针对配置文件类型的不同,初始化不同的解析器的简单工厂模式。

除了上面的一种实现方式,还有另外一种实现方式。

在上面的代码实现中,我们每次调用 RuleConfigParserFactory 的 createParser() 的时候,都要创建一个新的 parser。实际上,如果 parser 可以复用,为了节省内存和对象创建的时间,我们可以将 parser 事先创建好缓存起来。当调用 createParser() 函数的时候,我们从缓存中取出 parser 对象直接使用。

这有点类似单例模式和简单工厂模式的结合,具体的代码实现如下所示:

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
func Load(f string) RuleConfig {
ex := getFileExtension(f)
// 重构调用部分
parser := factory.createParser(ex)
if parser == nil {
panic("invalid parser")
}
b = getFileContent(f)
return parser.Parser(b)
}



var factory = newRuleConfigParserFactory()

type RuleConfigParserFactory struct {
m map[string]RuleConfigParser
}

// 重构的部分
// 在构建对象的时候初始化每个parser
func newRuleConfigParserFactory() RuleConfigParserFactory {
m := make(map[string]RuleConfigParser)
m["json"] = NewJsonParser()
m["xml"] = NewXmlParser()
m["yaml"] = NewYamlParser()
m["properties"] = NewProperties()

return RuleConfigParserFactory{
m : m
}
}

func (f *RuleConfigParserFactory) createParser(ex string) RuleConfigParser {
parser,ok := f.m[ex]
if !ok {
return nil
}
return parser
}

func getFileExtension(f string) string {
// 解析文件获取拓展名
return "json"
}

func getFileContent(f string) []byte {
// 模拟读取文件内容
return []byte(".....")
}

对于上面两种简单工厂模式的实现方法,如果我们要添加新的 parser,那势必要改动到 RuleConfigParserFactory 的代码。

那这是不是违反开闭原则呢?实际上,如果不是需要频繁地添加新的 parser,只是偶尔修改一下 RuleConfigParserFactory 代码,稍微不符合开闭原则,也是完全可以接受的。

除此之外,在 RuleConfigParserFactory 的第一种代码实现中,有一组 if 分支判断逻辑,是不是应该用多态或其他设计模式来替代呢?

实际上,如果 if 分支并不是很多,代码中有 if 分支也是完全可以接受的。应用多态或设计模式来替代 if 分支判断逻辑,也并不是没有任何缺点的,它虽然提高了代码的扩展性,更加符合开闭原则,但也增加了类的个数,牺牲了代码的可读性。

工厂方法

如果我们非得要把if分支逻辑去掉,那该怎么办呢?

比较经典处理方法就是利用多态。按照多台的实现思路,对上面的代码进行改动:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 定义工厂接口
type RuleConfigParserFactory interface {
createParser() RuleConfigParser
}

// 定义json、yaml、xml各种工厂实现类
type JsonRuleConfigParserFactory struct {}

func (j *JsonRuleConfigParserFactory) createParser() RuleConfigparser {
return NewJsonParser()
}

func newJsonRuleConfigParserFactory() RuleConfigParserFactory {
return JsonRuleConfigParserFactory{}
}

// yaml、xml也是类似的写法...

实际上,这就是工厂方法模式的典型代码实现。这样当我们新增一种 parser 的时候,只需要新增一个实现了 IRuleConfigParserFactory 接口的 Factory 类即可。所以,工厂方法模式比起简单工厂模式更加符合开闭原则。

从上面的工厂方法的实现来看,一切都很完美,但是实际上存在挺大的问题。问题存在于这些工厂类的使用上。接下来,我们看一下,如何用这些工厂类来实现 RuleConfigSource 的 load() 函数。具体的代码如下所示:

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
func Load(f string) RuleConfig {
ex := getFileExtension(f)
// 重构后
var factory RuleConfigParserFactory
// 还是存在if判断
if ex == "json" {
factory = newJsonRuleConfigParserFactory()
} else if ex == "xml" {
factory = newXmlRuleConfigParserFactory()
} else if ex == "yaml" {
factory = newYamlRuleConfigParserFactory()
} else if ex == "properties" {
factory = newPropertiesRuleConfigParserFactory()
}

if factory == nil {
panic("")
}

parser := factory.createParser(f)
if parser == nil {
panic("invalid parser")
}
b = getFileContent(f)
return parser.Parser(b)
}

从上面的代码实现来看,工厂类对象的创建逻辑又耦合进了 load() 函数中,跟我们最初的代码版本非常相似,引入工厂方法非但没有解决问题,反倒让设计变得更加复杂了。那怎么来解决这个问题呢?

我们可以为工厂类再创建一个简单工厂,也就是工厂的工厂,用来创建工厂类对象。

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
func Load(f string) RuleConfig {
ex := getFileExtension(f)
// 重构后
parserFactory := factory.createParser(ex)
if parserFactory == nil {
panic("")
}

parser := parserFactory.createParser()
if parser == nil {
panic("invalid parser")
}
b = getFileContent(f)
return parser.Parser(b)
}

// 重构的部分
var factory = newRuleConfigParserFactoryMap()
type RuleConfigParserFactoryMap struct{
m map[string]RuleConfigParserFatory
}


// 在构建对象的时候初始化每个parser
func newRuleConfigParserFactoryMap() RuleConfigParserFactoryMap {
m := make(map[string]RuleConfigParser)
m["json"] = NewJsonParserFactory()
m["xml"] = NewXmlParserFactory()
m["yaml"] = NewYamlParserFactory()
m["properties"] = NewPropertiesFactory()

return RuleConfigParserFactory{
m : m
}
}


func (f *RuleConfigParserFactoryMap) createParser(ex string) RuleConfigParser {
parser,ok := f.m[ex]
if !ok {
return nil
}
return parser
}

当我们需要添加新的规则配置解析器的时候,我们只需要创建新的 parser 类和 parser factory 类,并且在 RuleConfigParserFactoryMap 类中,将新的 parser factory 对象添加到 cachedFactories 中即可。代码的改动非常少,基本上符合开闭原则。

实际上,对于规则配置文件解析这个应用场景来说,工厂模式需要额外创建诸多 Factory 类,也会增加代码的复杂性,而且,每个 Factory 类只是做简单的 new 操作,功能非常单薄(只有一行代码),也没必要设计成独立的类,所以,在这个应用场景下,简单工厂模式简单好用,比工厂方法模式更加合适。

简单工厂和工厂方法的使用时机

之所以将某个代码块剥离出来,独立为函数或者类,原因是这个代码块的逻辑过于复杂,剥离之后能让代码更加清晰,更加可读、可维护。

但是,如果代码块本身并不复杂,就几行代码而已,我们完全没必要将它拆分成单独的函数或者类。

基于这个设计思想,当对象的创建逻辑比较复杂,不只是简单的 new 一下就可以,而是要组合其他类对象,做各种初始化操作的时候,我们推荐使用工厂方法模式,将复杂的创建逻辑拆分到多个工厂类中,让每个工厂类都不至于过于复杂。而使用简单工厂模式,将所有的创建逻辑都放到一个工厂类中,会导致这个工厂类变得很复杂。

除此之外,在某些场景下,如果对象不可复用,那工厂类每次都要返回不同的对象。如果我们使用简单工厂模式来实现,就只能选择第一种包含 if 分支逻辑的实现方式。如果我们还想避免烦人的 if-else 分支逻辑,这个时候,我们就推荐使用工厂方法模式。

抽象工厂

抽象工厂模式的应用场景比较特殊,没有前两种常用,所以这里只用提一下。

在简单工厂和工厂方法中,类只有一种分类方式。

比如,在规则配置解析那个例子中,解析器类只会根据配置文件格式(Json、Xml、Yaml……)来分类。

但是,如果类有两种分类方式,比如,我们既可以按照配置文件格式来分类,也可以按照解析的对象(Rule 规则配置还是 System 系统配置)来分类,那就会对应下面这 8 个 parser 类。

1
2
3
4
5
6
7
8
9
10
11
针对规则配置的解析器:基于接口IRuleConfigParser
JsonRuleConfigParser
XmlRuleConfigParser
YamlRuleConfigParser
PropertiesRuleConfigParser

针对系统配置的解析器:基于接口ISystemConfigParser
JsonSystemConfigParser
XmlSystemConfigParser
YamlSystemConfigParser
PropertiesSystemConfigParser

针对这种特殊的场景,如果还是继续用工厂方法来实现的话,我们要针对每个 parser 都编写一个工厂类,也就是要编写 8 个工厂类。如果我们未来还需要增加针对业务配置的解析器(比如 IBizConfigParser),那就要再对应地增加 4 个工厂类。而我们知道,过多的类也会让系统难维护。这个问题该怎么解决呢?

抽象工厂就是针对这种非常特殊的场景而诞生的。我们可以让一个工厂负责创建多个不同类型的对象(IRuleConfigParser、ISystemConfigParser 等),而不是只创建一种 parser 对象。