上一篇文章讲解了简单工厂和工厂方法,文章链接为Go设计模式(7)-工厂模式,这篇文章讲一下抽象工厂。抽象工厂不是很常用,主要是因为抽象工厂解决的场景比较特殊,实际开发中很难遇到,但抽象工厂提供了减少类个数、增加系统可维护性的思路,还是很值得借鉴的。
本文UML类图链接为:https://www.processon.com/view/link/6080def6079129456d4beecf
本文代码链接为:https://github.com/shidawuhen/asap/blob/master/controller/design/8abstractFactory.go
1.定义
1.1抽象工厂
抽象工厂:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
UML类图:
1.2分析
UML类图显示抽象工厂比较复杂,其实拆分一下能发现和工厂方法Go设计模式(7)-工厂模式区别不大。Client是调用方,可以不考虑。对于存在的AbstractProductA和AbstractProductB,如果我们去掉AbstractProductB和其子类,是不是就和工厂模式一样了?
在增加了AbstractProductB后,复杂度就上升了。抽象工厂的定义变为了”提供一个创建一系列相关对象的接口“,不再是工厂方法中的”定义一个用于创建对象的接口“,一个工厂有了创建多个对象的能力。
2.使用场景
在下面的讲解之前,我先给大家举个实例,方便讲解和理解。适合抽象工厂的实例不算多,不过好歹从《大话设计模式》上找到一个,虽然这种情况比较少,但也不是没有。
假设我们做了一个学生管理系统,系统有两张表,分别是学生表和成绩表,学生表有插入、更新操作,成绩表有插入、列表操作。系统是20年前建的,存储使用的是Access,现在打算改成MySQL,今后学校有钱了,打算换成Oracle。
这种情况使用工厂模式就不合适了,原因有两个
- 工厂模式返回的Product都有同一个父类,Product因为多态可以被随便替换。但是学生类和成绩类操作不一样,强制设置出一个父类,并不合理。
- 即使强制出一个父类,会导致生成4个工厂,如果表的个数增多(N)、存储类型增多(M),工厂类的个数为N*M,导致工厂类个数剧增。
仔细分析这个实例,其实有两个维度:
低维度的是学生类和成绩类,分别对应AbstractProductA和AbstractProductB,无论是哪种存储,操作是相同的,所以可以使用继承。
高维度的是存储方式,因为存储方式包含了对应低维度的学生类和成绩类,即ProductA1、ProductB1或者ProductA2、ProductB2。
抽象工厂ConcreteFactory是一个高维度的工厂,它代表的是存储方式,用于生成该存储方式下的所有低维度的类。所以ConcreteFactory需要生成对应的学生类和成绩类。
其实在理解了两个维度之后,抽象工厂的思想也很容易向工厂方法上靠近。上一篇文章Go设计模式(7)-工厂模式中写过,工厂方法的实现是:工厂方法先返回指定工厂,然后通过指定工厂创建ConcreteProduct。那么抽象工厂的实现是:抽象工厂先返回高维度的指定工厂,然后通过该工厂创建所有低维度的ConcreteProduct。
这种设计是合理的,首先在后期替换存储方式的时候,只需要统一修改创建的工厂即可,其它无需关注;其次无论增加低维度(科目类等)或者增加存储方式,工厂类变动很小,只需要增加函数(如对应增加低维度的科目类)或者增加工厂类(如对应增加高纬度的存储方式),Product类无法摆脱线性的增加(毕竟要编写新功能)。
3.代码实现
package main
import "fmt"
/// Product部分
/**
* @Description: 学生接口,定义了插入和更新功能
*/
type Student interface {
insert() bool
update() bool
}
/**
* @Description: Access操作Student类
*/
type AccessStudent struct {
}
/**
* @Description: 使用Access向Student表中插入数据
* @receiver a
* @return bool
*/
func (a *AccessStudent) insert() bool {
fmt.Println("AccessStudent insert")
return true
}
/**
* @Description: 使用Access向Student表中更新数据
* @receiver a
* @return bool
*/
func (a *AccessStudent) update() bool {
fmt.Println("AccessStudent update")
return true
}
/**
* @Description: MySQL操作Student类
*/
type MySQLStudent struct {
}
/**
* @Description: 使用MySQL向Student表中插入数据
* @receiver a
* @return bool
*/
func (m *MySQLStudent) insert() bool {
fmt.Println("MySQLStudent insert")
return true
}
/**
* @Description: 使用MySQL向Student表中更新数据
* @receiver a
* @return bool
*/
func (m *MySQLStudent) update() bool {
fmt.Println("MySQLStudent update")
return true
}
/**
* @Description: 成绩接口,定义了插入和列表功能
*/
type Score interface {
insert() bool
list() []int64
}
/**
* @Description: 使用Access操作Score类
*/
type AccessScore struct {
}
/**
* @Description: 使用Access向Score表中插入数据
* @receiver a
* @return bool
*/
func (a *AccessScore) insert() bool {
fmt.Println("AccessScore insert")
return true
}
/**
* @Description: 使用Access从Score表中获取成绩列表
* @receiver a
* @return []int64
*/
func (a *AccessScore) list() []int64 {
fmt.Println("AccessScore list")
return []int64{1, 2}
}
/**
* @Description: 使用MySQL操作Score类
*/
type MySQLScore struct {
}
/**
* @Description: 使用MySQL向Score表中插入数据
* @receiver a
* @return bool
*/
func (m *MySQLScore) insert() bool {
fmt.Println("MySQLScore insert")
return true
}
/**
* @Description: 使用MySQL从Score表中获取成绩列表
* @receiver a
* @return []int64
*/
func (m *MySQLScore) list() []int64 {
fmt.Println("MySQLScore list")
return []int64{1, 2}
}
/// Factory部分
/**
* @Description: 抽象工厂接口,代表高维度工厂,高维度工厂能够生成低维度对象
*/
type Factory interface {
createStudent() Student
createScore() Score
}
/**
* @Description: 高维度Access工厂
*/
type AccessFactory struct {
}
/**
* @Description: 高维度Access工厂,创建Access的Student对象
* @receiver a
* @return Student
*/
func (a *AccessFactory) createStudent() Student {
return &AccessStudent{}
}
/**
* @Description: 高维度Access工厂,创建Access的Score对象
* @receiver a
* @return Score
*/
func (a *AccessFactory) createScore() Score {
return &AccessScore{}
}
/**
* @Description: 高维度MySQL工厂
*/
type MySQLFactory struct {
}
/**
* @Description: 高维度MySQL工厂,创建MySQL的Student对象
* @receiver a
* @return Student
*/
func (m *MySQLFactory) createStudent() Student {
return &MySQLStudent{}
}
/**
* @Description: 高维度MySQL工厂,创建MySQL的Score对象
* @receiver a
* @return Score
*/
func (m *MySQLFactory) createScore() Score {
return &MySQLScore{}
}
/// 获得高维度工厂
func getFactory(storeType string) Factory {
switch storeType {
case "MySQL":
return &MySQLFactory{}
case "Access":
return &AccessFactory{}
}
return nil
}
func main() {
//抽象工厂使用代码
fmt.Println("------------抽象工厂")
factory := getFactory("MySQL")
if factory == nil {
fmt.Println("不支持该存储方式")
return
}
student := factory.createStudent()
score := factory.createScore()
student.insert()
student.update()
score.insert()
score.list()
}
通过修改storeType,会自动更换存储方式。如果将factory、student、score设置为全局的,则只需要在创建的时候进行更改就可以方便的更改存储方式了。
返回为:
➜ myproject go run main.go
------------抽象工厂
AccessStudent insert
AccessStudent update
AccessScore insert
AccessScore list
➜ myproject go run main.go
------------抽象工厂
MySQLStudent insert
MySQLStudent update
MySQLScore insert
MySQLScore list
总结
抽象工厂模式能够减少工厂类创建的数量,它更加体现了如何管理、分类信息的思想。
抽象工厂符合单一职责原则(每个表一个类)、里氏替换原则(使用继承使得无论是工厂还是Product,子类对象能够替换父类对象出现的任何地方)、依赖倒转原则(工厂类和AbstractProduct类关联,而不是和细节关联)。
至于开闭原则,增加新类和新存储方式,扩展是开放的,违背封闭性的位置在getFactory函数和增加Factory中的函数,前者可以使用配置文件解决,后者因为是Go语言,其实只是在Factory接口中增加了函数命名,其它都没有修改,变动不大。
我们构建类似功能的时候需要使用抽象工厂模式吗?我的理解是可以把模式的架子搭起来,尽量让模式对开发人员影响最小。
因为在项目生命周期中,更改存储可能性极低,如果确实支持多套存储,意味着对一个表增加操作,所有存储都需要同步增加该函数,维护成本太高了。但很难说没这个可能,所以可以搭架子,只管理一个存储即可。
至于对开发人员影响最小,是指使用的时候,开发人员能够方便获取表操作对象,而不必去理解抽象工厂模式,否则会得不偿失。毕竟合理的设计不包含过度设计和使用繁琐。
最后
大家如果喜欢我的文章,可以关注我的公众号(程序员麻辣烫)
我的个人博客为:https://shidawuhen.github.io/
往期文章回顾:
招聘
设计模式
- Go设计模式(8)-抽象工厂
- Go设计模式(7)-工厂模式
- Go设计模式(6)-单例模式
- Go设计模式(5)-类图符号表示法
- Go设计模式(4)-代码编写优化
- Go设计模式(4)-代码编写
- Go设计模式(3)-设计原则
- Go设计模式(2)-面向对象分析与设计
- Go设计模式(1)-语法
语言
架构
存储
网络
工具
读书笔记
思考