摘要:工厂方法模式(Factory Method Pattern)将对象创建的逻辑封装起来,为使用者提供一个简单易用的对象创建接口,常用于不指定对象具体类型的情况下创建对象的场景。

本文分享自华为云社区《【Go实现】实践GoF的23种设计模式:工厂方法模式》,作者: 元闰子。

简述

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

UML 结构

代码实现

示例

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

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

// demo/network/http/http_client.go
package 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/task
func (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/task
func (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 {
producer mq.Producible
}
func (a AllInOneFactory) Create() network.Socket {
return NewAccessLogSidecar(NewFlowCtrlSidecar(network.DefaultSocket()), a.producer)
}

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

// demo/service/mediator/service_mediator.go

type ServiceMediator struct {
...
server *http.Server
// 关键点4: 客户端依赖Factory抽象接口
sidecarFactory sidecar.Factory
} // Forward 转发请求,请求URL为 /{serviceType}+ServiceUri 的形式,如/serviceA/api/v1/task
func (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{}
}

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

  1. 定义一个工厂方法抽象接口,比如前文中的 sidecar.Factory。
  2. 工厂方法中,返回需要创建的对象/接口,比如 network.Socket。其中,工厂方法通常命名为 Create。
  3. 按照具体需要,定义工厂方法抽象接口的具体实现对象,比如 RawSocketFactory 和 AllInOneFactory。
  4. 客户端使用时,依赖工厂方法抽象接口。
  5. 在客户端初始化阶段,完成具体工厂对象的依赖注入。

扩展

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(producer mq.Producible) FactoryFunc {
return func() network.Socket {
return NewAccessLogSidecar(NewFlowCtrlSidecar(network.DefaultSocket()), producer)
}
} 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(producer)
}

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

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

// 并非FactoryFunc类型,无法实现多态
func AllInOneFactoryFunc(producer mq.Producible) network.Socket {
return NewAccessLogSidecar(NewFlowCtrlSidecar(network.DefaultSocket()), producer)
}

简单工厂

工厂方法模式的另一个变种是简单工厂,它并不通过多态,而是通过简单的 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 {
producer mq.Producible
} // 关键点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.producer)
default:
return nil
}
} // 关键点5: 创建产品时传入具体的sidecar类型,比如sidecar.AllInOne
simpleFactory := &sidecar.SimpleFactory{producer: producer}
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.go
type Packet struct {
src Endpoint
dest Endpoint
payload interface{}
} // 工厂方法
func NewPacket(src, dest Endpoint, payload interface{}) *Packet {
return &Packet{
src: src,
dest: dest,
payload: payload,
}
} // 用法
packet := NewPacket(src, dest, payload)

典型应用场景

  1. 对象实例化逻辑较为复杂时,可选择使用工厂方法模式/简单工厂/静态工厂方法来进行封装,为客户端提供一个易用的接口。
  2. 如果实例化的对象/接口涉及多种实现,可以使用工厂方法模式实现多态。
  3. 普通对象的创建,推荐使用静态工厂方法,比直接的实例化(比如 &Packet{src: src, dest: dest, payload: payload})具备更好的可读性和低耦合。

优缺点

优点

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

缺点

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

与其他模式的关联

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

参考

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

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

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

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

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

点击关注,第一时间了解华为云新鲜技术~

