设计模式是软件工程中各种常见问题的经典解决方案,设计模式不只是代码,而是组织代码的方式。假设一行行的代码是砖,设计模式就是蓝图。


创建型模式

创建型模式是处理对象创建的设计模式,试图根据实际情况使用合适的方式创建对象,增加已有代码的灵活性和可复用性。

工厂方法模式 Factory Method


问题

假设我们的业务需要一个支付渠道,我们开发了一个Pay方法,其可以用于支付。请看以下示例:

type Pay interface {
    Pay() string
}

type PayReq struct {
    OrderId string // 订单号
}

func (p *PayReq) Pay() string {
    // todo
    fmt.Println(p.OrderId)
    return "支付成功"
}

如上,我们定义了接口Pay,并实现了其方法Pay()。

如果业务需求变更,需要我们提供多种支付方式,一种叫APay,一种叫BPay,这二种支付方式所需的参数不同,APay只需要订单号OrderId,BPay则需要订单号OrderId和Uid。此时如何修改?

很容易想到的是在原有的代码基础上修改,比如:

type Pay interface {
  APay() string
  BPay() string
}

type PayReq struct {
  OrderId string // 订单号
  Uid int64
}

func (p *PayReq) APay() string {
  // todo
  fmt.Println(p.OrderId)
  return "APay支付成功"
}

func (p *PayReq) BPay() string {
  // todo
  fmt.Println(p.OrderId)
  fmt.Println(p.Uid)
  return "BPay支付成功"
}

我们为Pay接口实现了APay() 和BPay() 方法。虽然暂时实现了业务需求,但却使得结构体PayReq变得冗余了,APay() 并不需要Uid参数。如果之后再增加CPay、DPay、EPay,可想而知,代码会变得越来越难以维护

随着后续业务迭代,将不得不编写出复杂的代码。

解决

让我们想象一个工厂类,这个工厂类需要生产电线和开关等器具,我们可以为工厂类提供一个生产方法,当电线机器调用生产方法时,就产出电线,当开关机器调用生产方法时,就产出开关。

套用到我们的支付业务来,就是我们不再为接口提供APay方法、BPay方法,而只提供一个Pay方法,并将A支付方式和B支付方式的区别下放到子类。

请看示例:

package factorymethod

import "fmt"

type Pay interface {
  Pay(string) int
}

type PayReq struct {
  OrderId string
}

type APayReq struct {
  PayReq
}

func (p *APayReq) Pay() string {
  // todo
  fmt.Println(p.OrderId)
  return "APay支付成功"
}

type BPayReq struct {
  PayReq
  Uid int64
}

func (p *BPayReq) Pay() string {
  // todo
  fmt.Println(p.OrderId)
  fmt.Println(p.Uid)
  return "BPay支付成功"
}

我们用APay和BPay两个结构体重写了Pay() 方法,如果需要添加一种新的支付方式, 只需要重写新的Pay() 方法即可。

工厂方法的优点就在于避免了创建者和具体产品之间的紧密耦合,从而使得代码更容易维护。

测试代码:

package factorymethod

import (
  "testing"
)

func TestPay(t *testing.T) {
  aPay := APayReq{}
  if aPay.Pay() != "APay支付成功" {
    t.Fatal("aPay error")
  }

  bPay := BPayReq{}
  if bPay.Pay() != "BPay支付成功" {
    t.Fatal("bPay error")
  }
}

抽象工厂模式 Abstract Factory


问题

抽象工厂模式基于工厂方法模式。两者的区别在于:工厂方法模式是创建出一种产品,而抽象工厂模式是创建出一类产品。这二种都属于工厂模式,在设计上是相似的。

假设,有一个存储工厂,提供redis和mysql两种存储数据的方式。如果使用工厂方法模式,我们就需要一个存储工厂,并提供SaveRedis方法和SaveMysql方法。

如果此时业务还需要分成存储散文和古诗两种载体,这两种载体都可以进行redis和mysql存储。就可以使用抽象工厂模式,我们需要一个存储工厂作为父工厂,散文工厂和古诗工厂作为子工厂,并提供SaveRedis方法和SaveMysql方法。

解决

以上文的存储工厂业务为例,用抽象工厂模式的思路来设计代码,就像下面这样:

package abstractfactory

import "fmt"

