深入设计模式之工厂模式「附详细案例」

深入设计模式之工厂模式「附详细案例」

这是我写设计模式的第 3 篇,前 2 篇内容分别是

深入设计模式之适配器模式「GO 版本」 - 掘金

深入 GO 选项模式 - 掘金

若有兴趣,可以点链接看看。

写作背景

工厂模式在日常开发中非常常见,在日常开发中也看到不少使用工厂模式的代码,今天把我的想法和总结写下来提供给大家参考和讨论,一起学习。

名词解释、案例分析

工厂模式(Factory Design Pattern)是创建型设计模式,它提供了通用创建对象方法,具体创建哪种类型的对象是由工厂类决定。

工厂模式主要目的是将对象的创建和使用分离,调用方不需要知道创建的对象是什么,只需通过工厂类来获取所需的对象就可以了。

工厂类通常包含一个或多个方法,用来创建的各种类型的对象。这些方法可以接收参数,用来指定对象创建的方式,最后返回创建的对象。

工厂模式可以细分为:简单工厂(Simple Factory)、工厂方法(Factory Method)、抽象工厂(Abstract Factory)。

虽然划分了三种类型,但有些书籍是把"简单工厂"看作是"工厂方法的"子集、或者说是特例你可以参考:zh.wikipedia.org/wiki/%E5%B7...

下面我将会介绍这 3 种类型以及案例

SOP

我先介需求,后面案例中会一直使用哈,"SOP"也叫"营销自动化"。所谓 SOP,是 Standard Operating Procedure 三个单词中首字母的大写 ,即标准作业程序,指将某一事件的标准操作步骤和要求以统一的格式描述出来,用于指导和规范日常的工作标准作业程序_百度百科。比如你去餐厅,服务人员先给你拿菜单、点菜、上菜、买单这一套操作就是标准的流程。

"SOP"旨在帮助企业将复杂场景的用户运营策略自动化执行,并提供运营效果量化追踪的平台。比如:自动发短信、发消息等。

比如你要在情人节给朋友圈客户发送满减活动。你会经历下面这几步

1、 谁来发「员工」

2、 发给谁「客户」

3、 发什么「内容」

4、 何时发「时间」

5、 怎么发「通道」

今天案例主要围绕「通道」展开,通道可能是"短信通道"、"公众号群发通道"、"邮件通道"、"抖音私信通道"、"企业微信群发通道"等。。。

简单工厂

工厂类负责创建多个不同类型的对象。文字描述比较抽象,我通过上面"SOP"案例写几行代码解释下。

go 复制代码
// Sender 数据发送"接口"标准定义
type Sender interface {
	// SendMsg 发送消息
	SendMsg(ctx context.Context, targetID string, data interface{}) (interface{}, error)
}

// SMS 短信通道实现类
type SMS struct {
}

func NewSMS() Sender {
	return &SMS{}
}

// SendMsg 发送消息
func (c *SMS) SendMsg(ctx context.Context, targetID string, data interface{}) (interface{}, error) {
	// todo 通过短信通道触达客户逻辑省略
	fmt.Printf("推送信息,targetID=%v,data=%v\n", targetID, data)
	return data, nil
}

// Tiktok 抖音通道实现类
type Tiktok struct {
}

func NewTiktok() Sender {
	return &Tiktok{}
}

// SendMsg 发送消息
func (c *Tiktok) SendMsg(ctx context.Context, targetID string, data interface{}) (interface{}, error) {
	// todo 通过抖音通道触达客户逻辑省略
	return nil, nil
}

// WechatOfficial 公众号通道实现类
type WechatOfficial struct {
}

func NewWechatOfficial() Sender {
	return &WechatOfficial{}
}

// SendMsg 发送消息
func (c *WechatOfficial) SendMsg(ctx context.Context, targetID string, data interface{}) (interface{}, error) {
	// todo 通过公众号通道触达客户逻辑省略
	return nil, nil
}

// WeCom 企业微信通道实现类
type WeCom struct {
}

func NewWeCom() Sender {
	return &WeCom{}
}

// SendMsg 发送消息
func (c *WeCom) SendMsg(ctx context.Context, targetID string, data interface{}) (interface{}, error) {
	// todo 通过企业微信通道触达客户逻辑省略
	return nil, nil
}