实践GoF的设计模式:工厂方法模式的更多相关文章

  1. C++设计模式——工厂方法模式

    本文版权归果冻说所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接,否则保留追究法律责任的权利.» 本文链接:http://www.jellythink.com/arch ...

  2. 4. 星际争霸之php设计模式--工厂方法模式

    题记==============================================================================本php设计模式专辑来源于博客(jymo ...

  3. 实践GoF的设计模式:迭代器模式

    摘要:迭代器模式主要用在访问对象集合的场景,能够向客户端隐藏集合的实现细节. 本文分享自华为云社区<[Go实现]实践GoF的23种设计模式:迭代器模式>,作者:元闰子. 简介 有时会遇到这 ...

  4. JAVA设计模式--工厂方法模式

    工厂方法设计模式 抽象工厂角色: 这是工厂方法模式的核心,它与应用程序无关.是具体工厂角色必须实现的接口或者必须继承的父类.在java中它由抽象类或者接口来实现.具体工厂角色:它含有和具体业务逻辑有关 ...

  5. 深入浅出设计模式——工厂方法模式(Factory Method)

    介绍在简单工厂模式中,我们提到,工厂方法模式是简单工厂模式的一个延伸,它属于Gof23中设计模式的创建型设计模式.它解决的仍然是软件设计中与创建对象有关的问题.它可以更好的处理客户的需求变化. 引入我 ...

  6. 设计模式--工厂方法模式(Factory method pattern)及应用

    面向对象的好处: 通过封装,继承,多态把程序的耦合度降低. 用设计模式可以使程序更加灵活,容易修改,且易于复用. 1. 工厂方法模式 Define an interface for creating ...

  7. 我的Java设计模式-工厂方法模式

    女朋友dodo闹脾气,气势汹汹的说"我要吃雪糕".笔者心里啊乐滋滋的,一支雪糕就能哄回来,不亦乐乎?! 但是,雪糕买回来了,她竟然说"不想吃雪糕了,突然想吃披萨" ...

  8. 设计模式 — 工厂方法模式(Factory Method)

    在开发系统中,经常会碰到一个问题.现在需要实现的一些功能,但是这个功能模块以后一定是需要扩展的,那么现在开发中就不仅要实现现在的功能,还要考虑以后的扩展.那么为了系统的健壮,扩展就要遵循开闭原则(简单 ...

  9. C#设计模式--工厂方法模式

    0.C#设计模式-简单工厂模式 设计模式: 工厂方法模式(Factory Method Pattern) 介绍:简单工厂模式是要在工厂类中通过数据来做个决策,在工厂类中的多个类中实例化出来其中一个要用 ...

  10. Java设计模式—工厂方法模式&抽象工厂模式

    工厂方法模式与抽象工厂模式都是设计模式中重要而且常见的模式.       工厂方法模式:定义一个用于创建对象的接口,让子类决定实例化哪一个类.工厂方法使一个类的实例化延迟到其子类. 通用类图如下: 在 ...

随机推荐

  1. 入门篇-其之六-Java运算符(中)

    祝所有程序员,1024节日快乐!!! 一.自增/自减运算符 假设有一个变量intValue的值为10,如果想让这个值加1,有哪些方式? 首先,我们可以使用最原始的方式: int intValue = ...

  2. YbtOJ 质数与约数 4.统计元素

    搜不到题解,乐. 由题意,\(a_i\) 对答案有贡献当且仅当 \(i\in{[l,r]}\) 且对于 \(\forall a_j| {a_i},j\notin[l,r]\). 则对于每个 \(a_i ...

  3. 装箱问题(lgP1049)

    01背包问题. 与模板不同,这道题要求的是最小剩余空间,也就是求背包里最多能放多少东西. 所以状态转移方程变为 fi= fi - w[i] + wi . 其中 fi 表示当背包容量为 i 时可放的最大 ...

  4. P3870 [TJOI2009] 开关(线段树)

    P3870 [TJOI2009] 开关 思路:可以用线段树来维护区间中亮灯的个数,区间修改用加上懒标记就好 #include <bits/stdc++.h> #define LL long ...

  5. nginx 配置 proxy_next_upstream 会出现未预期 502 错误问题排查

    当使用nginx代理多个网关实例时, 当被请求服务的get 接口异常时,如 error timeout invalid_header http_500 http_502 http_503 http_5 ...

  6. HarmonyOS 高级特性

    引言 本章将探讨 HarmonyOS 的高级特性,包括分布式能力.安全机制和性能优化.这些特性可以帮助你构建更强大.更安全.更高效的应用. 目录 HarmonyOS 的分布式能力 HarmonyOS ...

  7. c#中命令模式详解

    基本介绍:   命令模式,顾名思义就是将命令抽象化,然后将请求者和接收者通过命令进行绑定.   而命令的请求者只管下达命令,命令的接收者只管执行命令.   从而实现了解耦,请求者和接受者二者相对独立. ...

  8. 从源码分析 Redis 异步删除各个参数的具体作用

    以前对异步删除几个参数的作用比较模糊,包括网上的很多资料都是一笔带过,语焉不详. 所以这次从源码(基于 Redis 7.0.5)的角度来深入分析下这几个参数的具体作用: lazyfree-lazy-u ...

  9. Vue源码学习(十八):实现组件注册(一)Vue.component()和Vue.extend()

    好家伙, 0.完整代码已开源 https://github.com/Fattiger4399/analytic-vue.git 1.思路 1.1.什么是组件化? Vue 组件化是指将复杂的应用程序拆分 ...

  10. ij社区版如何创建spring项目

    他们说是使用spring  init什么什么的,那个都是老版的名称了,你去插件里面搜找是肯定搜不到的,现在叫spring boot helper,用这个,安装一下就好了(注意本次是在2022/11/1 ...