// SaveArticle 抽象模式工厂接口
type SaveArticle interface {
	CreateProse() Prose
	CreateAncientPoetry() AncientPoetry
}

type SaveRedis struct{}

func (*SaveRedis) CreateProse() Prose {
	return &RedisProse{}
}

func (*SaveRedis) CreateAncientPoetry() AncientPoetry {
	return &RedisProse{}
}

type SaveMysql struct{}

func (*SaveMysql) CreateProse() Prose {
	return &MysqlProse{}
}

func (*SaveMysql) CreateAncientPoetry() AncientPoetry {
	return &MysqlProse{}
}

// Prose 散文
type Prose interface {
	SaveProse()
}

// AncientPoetry 古诗
type AncientPoetry interface {
	SaveAncientPoetry()
}

type RedisProse struct{}

func (*RedisProse) SaveProse() {
	fmt.Println("Redis Save Prose")
}

func (*RedisProse) SaveAncientPoetry() {
	fmt.Println("Redis Save Ancient Poetry")
}

type MysqlProse struct{}

func (*MysqlProse) SaveProse() {
	fmt.Println("Mysql Save Prose")
}

func (*MysqlProse) SaveAncientPoetry() {
	fmt.Println("Mysql Save Ancient Poetry")
}

我们定义了存储工厂,也就是SaveArticle接口,并实现了CreateProse方法和CreateAncientPoetry方法,这2个方法分别用于创建散文工厂和古诗工厂。

然后我们又分别为散文工厂和古诗工厂实现了SaveProse方法和SaveAncientPoetry方法,并用Redis结构体和Mysql结构体分别重写了2种存储方法。

测试代码:

package abstractfactory

func Save(saveArticle SaveArticle) {
  saveArticle.CreateProse().SaveProse()
  saveArticle.CreateAncientPoetry().SaveAncientPoetry()
}

func ExampleSaveRedis() {
  var factory SaveArticle
  factory = &SaveRedis{}
  Save(factory)
  // Output:
  // Redis Save Prose
  // Redis Save Ancient Poetry
}

func ExampleSaveMysql() {
  var factory SaveArticle
  factory = &SaveMysql{}
  Save(factory)
  // Output:
  // Mysql Save Prose
  // Mysql Save Ancient Poetry
}


 

建造者模式 Builder

问题

假设业务需要按步骤创建一系列复杂的对象,实现这些步骤的代码加在一起非常繁复,我们可以将这些代码放进一个包含了众多参数的构造函数中,但这个构造函数看起来将会非常杂乱无章,且难以维护。

假设业务需要建造一个房子对象,需要先打地基、建墙、建屋顶、建花园、放置家具……。我们需要非常多的步骤,并且这些步骤之间是有联系的,即使将各个步骤从一个大的构造函数抽出到其他小函数中,整个程序的层次结构看起来依然很复杂。

如何解决呢?像这种复杂的有许多步骤的构造函数,就可以用建造者模式来设计。

建造者模式的用处就在于能够分步骤创建复杂对象。

解决

在建造者模式中,我们需要清晰的定义每个步骤的代码,然后在一个构造函数中操作这些步骤,我们需要一个主管类,用这个主管类来管理各步骤。这样我们就只需要将所需参数传给一个构造函数,构造函数再将参数传递给对应的主管类,最后由主管类完成后续所有建造任务。

请看以下代码:

package builder

import "fmt"

// 建造者接口
type Builder interface {
  Part1()
  Part2()
  Part3()
}

// 管理类
type Director struct {
  builder Builder
}

// 构造函数
func NewDirector(builder Builder) *Director {
  return &Director{
    builder: builder,
  }
}

// 建造
func (d *Director) Construct() {
  d.builder.Part1()
  d.builder.Part2()
  d.builder.Part3()
}

type Builder struct {}

func (b *Builder) Part1() {
  fmt.Println("part1")
}

func (b *Builder) Part2() {
  fmt.Println("part2")
}

func (b *Builder) Part3() {
  fmt.Println("part3")
}

如上,我们实现part1、part2、part3这3个步骤,只需要执行构造函数,对应的管理类就可以运行建造方法Construct,完成3个步骤的执行。


测试代码:

package builder

func ExampleBuilder() {
  builder := &Builder{}
  director := NewDirector(builder)
  director.Construct()
  // Output:
  // part1
  // part2
  // part3
}