下段代码定义了简单工厂类

go 复制代码
/*
	简单工厂实现方案
*/

type SenderType string

const (
	SMSSenderType            SenderType = "sms"
	TiktokSenderType         SenderType = "tiktok"
	WeComSenderType          SenderType = "wecom"
	WechatOfficialSenderType SenderType = "wechat_official"
)

// Mgr 工厂类
type Mgr struct {
}

func NewMgr() *Mgr {
	return &Mgr{}
}

func (m *Mgr) GetInstance(tp SenderType) (Sender, bool) {
	if tp == SMSSenderType {
		return NewSMS(), true
	} else if tp == TiktokSenderType {
		return NewTiktok(), true
	} else if tp == WeComSenderType {
		return NewWeCom(), true
	} else if tp == WechatOfficialSenderType {
		return NewWechatOfficial(), true
	}
	return nil, false
}

如果你了解 Java,应该知道工厂类都是以"Factory"这个单词结尾的,但也不是必须。代码中获取指定子类对象 GetSender() 也有命名为 CreateInstance()、NewInstance(),大家根据自己的习惯命名就可以了,没有啥特殊要求的。

简单工厂业务方的调用代码如下

go 复制代码
func TestSimpleFactory(t *testing.T) {
	sender, exists := NewMgr().GetInstance(SMSSenderType)
	if !exists {
		panic("instance 未定义")
	}
	sender.SendMsg(context.TODO(), "xxx", "你好")
}

上段日志打印信息如下

diff 复制代码
=== RUN   TestSimpleFactory
推送信息,targetID=xxx,data=你好
--- PASS: TestSimpleFactory (0.00s)
PASS

从案例中可以看出工厂类是将对象创建和使用分离,对于使用者来说不需要关系复杂的创建的逻辑。

上段工厂类的代码每次都需要 New 新对象,工厂类创建的对象是可以复用的,不必每次都创建,我们可以事先创建好缓存起来,用的时候从缓存中取出来直接用就可以了。

你可以根据具体场景来决定是否复用对象。

给大家留一个作业,尝试把工厂类改造下,把对象缓存起来即取即用,这也是工厂类的另一种实现方案(提示下,单例模式或者策略模式配合工厂模式结合)。

看到这里可能有人要问了,每次增加一个通道实现类就需要在工厂类中增加 if else。这不是违反开闭原则了啊。我说说我的理解,if else 本身并不可怕,如果通道比较少或者说经过长时间的迭代产品才会增加一个通道(增加不频繁),我觉得 if else 是没有问题的,牺牲一些扩展性换来可读性。

工厂方法

工厂方法定义了一个创建工厂对象的抽象工厂接口,由具体的工厂类来实现该接口,用来创建具体的通道对象。每个通道类都有对应的工厂类。这里有几个关键字如下:

1、 抽象工厂接口

2、 工厂类(实现抽象工厂接口的子类)

3、 通道类(就是实现 Sender 的子类)

简单工厂最后我不是说如果通道新增频率低采用 if else 方案是没有问题的吗?如果要去掉 if else 有哪些方案呢?(其实方案有好几种)今天先介绍用工厂方法实现。代码如下:

go 复制代码
/*
	工厂方法实现方案
*/

// IFactory 抽象工厂接口
type IFactory interface {
	// GetSender 创建工厂方类对象
	GetSender() Sender
}

// SMSFactory 短信工厂类实现了 IFactory 接口,创建短信通道类
type SMSFactory struct {
}

func NewSMSFactory() IFactory {
	return &SMSFactory{}
}

func (s *SMSFactory) GetSender() Sender {
	return NewSMS()
}

// TiktokFactory 抖音工厂类实现了 IFactory 接口,创建通抖音道类
type TiktokFactory struct {
}

func NewTiktokFactory() IFactory {
	return &TiktokFactory{}
}

func (s *TiktokFactory) GetSender() Sender {
	return NewTiktok()
}

// WeComFactory 企业微信工厂类实现了 IFactory 接口,创建通企业微信通道类
type WeComFactory struct {
}

func NewWeComFactory() IFactory {
	return &WeComFactory{}
}

