有关创建型的几个设计模式总结

有关创建型的几个设计模式总结

前言

最近看博客遇到了几个关于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!试试这三种设计模式,优化代码贼顺手!

【2】设计模式------设计模式三大分类以及六大原则

【3】设计模式二三事

【4】工厂模式(factory Method)的本质是什么?为什么引入工厂模式?

【5】快来,这里有23种设计模式的Go语言实现

相关推荐
蜡笔小马12 小时前
14.C++设计模式-状态模式
c++·设计模式·状态模式
加油201913 小时前
嵌入式软件技术栈和学习路线详解
linux·arm开发·数据结构·mqtt·设计模式·嵌入式
likerhood13 小时前
设计模式 · 代理模式(Proxy Pattern)java
java·设计模式·代理模式
刀法如飞1 天前
Palantir Ontology 存储结构与读写机制原理深入剖析
大数据·设计模式·系统架构
KobeSacre1 天前
设计模式——七大设计原则
设计模式
倒流时光三十年1 天前
设计模式 之 责任链模式
设计模式·责任链模式
阿文的代码库1 天前
桥接设计模式的案例实现
设计模式
乐观的山里娃1 天前
【设计模式 14】责任链:谁来拍板
设计模式
乐观的山里娃2 天前
【设计模式 08】装饰器:加钱加服务
设计模式