上一篇文章讲解了简单工厂和工厂方法,文章链接为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.代码实现
1 | package main |
通过修改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接口中增加了函数命名,其它都没有修改,变动不大。
我们构建类似功能的时候需要使用抽象工厂模式吗?我的理解是可以把模式的架子搭起来,尽量让模式对开发人员影响最小。
因为在项目生命周期中,更改存储可能性极低,如果确实支持多套存储,意味着对一个表增加操作,所有存储都需要同步增加该函数,维护成本太高了。但很难说没这个可能,所以可以搭架子,只管理一个存储即可。
至于对开发人员影响最小,是指使用的时候,开发人员能够方便获取表操作对象,而不必去理解抽象工厂模式,否则会得不偿失。毕竟合理的设计不包含过度设计和使用繁琐。