原型模式 Prototype

问题

如果你希望生成一个对象,其与另一个对象完全相同,该如何实现呢?

如果遍历对象的所有成员,将其依次复制到新对象中,会稍显麻烦,而且有些对象可能会有私有成员变量遗漏。

原型模式将这个克隆的过程委派给了被克隆的实际对象,被克隆的对象就叫做“原型”。

解决

如果需要克隆一个新的对象,这个对象完全独立于它的原型,那么就可以使用原型模式。

原型模式的实现非常简单,请看以下代码:

package prototype

import "testing"

var manager *PrototypeManager

type Type1 struct {
  name string
}

func (t *Type1) Clone() *Type1 {
  tc := *t
  return &tc
}

func TestClone(t *testing.T) {
  t1 := &Type1{
    name: "type1",
  }

  t2 := t1.Clone()

  if t1 == t2 {
    t.Fatal("error! get clone not working")
  }
}

我们依靠一个Clone方法实现了原型Type1的克隆。

原型模式的用处就在于我们可以克隆对象,而无需与原型对象的依赖相耦合。

单例模式 Singleton

问题

存储着重要对象的全局变量,往往意味着“不安全”,因为你无法保证这个全局变量的值不会在项目的某个引用处被覆盖掉。

对数据的修改经常导致出乎意料的的结果和难以发现的bug。我在一处更新数据,却没有意识到软件中的另一处期望着完全不同的数据,于是一个功能就失效了,而且找出故障的原因也会非常困难。

一个较好的解决方案是:将这样的“可变数据”封装起来,写一个查询方法专门用来获取这些值。

单例模式则更进一步:除了要为“可变数据”提供一个全局访问方法,它还要保证获取到的只有同一个实例。也就是说,如果你打算用一个构造函数创建一个对象,单例模式将保证你得到的不是一个新的对象,而是之前创建过的对象,并且每次它所返回的都只有这同一个对象,也就是单例。这可以保护该对象实例不被篡改。

解决

单例模式需要一个全局构造函数,这个构造函数会返回一个私有的对象,无论何时调用,它总是返回相同的对象。

请看以下代码:

package singleton

import (
  "sync"
)

// 单例实例
type singleton struct {
  Value int
}

type Singleton interface {
  getValue() int
}

func (s singleton) getValue() int {
  return s.Value
}

var (
  instance *singleton
  once     sync.Once
)

// 构造方法,用于获取单例模式对象
func GetInstance(v int) Singleton {
  once.Do(func() {
    instance = &singleton{Value: v}
  })

  return instance
}

单例实例singleton被保存为一个私有的变量,以保证不被其他包的函数引用。

用构造方法GetInstance可以获得单例实例,函数中使用了sync包的once方法,以保证实例只会在首次调用时被初始化一次,之后再调用构造方法都只会返回同一个实例。

测试代码:

func TestSingleton(t *testing.T) {
  ins1 := GetInstance2(1)
  ins2 := GetInstance2(2)
  if ins1 != ins2 {
    t.Fatal("instance is not equal")
  }
}

如果你需要更加严格地控制全局变量,这确实很有必要,那么就使用单例模式吧。

结构型模式

结构型模式将一些对象和类组装成更大的结构体,并同时保持结构的灵活和高效。

适配器模式 Adapter

问题

适配器模式说白了就是兼容。

假设一开始我们提供了A对象,后期随着业务迭代,又需要从A对象的基础之上衍生出不同的需求。如果有很多函数已经在线上调用了A对象,此时再对A对象进行修改就比较麻烦,因为需要考虑兼容问题。还有更糟糕的情况, 你可能没有程序库的源代码, 从而无法对其进行修改。

此时就可以用一个适配器,它就像一个接口转换器,调用方只需要调用这个适配器接口,而不需要关注其背后的实现,由适配器接口封装复杂的过程。

解决

假设有2个接口,一个将厘米转为米,一个将米转为厘米。我们提供一个适配器接口,使调用方不需要再操心调用哪个接口,直接由适配器做好兼容。

请看以下代码:

package adapter

// 提供一个获取米的接口和一个获取厘米的接口

type Cm interface {
  getLength(float64) float64
}

type M interface {
  getLength(float64) float64
}

