本文正在参加「金石计划 . 瓜分6万现金大奖」
最近review代码,感觉工厂方法使用的不准确,正好以此为例聊一下工厂方法的演进。
实例
初始需求
假设我们有一个需求,需要根据不同的信号做不同的事情,如做饭、吃饭。
在此需求基础上,我们用Go实现比较简单,使用经典的简单工厂即可:
- 创建一个interface,包含参数检查、执行动作
- 创建做饭、吃饭类,实现interface中的两个函数
因为信号不同行为不同,根据查表法与switch有什么区别?,为后期扩展方便,我们选择switch方式。
需求进化
后面我们发现需求变了,要增加洗碗、拖地,而且这两个的操作和做饭也很相似。
这种情况下我们可以选择的方案有:
- 复用做饭类,在里面通过if判断是洗碗还是拖地
- 做饭、吃饭、洗碗、拖地完全独立,相互之间没有交集
我们肯定选择方案2,使用下面两个技巧使代码高内聚、低耦合
- 使用基类:如洗碗、拖地都需要用水清扫,这些相同操作,在基类中实现,洗碗、拖地类继承基类
- 提取公因子,将各个类共同的功能放到框架中,如在做之前都吼了一嗓子,“我不想工作“
简单工厂实现
关于工厂模式,大家可以看一下我的这篇文章Go设计模式(7)-工厂模式。简单工厂方法的UML图如下:
对于初始需求的代码实现如下所示:
package main
import "fmt"
/**
* @Author: Jason Pang
* @Description:
*/
type Life interface {
CheckParams() error //参数检查
Do() error //执行动作
}
type Cook struct {
}
/**
* @Author: Jason Pang
* @Description: 做饭参数检查
* @receiver c
* @return error
*/
func (c *Cook) CheckParams() error {
fmt.Println("cook 检查参数,食材准备完毕")
return nil
}
/**
* @Author: Jason Pang
* @Description: 开始做饭
* @receiver c
* @return error
*/
func (c *Cook) Do() error {
fmt.Println("不想工作")
fmt.Println("开始做饭")
return nil
}
type Eat struct {
}
/**
* @Author: Jason Pang
* @Description: 吃饭参数检查
* @receiver c
* @return error
*/
func (c *Eat) CheckParams() error {
fmt.Println("eat 检查参数,饭已做好,碗筷放好")
return nil
}
/**
* @Author: Jason Pang
* @Description: 开始吃饭
* @receiver c
* @return error
*/
func (c *Eat) Do() error {
fmt.Println("不想工作")
fmt.Println("开始吃饭")
return nil
}
/**
* @Description: 简单工厂
*/
type Factory struct {
}
func (simple *Factory) create(ext string) Life {
switch ext {
case "cook":
return &Cook{}
case "eat":
return &Eat{}
}
return nil
}
func main() {
//简单工厂使用代码
fmt.Println("------------简单工厂")
factory := &Factory{}
life := factory.create("cook")
if life != nil {
life.CheckParams()
life.Do()
}
}
简单工厂演进实现
需求演变之后,代码实现如下:
package main
import "fmt"
/**
* @Author: Jason Pang
* @Description:
*/
type Life interface {
CheckParams() error //参数检查
Do() error //执行动作
}
/**
* @Author: Jason Pang
* @Description: 基类
*/
type BaseLife struct {
}
/**
* @Author: Jason Pang
* @Description: 参数检查
* @receiver c
* @return error
*/
func (c *BaseLife) CheckParams() error {
fmt.Println("通用参数检查")
return nil
}
/**
* @Author: Jason Pang
* @Description: 开始做饭
* @receiver c
* @return error
*/
func (c *BaseLife) Do() error {
fmt.Println("用水处理")
return nil
}
type Cook struct {
}
/**
* @Author: Jason Pang
* @Description: 做饭参数检查
* @receiver c
* @return error
*/
func (c *Cook) CheckParams() error {
fmt.Println("cook 检查参数,食材准备完毕")
return nil
}
/**
* @Author: Jason Pang
* @Description: 开始做饭
* @receiver c
* @return error
*/
func (c *Cook) Do() error {
fmt.Println("开始做饭")
return nil
}
type Eat struct {
}
/**
* @Author: Jason Pang
* @Description: 吃饭参数检查
* @receiver c
* @return error
*/
func (c *Eat) CheckParams() error {
fmt.Println("eat 检查参数,饭已做好,碗筷放好")
return nil
}
/**
* @Author: Jason Pang
* @Description: 开始吃饭
* @receiver c
* @return error
*/
func (c *Eat) Do() error {
fmt.Println("开始吃饭")
return nil
}
/**
* @Author: Jason Pang
* @Description: 洗碗
*/
type Wash struct {
BaseLife
}
type Mop struct {
BaseLife
}
func (c *Mop) CheckParams() error {
fmt.Println("mop 检查参数,拖把是否存在")
return nil
}
/**
* @Description: 简单工厂
*/
type Factory struct {
}
func (simple *Factory) create(ext string) Life {
switch ext {
case "cook":
return &Cook{}
case "eat":
return &Eat{}
case "wash":
return &Wash{}
case "mop":
return &Mop{}
}
return nil
}
func EchoBeforeDo() {
fmt.Println("不想工作")
}
func main() {
//简单工厂使用代码
fmt.Println("------------简单工厂")
factory := &Factory{}
life := factory.create("mop")
if life != nil {
life.CheckParams()
EchoBeforeDo()
life.Do()
}
}
输出:
➜ myproject go run main.go
------------简单工厂
cook 检查参数,食材准备完毕
不想工作
开始做饭
➜ myproject go run main.go
------------简单工厂
eat 检查参数,饭已做好,碗筷放好
不想工作
开始吃饭
➜ myproject go run main.go
------------简单工厂
通用参数检查
不想工作
用水处理
➜ myproject go run main.go
------------简单工厂
mop 检查参数,拖把是否存在
不想工作
用水处理
大家可以看到,这种方案即保证了各个操作之间的独立,又复用了共同代码(通过基类和提取公因子)。
总结
使用工厂方法,有两个检验标准
- 具体产品类不应该相互之间关联
- 产品类里也不应该有相同的代码
随着对业务的理解,区分出变与不变的内容,不变的内容需要整合到框架中,不应该在各个产品类里。
产品类只需关注自己的逻辑,按照接口要求处理输入和返回值。这样今后即使有新功能接入,开发者也不需要关心整体框架,上手速度快、出问题的概率低。
如果有默认的逻辑操作能跑通整个流程,最好有一个基类实现这个逻辑,这样就能最大程度的进行复用。
开发过程中需要随着业务的变化和自己对业务的理解不断重构代码,这样才能让代码不成为屎山。但很多同学可能不敢重构,怕引起更多问题。其实这就和单元测试、自动化测试等关联起来了,只要质量保障的好,才能更放心的修改。我认为质量保障就是内功了,需要不断的坚持、不松懈,需要团队有很强的执行力,这是很难短时间被学去的,这便是护城河。
最后
大家如果喜欢我的文章,可以关注我的公众号(程序员麻辣烫)
我的个人博客为:shidawuhen.github.io/
往期文章回顾: