问答文章1 问答文章501 问答文章1001 问答文章1501 问答文章2001 问答文章2501 问答文章3001 问答文章3501 问答文章4001 问答文章4501 问答文章5001 问答文章5501 问答文章6001 问答文章6501 问答文章7001 问答文章7501 问答文章8001 问答文章8501 问答文章9001 问答文章9501

【Go实现】实践GoF的23种设计模式:工厂方法模式

发布网友 发布时间:2024-09-30 14:28

我来回答

1个回答

热心网友 时间:2024-11-26 18:35

上一篇:【Go实现】实践GoF的23种设计模式:建造者模式

简单的分布式应用系统(示例代码工程):https://github.com/ruanrunxue/Practice-Design-Pattern--Go-Implementation

简述

工厂方法模式(Factory Method Pattern)跟上一篇讨论的建造者模式类似,都是将对象创建的逻辑封装起来,为使用者提供一个简单易用的对象创建接口。两者在应用场景上稍有区别,建造者模式常用于需要传递多个参数来进行实例化的场景;工厂方法模式常用于不指定对象具体类型的情况下创建对象的场景。

UML 结构代码实现示例

在简单的分布式应用系统(示例代码工程)中,我们设计了 Sidecar 边车模块, Sidecar 的作用是为了给原生的 Socket 增加额外的功能,比如流控、日志等。

Sidecar 模块的设计运用了装饰者模式,修饰的是 Socket 。所以客户端其实是把 Sidecar 当成是 Socket 来使用了,比如:

// demo/network/http/http_client.gopackage http// 创建一个新的HTTP客户端,以Socket接口作为入参func NewClient(socket network.Socket, ip string) (*Client, error) {... // 一些初始化逻辑 return client, nil}// 使用NewClient时,我们可以传入Sidecar来给Http客户端附加额外的流控功能client, err := http.NewClient(sidecar.NewFlowCtrlSidecar(network.DefaultSocket()), "192.168.0.1")

在服务消息中介中,每次收到上游服务的 HTTP 请求,都会调用 http.NewClient 来创建一个 HTTP 客户端,并通过它将请求转发给下游服务:

type ServiceMediator struct {... server *http.Server}// Forward 转发请求,请求URL为 /{serviceType}+ServiceUri 的形式,如/serviceA/api/v1/taskfunc (s *ServiceMediator) Forward(req *http.Request) *http.Response {...// 发现下游服务的目的IP地址dest, err := s.discovery(svcType)// 创建HTTP客户端,硬编码sidecar.NewFlowCtrlSidecar(network.DefaultSocket())client, err := http.NewClient(sidecar.NewFlowCtrlSidecar(network.DefaultSocket()), s.localIp)// 通过HTTP客户端转发请求resp, err := client.Send(dest, forwardReq)...}

在上述实现中,我们在调用 http.NewClient 时把 sidecar.NewFlowCtrlSidecar(network.DefaultSocket()) 硬编码进去了,那么如果以后要扩展 Sidecar ,就得修改这段代码逻辑,这违反了开闭原则 OCP。

有经验的同学可能会想到,可以通过让 ServiceMediator 依赖 Socket 接口,在 Forward 方法调用 http.NewClient 时把 Socket 接口作为入参;然后在 ServiceMediator 初始化时,将具体类型的 Sidecar 注入到 ServiceMediator 中:

type ServiceMediator struct {... server *http.Server// 依赖Socket抽象接口socket network.Socket}// Forward 转发请求,请求URL为 /{serviceType}+ServiceUri 的形式,如/serviceA/api/v1/taskfunc (s *ServiceMediator) Forward(req *http.Request) *http.Response {...// 发现下游服务的目的IP地址dest, err := s.discovery(svcType)// 创建HTTP客户端,将s.socket抽象接口作为入参client, err := http.NewClient(s.socket, s.localIp)// 通过HTTP客户端转发请求resp, err := client.Send(dest, forwardReq)...}// 在ServiceMediator初始化时,将具体类型的Sidecar注入到ServiceMediator中mediator := &ServiceMediator{socket: sidecar.NewFlowCtrlSidecar(network.DefaultSocket())}

上述的修改,从原来依赖具体,改成了依赖抽象,符合了开闭原则。

但是, Forward 方法存在并发调用的场景,因此它希望每次被调用时都创建一个新的 Socket/Sidecar 来完成网络通信,否则就需要加锁来保证并发安全。而上述的修改会导致在 ServiceMediator 的生命周期内都使用同一个 Socket/Sidecar,显然不符合要求。

因此,我们需要一个方法,既能够满足开闭原则,而且在每次调用Forward 方法时也能够创建新的 Socket/Sidecar 实例。工厂方法模式恰好就能满足这两点要求,下面我们通过它来完成代码的优化。

实现// demo/sidecar/sidecar_factory.go// 关键点1: 定义一个Sidecar工厂抽象接口type Factory interface {// 关键点2: 工厂方法返回Socket抽象接口 Create() network.Socket}// 关键点3: 按照需要实现具体的工厂// demo/sidecar/raw_socket_sidecar_factory.go// RawSocketFactory 只具备原生socket功能的sidecar,实现了Factory接口type RawSocketFactory struct {}func (r RawSocketFactory) Create() network.Socket { return network.DefaultSocket()}// demo/sidecar/all_in_one_sidecar_factory.go// AllInOneFactory 具备所有功能的sidecar工厂,实现了Factory接口type AllInOneFactory struct { procer mq.Procible}func (a AllInOneFactory) Create() network.Socket { return NewAccessLogSidecar(NewFlowCtrlSidecar(network.DefaultSocket()), a.procer)}

上述代码中,我们定义了一个工厂抽象接口 Factory ,并有了 2 个具体的实现 RawSocketFactory 和 AllInOneFactory。最后, ServiceMediator 依赖 Factory ,并在 Forward 方法中通过 Factory 来创建新的 Socket/Sidecar :

// demo/service/mediator/service_mediator.gotype ServiceMediator struct {... server *http.Server// 关键点4: 客户端依赖Factory抽象接口sidecarFactory sidecar.Factory}// Forward 转发请求,请求URL为 /{serviceType}+ServiceUri 的形式,如/serviceA/api/v1/taskfunc (s *ServiceMediator) Forward(req *http.Request) *http.Response {...// 发现下游服务的目的IP地址dest, err := s.discovery(svcType)// 创建HTTP客户端,调用sidecarFactory.Create()生成Socket作为入参client, err := http.NewClient(s.sidecarFactory.Create(), s.localIp)// 通过HTTP客户端转发请求resp, err := client.Send(dest, forwardReq)...}// 关键点5: 在ServiceMediator初始化时,将具体类型的sidecar.Factory注入到ServiceMediator中mediator := &ServiceMediator{sidecarFactory: &AllInOneFactory{}// sidecarFactory: &RawSocketFactory{}}

下面总结实现工厂方法模式的几个关键点:

定义一个工厂方法抽象接口,比如前文中的 sidecar.Factory。

工厂方法中,返回需要创建的对象/接口,比如 network.Socket。其中,工厂方法通常命名为 Create。

按照具体需要,定义工厂方法抽象接口的具体实现对象,比如 RawSocketFactory 和 AllInOneFactory。

客户端使用时,依赖工厂方法抽象接口。

在客户端初始化阶段,完成具体工厂对象的依赖注入。

扩展Go 风格的实现

前文的工厂方法模式实现,是非常典型的面向对象风格,下面我们给出一个更具 Go 风格的实现。

// demo/sidecar/sidecar_factory_func.go// 关键点1: 定义Sidecar工厂方法类型type FactoryFunc func() network.Socket// 关键点2: 按需定义具体的工厂方法实现,注意这里定义的是工厂方法的工厂方法,返回的是FactoryFunc工厂方法类型func RawSocketFactoryFunc() FactoryFunc { return func() network.Socket {return network.DefaultSocket() }}func AllInOneFactoryFunc(procer mq.Procible) FactoryFunc { return func() network.Socket {return NewAccessLogSidecar(NewFlowCtrlSidecar(network.DefaultSocket()), procer) }}type ServiceMediator struct {... server *http.Server// 关键点3: 客户端依赖FactoryFunc工厂方法类型sidecarFactoryFunc FactoryFunc}func (s *ServiceMediator) Forward(req *http.Request) *http.Response {...dest, err := s.discovery(svcType)// 关键点4: 创建HTTP客户端,调用sidecarFactoryFunc()生成Socket作为入参client, err := http.NewClient(s.sidecarFactoryFunc(), s.localIp)resp, err := client.Send(dest, forwardReq)...}// 关键点5: 在ServiceMediator初始化时,将具体类型的FactoryFunc注入到ServiceMediator中mediator := &ServiceMediator{sidecarFactoryFunc: RawSocketFactoryFunc()// sidecarFactory: AllInOneFactoryFunc(procer)}

上述的实现,利用了 Go 语言中函数作为一等公民的特点,少定义了几个 interface 和 struct,代码更加的简洁。

几个实现的关键点与面向对象风格的实现类似。值得注意的是 关键点2 ,我们相当于定义了一个工厂方法的工厂方法,这么做是为了利用函数闭包的特点来传递参数。如果直接定义工厂方法,那么 AllInOneFactoryFunc 的实现是下面这样的,无法实现多态:

// 并非FactoryFunc类型,无法实现多态func AllInOneFactoryFunc(procer mq.Procible) network.Socket {return NewAccessLogSidecar(NewFlowCtrlSidecar(network.DefaultSocket()), procer)}简单工厂

工厂方法模式的另一个变种是简单工厂,它并不通过多态,而是通过简单的 switch-case/if-else 条件判断来决定创建哪种产品:

// demo/sidecar/sidecar_simple_factory.go// 关键点1: 定义sidecar类型type Type uint8// 关键点2: 按照需要定义sidecar具体类型const ( Raw Type = iota AllInOne)// 关键点3: 定义简单工厂对象type SimpleFactory struct { procer mq.Procible}// 关键点4: 定义工厂方法,入参为sidecar类型,根据switch-case或者if-else来创建产品func (s SimpleFactory) Create(sidecarType Type) network.Socket { switch sidecarType { case Raw:return network.DefaultSocket() case AllInOne:return NewAccessLogSidecar(NewFlowCtrlSidecar(network.DefaultSocket()), s.procer) default:return nil }}// 关键点5: 创建产品时传入具体的sidecar类型,比如sidecar.AllInOnesimpleFactory := &sidecar.SimpleFactory{procer: procer}sidecar := simpleFactory.Create(sidecar.AllInOne)静态工厂方法

静态工厂方法是 Java/C++ 的说法,主要用于替代构造函数来完成对象的实例化,能够让代码的可读性更好,而且起到了与客户端解耦的作用。比如 Java 的静态工厂方法实现如下:

public class Packet {private final Endpoint src;private final Endpoint dest;private final Object payload;private Packet(Endpoint src, Endpoint dest, Object payload) {this.src = src;this.dest = dest;this.payload = payload;}// 静态工厂方法public static Packet of(Endpoint src, Endpoint dest, Object payload) {return new Packet(src, dest, payload);}...}// 用法packet = Packet.of(src, dest, payload)

Go 中并没有静态一说,直接通过普通函数来完成对象的构造即可,比如:

// demo/network/packet.gotype Packet struct { src Endpoint destEndpoint payload interface{}}// 工厂方法func NewPacket(src, dest Endpoint, payload interface{}) *Packet { return &Packet{src: src,dest:dest,payload: payload, }}// 用法packet := NewPacket(src, dest, payload)典型应用场景

对象实例化逻辑较为复杂时,可选择使用工厂方法模式/简单工厂/静态工厂方法来进行封装,为客户端提供一个易用的接口。

如果实例化的对象/接口涉及多种实现,可以使用工厂方法模式实现多态。

普通对象的创建,推荐使用静态工厂方法,比直接的实例化(比如 &Packet{src: src, dest: dest, payload: payload})具备更好的可读性和低耦合。

优缺点优点

代码的可读性更好。

与客户端程序解耦,当实例化逻辑变更时,只需改动工厂方法即可,避免了霰弹式修改。

缺点

引入工厂方法模式会新增一些对象/接口的定义,滥用会导致代码更加复杂。

与其他模式的关联

很多同学容易将工厂方法模式和抽象工厂模式混淆,抽象工厂模式主要运用在实例化“产品族”的场景,可以看成是工厂方法模式的一种演进。

参考

[1] 【Go实现】实践GoF的23种设计模式:SOLID原则, 元闰子

[2] Design Patterns, Chapter 3. Creational Patterns, GoF

[3] Factory patterns in Go (Golang), Soham Kamani

[4] 工厂方法, 维基百科

更多文章请关注微信公众号:元闰子的邀请

原文:https://juejin.cn/post/7100097763395764232
声明声明:本网页内容为用户发布,旨在传播知识,不代表本网认同其观点,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。E-MAIL:11247931@qq.com
如何查被录取到的专业 怎样查被录取的专业 录取后怎样查询录取的专业 录取专业怎么查 已被录取怎么查专业 ghs网络语什么意思_ghs网络语意思出处含义介绍 纸箱企业管理软件 ghs什么意思网络(ghs什么意思网络用语) 《喜羊羊与灰太狼》大结局 0与任何数相加都得原数吗? 23种经典设计模式学习笔记 海尔htr-688网络连接 ALCIALCI 的分类及技术参数 如何关闭微信游戏圈小程序的游戏圈展示 我已经出社会很多年了,我初中都没毕业,我想自学拿到高中毕业证然后再去... 怎样关掉微信游戏圈 iphone公交卡怎么刷 微信游戏动态删除不了吗? 我还没有初中毕业证,我能考大专学历吗?到时候能用吗?会不会被查到?_百... word的字符间距如何设定成固定值word的字符间距如何设定成固定值样式... 淘宝用什么付款方式比较安全?有哪些付款方式?该怎么开通支付宝?拜托各 ... Word中绝对有效的的设置字符间距技 ...提示支付宝正在检测您的安全设置...QQ294905152 远程协助~~搞好了... 关于国家助学的有关政策有哪些? 为了促进高校毕业生就业,国家出台了哪些真招实策 ...都在什么地方?我只能找到7个。拜托各位大神 DNF怎么选地图难度拜托各位大神 室内消火栓箱配置都包括什么 睡觉之前有看见一条小狗,晚上做梦梦见一条狗追着我跑,我怕的要命,但是... 梦见去了一个道教的庙里有条狗一直对我叫很凶,只是见到我怒叫!我后来... 3G手机无线网络怎么收费 爱意通 AY688 儿童手机 红色怎么样 ipad连无线网为什么出现无互联网连接? 一杯豆浆和一个水煮蛋的热量是多少 怎么查电脑硬盘分区情况? ...一个鸡蛋,中午一份炒饭,晚上一杯牛奶,热量是多少?能不能减肥?会... 猫耳螨能自愈吗 怎么治 拼多多可以挂抖音吗 抖音可以添加拼多多商品吗 商人的商最早的意思是什么 商人的商最早意思是指 抖音可以绑定拼多多吗 猫咪耳螨可以自愈吗 10朵玫瑰花代表什么意思 10朵玫瑰花代表的寓意 怎么才能知道qq好友所在的城市 ps怎么把一个图片中间的一个某个物体去掉ps怎么把一个图片中间的一个... 玫瑰 百合 等几种名贵花朵的含义谢谢了 白露节气收获哪些农作物不同地域在白露节气成熟的农作物不同 白露节气种植什么农作物,2023白露节气可以种什么菜 白露节气农作物特点 白露节气气象变化