工厂方法演进

64 阅读5分钟

本文正在参加「金石计划 . 瓜分6万现金大奖」

最近review代码,感觉工厂方法使用的不准确,正好以此为例聊一下工厂方法的演进。

实例

初始需求

假设我们有一个需求,需要根据不同的信号做不同的事情,如做饭、吃饭。

在此需求基础上,我们用Go实现比较简单,使用经典的简单工厂即可:

  • 创建一个interface,包含参数检查、执行动作
  • 创建做饭、吃饭类,实现interface中的两个函数

因为信号不同行为不同,根据查表法与switch有什么区别?,为后期扩展方便,我们选择switch方式。

需求进化

后面我们发现需求变了,要增加洗碗、拖地,而且这两个的操作和做饭也很相似。

这种情况下我们可以选择的方案有:

  1. 复用做饭类,在里面通过if判断是洗碗还是拖地
  2. 做饭、吃饭、洗碗、拖地完全独立,相互之间没有交集

我们肯定选择方案2,使用下面两个技巧使代码高内聚、低耦合

  1. 使用基类:如洗碗、拖地都需要用水清扫,这些相同操作,在基类中实现,洗碗、拖地类继承基类
  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 检查参数,拖把是否存在
不想工作
用水处理

大家可以看到,这种方案即保证了各个操作之间的独立,又复用了共同代码(通过基类和提取公因子)。

总结

使用工厂方法,有两个检验标准

  1. 具体产品类不应该相互之间关联
  2. 产品类里也不应该有相同的代码

随着对业务的理解,区分出变与不变的内容,不变的内容需要整合到框架中,不应该在各个产品类里。

产品类只需关注自己的逻辑,按照接口要求处理输入和返回值。这样今后即使有新功能接入,开发者也不需要关心整体框架,上手速度快、出问题的概率低。

如果有默认的逻辑操作能跑通整个流程,最好有一个基类实现这个逻辑,这样就能最大程度的进行复用。

开发过程中需要随着业务的变化和自己对业务的理解不断重构代码,这样才能让代码不成为屎山。但很多同学可能不敢重构,怕引起更多问题。其实这就和单元测试、自动化测试等关联起来了,只要质量保障的好,才能更放心的修改。我认为质量保障就是内功了,需要不断的坚持、不松懈,需要团队有很强的执行力,这是很难短时间被学去的,这便是护城河。

代码位置:github.com/shidawuhen/…

最后

大家如果喜欢我的文章,可以关注我的公众号(程序员麻辣烫)

我的个人博客为:shidawuhen.github.io/

往期文章回顾:

  1. 设计模式

  2. 招聘

  3. 思考

  4. 存储

  5. 算法系列

  6. 读书笔记

  7. 小工具

  8. 架构

  9. 网络

  10. Go语言