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

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

深入设计模式-适配器模式「GO 版本」-CSDN博客

深入 GO 选项模式-CSDN博客

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

写作背景

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

名词解释、案例分析

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

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

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

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

虽然划分了三种类型,但有些书籍是把”简单工厂“看作是“工厂方法的“子集、或者说是特例你可以参考:https://zh.wikipedia.org/wiki/%E5%B7%A5%E5%8E%82%E6%96%B9%E6%B3%95#%E5%B7%A5%E5%8E%82

本文介绍案例主要围绕「通道」触达用户,通道可能是”短信通道“、”公众号群发通道“、“邮件通道”、“抖音私信通道”、“企业微信群发通道“等。。。假设你做一个解决方案整合通道能力给客户提供更加便利触达用户能力。

简单工厂

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

// 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
}

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

/*
	简单工厂实现方案
*/

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(),大家根据自己的习惯命名就可以了,没有啥特殊要求的。

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

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

上段日志打印信息如下

=== 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 有哪些方案呢?(其实方案有好几种)今天先介绍用工厂方法实现。代码如下:

/*
	工厂方法实现方案
*/

// 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 实现类就可以了。看起来好像没啥问题,但是你忽略了工厂子类的使用,接下来我贴一段代码看看业务方是如何使用的。

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)
}

日志打印如下:

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

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

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

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

抽象工厂

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

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

// 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 等),而不是只创建一种。可以减少工厂类的个数。

下面看看业务方调用

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
}

日志打印如下:

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

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

总结

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

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

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

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

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

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

思考题

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

参考文献

https://zh.wikipedia.org/wiki/%E5%B7%A5%E5%8E%82%E6%96%B9%E6%B3%95#%E7%AE%80%E5%8D%95%E5%B7%A5%E5%8E%82​zh.wikipedia.org/wiki/%E5%B7%A5%E5%8E%82%E6%96%B9%E6%B3%95#%E7%AE%80%E5%8D%95%E5%B7%A5%E5%8E%82

https://en.wikipedia.org/wiki/Factory_method_pattern#C#​en.wikipedia.org/wiki/Factory_method_pattern#C#

公众号地址

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

  • 25
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值