func NewM() M {
  return &getLengthM{}
}

type getLengthM struct{}

func (*getLengthM) getLength(cm float64) float64 {
  return cm / 10
}

func NewCm() Cm {
  return &getLengthCm{}
}

type getLengthCm struct{}

func (a *getLengthCm) getLength(m float64) float64 {
  return m * 10
}

// 适配器

type LengthAdapter interface {
  getLength(string, float64) float64
}

func NewLengthAdapter() LengthAdapter {
  return &getLengthAdapter{}
}

type getLengthAdapter struct{}

func (*getLengthAdapter) getLength(isType string, into float64) float64 {
  if isType == "m" {
    return NewM().getLength(into)
  }
  return NewCm().getLength(into)
}

上面实现了Cm和M两个接口,并由适配器LengthAdapter做兼容。

测试代码:

package adapter

import "testing"

func TestAdapter(t *testing.T) {
  into := 10.5
  getLengthAdapter := NewLengthAdapter().getLength("m", into)
  getLengthM := NewM().getLength(into)
  if getLengthAdapter != getLengthM {
    t.Fatalf("getLengthAdapter: %f, getLengthM: %f", getLengthAdapter, getLengthM)
  }
}

桥接模式Bridge

问题

假设一开始业务需要两种发送信息的渠道,sms和email,我们可以分别实现sms和email两个接口。

之后随着业务迭代,又产生了新的需求,需要提供两种系统发送方式,systemA和systemB,并且这两种系统发送方式都应该支持sms和email渠道。

此时至少需要提供4种方法:systemA to sms,systemA to email,systemB to sms,systemB to email。

如果再分别增加一种渠道和一种系统发送方式,就需要提供9种方法。这将导致代码的复杂程度指数增长。

解决

其实之前我们是在用继承的想法来看问题,桥接模式则希望将继承关系转变为关联关系,使两个类独立存在。

详细说一下:

  • 桥接模式需要将抽象和实现区分开;
  • 桥接模式需要将“渠道”和“系统发送方式”这两种类别区分开;
  • 最后在“系统发送方式”的类里调用“渠道”的抽象接口,使他们从继承关系转变为关联关系。

用一句话总结桥接模式的理念,就是:“将抽象与实现解耦,将不同类别的继承关系改为关联关系。

请看以下代码:

package bridge

import "fmt"

// 两种发送消息的方法

type SendMessage interface {
  send(text, to string)
}

type sms struct{}

func NewSms() SendMessage {
  return &sms{}
}

func (*sms) send(text, to string) {
  fmt.Println(fmt.Sprintf("send %s to %s sms", text, to))
}

type email struct{}

func NewEmail() SendMessage {
  return &email{}
}

func (*email) send(text, to string) {
  fmt.Println(fmt.Sprintf("send %s to %s email", text, to))
}

// 两种发送系统

type systemA struct {
  method SendMessage
}

func NewSystemA(method SendMessage) *systemA {
  return &systemA{
    method: method,
  }
}

func (m *systemA) SendMessage(text, to string) {
  m.method.send(fmt.Sprintf("[System A] %s", text), to)
}

type systemB struct {
  method SendMessage
}

func NewSystemB(method SendMessage) *systemB {
  return &systemB{
    method: method,
  }
}

func (m *systemB) SendMessage(text, to string) {
  m.method.send(fmt.Sprintf("[System B] %s", text), to)
}

可以看到我们先定义了sms和email二种实现,以及接口SendMessage。接着我们实现了systemA和systemB,并调用了抽象接口SendMessage。

测试代码:

package bridge

func ExampleSystemA() {
  NewSystemA(NewSms()).SendMessage("hi", "baby")
  NewSystemA(NewEmail()).SendMessage("hi", "baby")
  // Output:
  // send [System A] hi to baby sms
  // send [System A] hi to baby email
}

func ExampleSystemB() {
  NewSystemB(NewSms()).SendMessage("hi", "baby")
  NewSystemB(NewEmail()).SendMessage("hi", "baby")
  // Output:
  // send [System B] hi to baby sms
  // send [System B] hi to baby email
}

如果你想要拆分或重组一个具有多重功能的复杂类,可以使用桥接模式。

Logo

技术共进,成长同行——讯飞AI开发者社区

更多推荐