有关创建型的几个设计模式总结
前言
最近看博客遇到了几个关于go中设计模式的问题, 想起了之前还有遗留的一篇关于设计模式的总结还没完成。这里正好总结一下,填一下坑。
关于工厂模式
创建对象在编程中是用的最多的一个操作来。有的时候对象的创建逻辑比较复杂,可能包括很多步骤,也可能依赖于其他的对象逻辑,这个时候使用工厂模式的作用就出来了。工厂模式的核心是封装对象创建的细节,让调用者只关心接口,不关心具体实现。 使用工厂模式是希望能够创建一个对象,但创建过程比较复杂,希望对外隐藏这些细节。
关于工厂模式,细分下来有简单工厂、工厂方法和抽象工厂。
简单工厂模式
简单工厂就是很简单,它是根据传入的参数,生产所有类型的产品。用 swich...case 的方式来决定生产的产品类型。
go
// 产品接口:所有奶茶都实现这个接口
type MilkTea interface {
GetName() string
GetPrice() int
}
// 具体产品:珍珠奶茶
type PearlMilkTea struct{}
func (p *PearlMilkTea) GetName() string { return "珍珠奶茶" }
func (p *PearlMilkTea) GetPrice() int { return 12 }
// 具体产品:芋泥奶茶
type TaroMilkTea struct{}
func (t *TaroMilkTea) GetName() string { return "芋泥奶茶" }
func (t *TaroMilkTea) GetPrice() int { return 15 }
// 简单工厂:一个函数生产所有产品
func NewMilkTea(flavor string) MilkTea {
switch flavor {
case "pearl":
return &PearlMilkTea{}
case "taro":
return &TaroMilkTea{}
default:
return nil
}
}
func main() {
tea := NewMilkTea("pearl")
fmt.Println(tea.GetName(), tea.GetPrice()) // 输出:珍珠奶茶 12
}
简单工厂模式比较简单,适用于产品种类比较少(小于5个),不怎么频繁增加的场景。 虽然比较简单,但大部分场景用的都是它。
工厂方法模式
上面的简单工厂,每次新增加产品都得增加 switch...case...的分支,不够优雅。 可以进行拆分,把原来的大工厂拆分成多个小工厂,每个工厂只负责生产一种产品。
go
// 产品接口(和简单工厂一样)
type MilkTea interface {
GetName() string
GetPrice() int
}
// 具体产品(和简单工厂一样)
type PearlMilkTea struct{}
func (p *PearlMilkTea) GetName() string { return "珍珠奶茶" }
func (p *PearlMilkTea) GetPrice() int { return 12 }
type TaroMilkTea struct{}
func (t *TaroMilkTea) GetName() string { return "芋泥奶茶" }
func (t *TaroMilkTea) GetPrice() int { return 15 }
// 工厂接口:定义所有工厂必须实现的方法
type MilkTeaFactory interface {
Create() MilkTea
}
// 具体工厂:每个产品对应一个工厂
type PearlMilkTeaFactory struct{}
func (p *PearlMilkTeaFactory) Create() MilkTea {
return &PearlMilkTea{}
}
type TaroMilkTeaFactory struct{}
func (t *TaroMilkTeaFactory) Create() MilkTea {
return &TaroMilkTea{}
}
// 统一的生产过程
func MakeTea(factory MilkTeaFactory) MilkTea {
fmt.Println("准备制作奶茶...")
tea := factory.Create()
fmt.Println("奶茶制作完成:", tea.GetName())
return tea
}
func main() {
// 可以动态切换工厂
MakeTea(&PearlMilkTeaFactory{})
MakeTea(&TaroMilkTeaFactory{})
}
工厂方法又做了一层抽象,通过抽象出一个带有 Create 的工厂来创建具体的产品。 老实说,上面的示例实际中用到的不多。即使用到的话,在 go 语言中可以使用一个更简洁的方式来实现。
go
// 定义一个函数类型作为工厂接口
type MilkTeaFactory func() MilkTea
func NewPearlMilkTea() MilkTea {
return &PearlMilkTea{}
}
func NewTaroMilkTea() MilkTea {
return &TaroMilkTea{}
}
func MakeTea(factory MilkTeaFactory) MilkTea {
fmt.Println("准备制作奶茶...")
tea := factory()
fmt.Println("奶茶制作完成:", tea.GetName())
return tea
}
func main() {
MakeTea(NewPearlMilkTea) // 直接把函数作为参数传递
MakeTea(NewTaroMilkTea)
}
实际中比较常用的是下面的这种直接生成对应的产品方式:
go
type MilkTea interface {
GetName() string
GetPrice() int
}
type PearlMilkTea struct{}
func (p *PearlMilkTea) GetName() string { return "珍珠奶茶" }
func (p *PearlMilkTea) GetPrice() int { return 12 }
type TaroMilkTea struct{}
func (t *TaroMilkTea) GetName() string { return "芋泥奶茶" }
func (t *TaroMilkTea) GetPrice() int { return 15 }
// 具体工厂1
func NewPearlMilkTea() MilkTea {
return &PearlMilkTea{}
}
// 具体工厂2
func NewTaroMilkTea() MilkTea {
return &TaroMilkTea{}
}
func main() {
// 想要什么直接生产
tea := NewPearlMilkTea()
// tea:=NewTaroMilkTea()
fmt.Println(tea.GetName(), tea.GetPrice()) // 输出:珍珠奶茶 12
}
抽象工厂模式
前面的工厂模式生产的是一种类型的产品。抽象工厂的是一组有关联的产品,是一个"套餐包"。
比如说奶茶店里的两种套餐:
- 经典套餐:珍珠奶茶 + 薯条
- 豪华套餐:芋泥奶茶 + 炸鸡
go
// 产品族接口1:奶茶
type MilkTea interface {
GetName() string
}
// 产品族接口2:小吃
type Snack interface {
GetName() string
}
// 具体产品:珍珠奶茶、芋泥奶茶
type PearlMilkTea struct{}
func (p *PearlMilkTea) GetName() string { return "珍珠奶茶" }
type TaroMilkTea struct{}
func (t *TaroMilkTea) GetName() string { return "芋泥奶茶" }
// 具体产品:薯条、炸鸡
type FrenchFries struct{}
func (f *FrenchFries) GetName() string { return "薯条" }
type FriedChicken struct{}
func (f *FriedChicken) GetName() string { return "炸鸡" }
// 抽象工厂接口:定义生产一族产品的方法
type ComboFactory interface {
CreateMilkTea() MilkTea
CreateSnack() Snack
}
// 具体工厂:每个工厂生产一族产品
type ClassicComboFactory struct{}
func (c *ClassicComboFactory) CreateMilkTea() MilkTea {
return &PearlMilkTea{}
}
func (c *ClassicComboFactory) CreateSnack() Snack {
return &FrenchFries{}
}
type LuxuryComboFactory struct{}
func (l *LuxuryComboFactory) CreateMilkTea() MilkTea {
return &TaroMilkTea{}
}
func (l *LuxuryComboFactory) CreateSnack() Snack {
return &FriedChicken{}
}
func main() {
var factory ComboFactory
factory = &ClassicComboFactory()
tea := factory.CreateMilkTea()
snack := factory.CreateSnack()
fmt.Println("经典套餐:", tea.GetName(), "+", snack.GetName())
// 输出:经典套餐:珍珠奶茶 + 薯条
}
抽象工厂模式在实际中中几乎很少用到。绝大多数情况下,都不需要创建一族相关的产品。如果真的需要,用几个独立的工厂函数组合就够了。
关于单例模式
单例模式很简单,就是整个类只需要一个实例即可。一般用于全局缓存、对象池、线程池这种场景。
在实际中,最简单、最常见(80%)的场景是使用饿汉式的生成方式,定义一个全局的(确切的说是包级别)的类变量。如下所示:
go
package singleton
// 私有化结构体,防止外部直接实例化
type singleton struct {
// 可以添加需要的字段
Name string
}
// 包级私有变量,程序启动时自动初始化
var instance = &singleton{
Name: "饿汉单例",
}
// 公有方法,提供全局访问点
func GetInstance() *singleton {
return instance
}
这种方式实现最简单、代码量最少,也没有并发开销。只不过要事先生成,可能会浪费内存空间。
还有一种懒汉模式,在第一次使用的时候动态生成。
如下所示:
go
package singleton
import "sync"
type singleton struct {
Name string
}
var (
instance *singleton
mutex sync.Mutex
)
func GetInstance() *singleton {
// 第一次检查:如果实例已经存在,直接返回,不需要加锁
if instance == nil {
mutex.Lock()
defer mutex.Unlock()
// 第二次检查:防止多个goroutine都通过了第一次检查
if instance == nil {
instance = &singleton{Name: "双重检查锁定懒汉单例"}
}
}
return instance
}
本质上就是用一把锁来防止并发访问。对于防止并发访问,可以直接使用 go 提供的一个并发原语 sync.Once。
go
package singleton
import "sync"
type singleton struct {
Name string
}
var (
instance *singleton
once sync.Once // 保证只执行一次
)
func GetInstance() *singleton {
once.Do(func() {
// 这里的代码只会执行一次,无论有多少个goroutine同时调用
instance = &singleton{Name: "sync.Once版懒汉单例"}
})
return instance
}
关于建造者模式(builder模式)
有的时候我们想配置new一个实体struct,但是实体在正式工作之前需要配置一系列的参数(有些是可选的,有些是必须传递的),如果每种参数都生成一个特殊的new签名处理起来是相当麻烦的。比如说:
go
type Server struct {
Protocal string
Timeout int
Addr string
Port int
//...
}
func NewDefaultServer(addr string, port int) (*Server, error) {
return &Server{addr, port, "tcp", 30 * time.Second, 100, nil}, nil
}
func NewTLSServer(addr string, port int, tls *tls.Config) (*Server, error) {
return &Server{addr, port, "tcp", 30 * time.Second, 100, tls}, nil
}
func NewServerWithTimeout(addr string, port int, timeout time.Duration) (*Server, error) {
return &Server{addr, port, "tcp", timeout, 100, nil}, nil
}
func NewTLSServerWithMaxConnAndTimeout(addr string, port int, maxconns int, timeout time.Duration, tls *tls.Config) (*Server, error) {
return &Server{addr, port, "tcp", 30 * time.Second, maxconns, tls}, nil
}
比较好的一种方式是使用一个Builder模式或者Functional Options模式。 前者另使用一个builder结构来完成参数的配置工作。如下所示。
go
//使用一个builder类来做包装
type ServerBuilder struct {
Server
}
func (sb *ServerBuilder) Create(addr string, port int) *ServerBuilder {
sb.Server.Addr = addr
sb.Server.Port = port
//其它代码设置其它成员的默认值
return sb
}
func (sb *ServerBuilder) WithProtocol(protocol string) *ServerBuilder {
sb.Server.Protocol = protocol
return sb
}
func (sb *ServerBuilder) WithMaxConn( maxconn int) *ServerBuilder {
sb.Server.MaxConns = maxconn
return sb
}
func (sb *ServerBuilder) WithTimeOut( timeout time.Duration) *ServerBuilder {
sb.Server.Timeout = timeout
return sb
}
func (sb *ServerBuilder) WithTLS( tls *tls.Config) *ServerBuilder {
sb.Server.TLS = tls
return sb
}
func (sb *ServerBuilder) Build() (Server) {
return sb.Server
}
xxxBuiler算是一个生成(或者初始化)的类,它的功能就是配置生成一个xxx实体。通过暴露不同的单个Withxxx函数,用来初始化各个参数(根据用户的选择)。通过使用 builder 模式可以分步、灵活地创建复杂对象,解决构造函数参数爆炸和调用时参数顺序混乱问题。 在go中很多库都是使用这种方式,如stringBuiler, sqlBuilder等。
除了使用Builder结构来说,还可以使用function来完成这个功能。如下所示。
go
type Option func(*Server)
func Protocol(p string) Option {
return func(s *Server) {
s.Protocol = p
}
}
func Timeout(timeout time.Duration) Option {
return func(s *Server) {
s.Timeout = timeout
}
}
func MaxConns(maxconns int) Option {
return func(s *Server) {
s.MaxConns = maxconns
}
}
func TLS(tls *tls.Config) Option {
return func(s *Server) {
s.TLS = tls
}
}
这样我们在生成默认的实体中,传入多个具体的配置option func , 在构造函数中通过opt(&xxx) 循环配置。最终也可以实现配置实体的功能。
go
func NewServer(addr string, port int, options ...func(*Server)) (*Server, error) {
srv := Server{
Addr: addr,
Port: port,
Protocol: "tcp",
Timeout: 30 * time.Second,
MaxConns: 1000,
TLS: nil,
}
for _, option := range options {
option(&srv)
}
//...
return &srv, nil
}
使用如下方式进行配置
go
s1, _ := NewServer("localhost", 1024)
s2, _ := NewServer("localhost", 2048, Protocol("udp"))
s3, _ := NewServer("0.0.0.0", 8080, Timeout(300*time.Second), MaxConns(1000))
上述两种方式都蛮好,但是各有缺点;前者需要另外搞一个xxxBuilder实体出来,然后告诉这个实体该怎么初始化xxx; 但是这种方式灵活些;后者直接告诉xxx,如何配置,将配置当成函数参数传入构造函数中,但是这样灵活些稍微低一些,如果我想要一个先构造一个xxx,然后在根据条件来进行配置,就不太好搞 了。
关于原型模式
原型模式解决的是另一种对象创建方式, 在某系场景下,new 一个对象的逻辑很复杂,可能涉及到大量的网络查询、复杂的计算等,创建起来很耗时。 这个时候就可以使用 原型模式, 用 "复制" 代替 "new",用内存拷贝的成本代替初始化的成本。
go 中 http 的 request.clone 就是一个例子。
go
// net/http/request.go
func (r *Request) Clone(ctx context.Context) *Request {
// 浅拷贝基本字段
r2 := new(Request)
*r2 = *r
// 深拷贝引用类型字段
r2.ctx = ctx
r2.Header = r.Header.Clone() // Header是map类型,需要深拷贝
r2.Trailer = r.Trailer.Clone()
if r.URL != nil {
r2.URL = new(url.URL)
*r2.URL = *r.URL
}
// ... 其他引用类型字段的深拷贝
return r2
}
原型模式要注意的一个点就是深拷贝和浅拷贝的问题,这个概念很常见,这里就不多赘述了。
后记
工作了几年之后发现设计模式更多的是一种思想,写代码时不能教科书式的生搬硬套,需要需要结合场景、结合语言来实现。如果实在拿不准的,就选用最简单的那种方式。最简单的,往往易理解、不易出错,易扩展。
参考
【1】干掉if else!试试这三种设计模式,优化代码贼顺手!
【3】设计模式二三事