func (s *WeComFactory) GetSender() Sender {
	return NewTiktok()
}

// WechatOfficialFactory 微信公众号工厂类实现了 IFactory 接口,创建通公众号通道类
type WechatOfficialFactory struct {
}

func NewWechatOfficialFactory() IFactory {
	return &WechatOfficialFactory{}
}

func (s *WechatOfficialFactory) GetSender() Sender {
	return NewTiktok()
}

工厂方法的实现新增了 IFactory 接口,SMSFactory、TiktokFactory、WeComFactory、WechatOfficialFactory 类分别实现了该接口成功创建了通道实现类。如果后面新增通道的同步增加 IFactory 实现类和 Sender 实现类就可以了。看起来好像没啥问题,但是你忽略了工厂子类的使用,接下来我贴一段代码看看业务方是如何使用的。

go 复制代码
func TestFactoryMethod(t *testing.T) {
	SendMsg(context.TODO(), SMSSenderType, "xxx", "成都欢迎您")
}

func SendMsg(ctx context.Context, tp SenderType, targetID string, data interface{}) (interface{}, error) {
	var (
		factory IFactory
	)

	if tp == SMSSenderType {
		factory = NewSMSFactory()
	} else if tp == TiktokSenderType {
		factory = NewTiktokFactory()
	} else if tp == WeComSenderType {
		factory = NewWeComFactory()
	} else if tp == WechatOfficialSenderType {
		factory = NewWechatOfficialFactory()
	} else {
		return nil, errors.New("未找到工厂类")
	}

	return factory.GetSender().SendMsg(ctx, targetID, data)
}

日志打印如下:

diff 复制代码
=== RUN   TestFactoryMethod
推送信息,targetID=xxx,data=成都欢迎您
--- PASS: TestFactoryMethod (0.00s)
PASS

代码上工厂对象的创建又耦合到 SendMsg 函数了,跟简单工厂的方法的代码基本一样,不但没有解决到问题,反而又引入了一个接口和实现类,变得更加复杂了,开发者开发过程还要写不少代码。如果要解决这个问题,只能引入额外的类或者函数来抽象。

讲完简单工厂和工厂方法,大家肯定疑惑了。工厂方法和简单工厂分别用在啥场景?

如果对象的创建逻辑比较简单,不涉及复杂的条件判断或者依赖关系,可以考虑使用简单工厂了。但是创建逻辑复杂,比如涉及多种条件判断或依赖关系或者组合其它对象建议使用工厂方法,实际在我的开发中我是没有遇到复杂的依赖关系的,用简单工厂就解决问题了。

抽象工厂

抽象工厂包含一个抽象工厂接口和多个工厂子类,每个具体工厂类负责创建一个完整的产品族。完整的产品族指的是一组相关或相互依赖的产品集合,这些产品在功能上彼此补充,共同构成一个完整的解决方案或系统。

还是以 SOP 为例子,现在的简单工厂或者工厂方法只能创建通道实现类,还有一些场景比如通过通道触达达客户后,需要保存一条触达到记录,由于每个平台返回的数据格式不同,要适配数据。这时候简单工厂和工厂方法就搞不定了。那应该怎们做呢?举一个简单案例。

go 复制代码
// Processor 定义了通道触达客户后,数据处理接口
type Processor interface {
	ConvertData(ctx context.Context, in interface{}) interface{}
}

// SMSProcessor 短信通道实现类
type SMSProcessor struct {
}

func NewSMSProcessor() Processor {
	return &SMSProcessor{}
}

func (s *SMSProcessor) ConvertData(ctx context.Context, in interface{}) interface{} {
	return in
}

// IAbstractFactory 定义抽象工厂处理接口,抽象工厂提供一组相同主题的接口实现
type IAbstractFactory interface {
	GetSender() Sender
	GetProcessor() Processor
}

// SMSAbstractFactory 短信工厂类实现了 IFactory 接口,创建短信通道类
type SMSAbstractFactory struct {
}

func NewSMSAbstractFactory() IAbstractFactory {
	return &SMSAbstractFactory{}
}

func (s *SMSAbstractFactory) GetSender() Sender {
	return NewSMS()
}

func (s *SMSAbstractFactory) GetProcessor() Processor {
	return NewSMSProcessor()
}

定义了 Processor 接口,通道触达后处理返回数据,SMSProcessor 实现了 Processor 接口。定义了 IAbstractFactory 抽象工厂接口,该接口提供获取 Sender 对象的 GetSender() 方法和获取 Processor 对象的 GetProcessor() 方法,该接口提供了一组同主题方法的定义。简单总结就是可以让一个工厂类负责创建多个不同类型的对象(Sender、Processor 等),而不是只创建一种。可以减少工厂类的个数。

下面看看业务方调用

go 复制代码
func TestAbstractFactory(t *testing.T) {
	Handle(context.TODO(), SMSSenderType, "1112", "您好")
}

func Handle(ctx context.Context, tp SenderType, targetID string, data interface{}) error {
	var (
		factory IAbstractFactory
	)

	if tp == SMSSenderType {
		factory = NewSMSAbstractFactory()
	} else {
		return errors.New("未找到工厂类")
	}
	// todo 省略其他实现类。。。自己补充

	resp, err := factory.GetSender().SendMsg(ctx, targetID, data)
	if err != nil {
		return err
	}

	d := factory.GetProcessor().ConvertData(ctx, resp)
	// todo 处理完数据后保存数据库
	fmt.Printf("d=%v", d)
	return nil
}

日志打印如下:

ini 复制代码
=== RUN   TestAbstractFactory
推送信息,targetID=1112,data=您好
d=您好--- PASS: TestAbstractFactory (0.00s)
PASS

抽象工厂的使用场景更少,至少在我的日常开发中是没有使用过的,大家了解下就好了。

总结

工厂模式关注的是对象的创建过程,它将创建过程封装到工厂类中,调用方不需要关心对象的具体创建过程,只需通过工厂方法或抽象工厂获取所需的对象即可。

工厂模式可以总结几个优势吧

1、 封装变化:例如更改创建对象的方式、添加新的对象类型等,可以通过修改工厂类来实现,而不需要修改调用者的代码。这种封装性可以保持调用者代码的稳定性,调用者无需关心对象创建逻辑的变化。

2、 代码复用:将创建对象的代码抽离到独立的工厂类中,可以在不同的地方复用该工厂类,而不需要重复编写创建对象的代码。这提高代码的复用,减少代码的冗余。

3、 屏蔽复杂性:工厂类封装复杂创建逻辑,包括条件判断、依赖管理等,调用者无需了解对象的具体创建过程。这种隔离性使得调用代码更加简洁清晰。

4、 降低复杂度:将对象的创建逻辑抽离到独立的工厂类中,原本的函数或类职责更单一、更专注,代码结构更加清晰,易于理解和维护。这有助于降低系统的复杂度,提高代码的可维护性和可扩展性。

思考题

工厂模式讲完了,大家发现没?单靠工厂模式写出来的代码总差点意思,比如 if else 还没有彻底解决、如何优化才能让代码更符合开闭原则等。大家可以思考下这个问题,后面讲策略模式再揭晓。

参考文献

zh.wikipedia.org/wiki/%E5%B7...

en.wikipedia.org/wiki/Factor...

相关推荐
网络安全queen23 分钟前
渗透测试面试问题
面试·职场和发展
疯一样的码农1 小时前
基于Spring Boot + Vue3实现的在线商品竞拍管理系统源码+文档
java·spring boot·后端
Y编程小白2 小时前
SpringBoot的pom.xml文件中,scope标签有几种配置?
xml·spring boot·后端
健康平安的活着3 小时前
springboot整合log4j2日志框架1
spring boot·后端·log4j
是一只派大鑫3 小时前
从头开始学MyBatis—04缓存、逆向工程、分页插件
java·后端·mybatis
andyweike3 小时前
设计模式的分类
设计模式
夏旭泽3 小时前
设计模式-策略模式
设计模式·策略模式
GoGeekBaird4 小时前
69天探索操作系统-第25天:文件系统基础
后端·操作系统
技术小泽4 小时前
代码思想之快慢路径
java·开发语言·spring boot·后端·设计规范
*长铗归来*4 小时前
ASP.NET Core Web API Hangfire
后端·c#·asp.net·.netcore