GO 常用设计模式

 

设计模式简介


什么是设计模式

设计模式(design pattern):是对软件设计中普遍存在、反复出现的问题所提出的解决方案,这里的问题就是我们应该怎么去写/设计我们的代码,让我们的代码可读性、可扩展性、可重用性、可靠性更好,通过合理的代码设计让我们的程序拥有“高内聚,低耦合”的特性,这就是设计模式要解决的问题。

本质是为了提高软件的可维护性、可扩展性、通用性,并降低软件的复杂度。

设计模式分类

1. 创建型模式:提供创建对象的机制,增加已有代码的灵活性和可复用性。

2. 结构型模式:介绍如何将对象和类组装成较大的结构,并同时保持结构的灵活和高效

3. 行为型模式:负责对象间的高效沟通和职责委派

我们为什么要学

设计模式是前人摸索出来的一种代码设计经验,学习它就像站在巨人的肩膀上,参悟其中的设计理念,从而在实践中写出高质量代码。因此不论是编程新手还是老鸟,都应该去学习设计模式。

软件设计原则

1. 代码复用

2. 扩展性

设计原则

封装变化的内容

面向接口开发,而不是面向实现

原则:面向接口进行开发,而不是面向实现;依赖于抽象类型,而不是具体类型。

组合优于继承

SOLID原则

一. 单一职责原则(Single Responsibility Principle)

二. 开闭原则(Open/Closed Principle)

三. 里氏替换原则(Liskov Substitution Principle)

四. 接口隔离原则(Interface Segregation Principle)

五. 依赖倒置原则(Dependency Inversion Principle)

创建模式

简单工厂

简单工厂就是我们首先声明一个类,这个类叫做工厂类,在这个内我们可以声明一个(静态)方法,这个方法会根据参数的值生成相应的对象(我们把这个对象叫“产品”)。

简单工厂的好处是:

  1. 使用者可以直接获得一个构造好的对象(根据我们在工厂类内方法的传参值),而不需要关心这个对象的构造的过程。
  2. 当我们新增一个对象(产品)时,我们只需要修改这个工厂类内的方法就行了,这样降低了所需对象(产品)与我们使用对象的代码逻辑的耦合,符合开闭原则。

代码

package main

import "fmt"

// 对象接口
type BMW interface {
	run()
}

// 构建对象1
type BMW730 struct {
}
func (b BMW730) run() {
	fmt.Println("BMW730 is running...")
}

// 构建对象2
type BMW840 struct {
}
func (b BMW840) run() {
	fmt.Println("BMW840 is running...")
}

// 工厂对象
type Factory struct {
}
func (f Factory)produceBMW(BMW_TYPE string) BMW {
	switch BMW_TYPE {
	case "BMW730":
		return BMW730{}
	case "BMW840":
		return BMW840{}
	default:
		return nil
	}
}

func main() {
	// 生成工厂对象
	factory := new(Factory)
	// 使用工厂对象生成产品对象
	p1 := factory.produceBMW("BMW730")
	p1.run()

	p2 := factory.produceBMW("BMW840")
	p2.run()
}

工厂方法

在工厂方法模式(Factory Methord Pattern)中,工厂父类(在go中为interface)负责定义创建产品对象的公共接口,子工厂类要实现父工厂中定义的接口,每一个工厂子类则负责生成具体种类的产品对象,这样做的目的是将产品类的实例化操作延迟到工厂子类中完成。

工厂方法的好处:

在工厂方法中,客户端不需要知道产品的类名,只需要知道所对应的子工厂类即可,具体的产品对象由具体的子工厂类创建,客户端只需要知道创建具体产品的子工厂类对象就行。

当要有新种类的产品对象出现时,我们只需要要新增生产这个产品的子工厂类(这个类去实现父工厂类中定义的接口),就可以通过这个子工厂类就能生产这个新产品对象了,此时我们没有对代码进行修改,只是对代码进行了扩展(新增了一个子工厂类),因此符合开闭原则。

代码

package main

import "fmt"

// 对象接口
type BMW interface {
	run()
}

// 构建对象1
type BMW730 struct {
}
func (b BMW730) run() {
	fmt.Println("BMW730 is running...")
}

// 构建对象2
type BMW840 struct {
}
func (b BMW840) run() {
	fmt.Println("BMW840 is running...")
}

// 抽象父类工厂
type Factory interface {
	produceBMW() BMW
}
// 子类实现父抽象类接口,生成特定种类的产品BMW730
type BMW730Factory struct {
}
func (b BMW730Factory) produceBMW() BMW {
	return BMW730{}
}

// 子类实现父抽象类接口,生成特定种类的产品BMW840
type BMW840Factory struct {
}
func (b BMW840Factory) produceBMW() BMW {
	return BMW840{}
}

// 通过向参数中传不同的子类,生成不同的产品(某种车)
func get_bmw(bmw Factory) {
	car := bmw.produceBMW()
	car.run()
}

func main() {
	// 生成不同子工厂对象
	factory_730 := new(BMW730Factory)
	factory_840 := new(BMW840Factory)

	// 通过不同子工厂对象,生产不同种类的车
	get_bmw(factory_730)
	get_bmw(factory_840)
}

抽象工厂

抽象工厂(Abstract Factory Pattern)通过提供了一个抽象工厂类,这个抽象工厂类定义了多个接口,每个接口都可以生产一种产品。子类工厂在实现抽象工厂的接口后,可以通过不同的接口生产出不同种类的对象。

代码

package main

import "fmt"

// 构建对象1
type BMW730 struct {
}
func (b BMW730) run() {
	fmt.Println("BMW730 is running...")
}

// 构建对象2
type BMW840 struct {
}
func (b BMW840) run() {
	fmt.Println("BMW840 is running...")
}

// 抽象工厂
type BMWFactory interface {
	produce730BMW() BMW730
	produce840BMW() BMW840
}

// 具体工厂子类
type ConcreteBMWFactory struct {
}

func (c ConcreteBMWFactory) produce730BMW() BMW730 {
	return BMW730{}
}
func (c ConcreteBMWFactory) produce840BMW() BMW840 {
	return BMW840{}
}
func main() {
	// 实例化子类工厂,这个工厂可以根据不同的函数生成不同种类的车产品
	concreteBMWFactory := ConcreteBMWFactory{}

	bmw730 := concreteBMWFactory.produce730BMW()
	bmw840 := concreteBMWFactory.produce840BMW()
	bmw730.run()
	bmw840.run()
}

单例模式

单例模式(Singleton Pattern)是一种简单的创建模式,有些对象我们往往只需要全局一个,比如全局缓存、数据库连接等。

使用场景

需要频繁实例化然后销毁的对象

创建对象耗时过多或资源过多,但有经常用到

系统只需要一个实例对象,如果系统要求提供一个唯一的序列号生成器或资源管理器,或者对象消耗资源太大而只允许创建一个对象。

代码

package main

import (
	"fmt"
	"sync"
)

// 数据库连接对象实例
type DBInstance struct {
}

var (
	once       sync.Once
	dbInstance *DBInstance
)

// 注意初始对象实例过程也可以放到init函数中进行
func NewInstance() *DBInstance {
	// 只有第一次才会实例化数据库连接对象
	if dbInstance != nil {
		once.Do(func() {
			dbInstance = &DBInstance{}
		})
	}
	return dbInstance
}

func main() {
	for i := 0; i <= 10; i++ {
		// 在使用的时候获取数据库实例对象
		db := NewInstance()
		fmt.Println(db)
	}
}

建造者模式

建造者模式(Build Pattern)又叫生成器,将一个复杂对象分解成多个相对简单的部分,之后按步骤创建这个复杂对象的每一个部分。该模式允许你使用相同的创建代码生成不同的对象。

代码

package main

import "fmt"

type Car struct {
	engine  string
	chassis string
	body    string
}

// 生成器
type CarBuilder struct {
	engine  string
	chassis string
	body    string
}

func (c *CarBuilder) addChassis(chassis string) {
	c.chassis = chassis
}

func (c *CarBuilder) addEngine(engine string) {
	c.engine = engine
}

func (c *CarBuilder) addBody(body string) {
	c.body = body
}

func (c *CarBuilder) build() Car {
	return Car{
		engine: c.engine,
		chassis: c.chassis,
		body:    c.body,
	}
}

func main() {
	carBuilder := &CarBuilder{}
	// 按步骤创建对象
	carBuilder.addEngine("v12")
	carBuilder.addChassis("复合材料")
	carBuilder.addBody("镁合金")
	// 构建Car对象
	car := carBuilder.build()
	fmt.Println(car)
}

原型模式

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

如果遍历对象的所有成员,将其依次复制到新对象中,会稍显麻烦。

原型模式(Prototype Pattern)将这个克隆新对象过程委派给了被克隆对象(其实就是将这个被克隆对象的类中增加一个克隆函数),被克隆对象叫做原型。

注意:在写克隆函数时,要注意深浅拷贝问题。

package main

import "fmt"

type obj struct {
	name *string
}

func (o *obj) Clone() *obj {
	new_obj := *o
	return &new_obj
}

func main() {
	name := "qiliang"
	o1 := &obj{name: &name}
	o2 := o1.Clone()
	// 注意会有浅拷贝问题
	*o2.name = "xiaolin"
	fmt.Println(*o1.name)
	fmt.Println(*o2.name)
}

结构型模式


适配器模式

适配器模式(Adaptor Pattern)说白了就是兼容,假设一开始我们提供了A对象,后期随着业务迭代,有需要在A对象的基础上衍生出不同的需求。如果有很多函数已经在线上调用了A对象,此时再对A对象修改就比较麻烦,也可能会出现问题。甚至更糟糕的情况,你坑你没有程序库的源代码,从而无法进行修改。

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

package main

import "fmt"

type CM2M struct {
}

func (c CM2M) CMtoM(cm float64) (m float64) {
	m = cm / 100
	return
}

type M2CM struct {
}

func (c M2CM) MtoCM(m float64) (cm float64) {
	cm = m * 100;
	return cm
}

// 适配器函数,自动进行cm与m之间的转换
// 有了这个适配器逻辑后我们,不需要再关注转换的逻辑
type AdapterTransLength interface {
	transLength(string, float64) float64
}

type TransLengthAdapter struct {
}

func (t TransLengthAdapter)transLength(whickType string, length float64) float64 {
	if whickType == "m" {
		return M2CM{}.MtoCM(length)
	}
	return CM2M{}.CMtoM(length)
}

func main()  {
	transAdapter := TransLengthAdapter{}
	ans := transAdapter.transLength("m", 10)
	fmt.Println(ans)
}

桥接模式

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

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

此时至少需要提供4中方法:systemA to sms、systemA to email、systemB to sms、systemB to email

如果在增加新的需求维度,那么类的数量会出现质数增长,这是我们不能接受的

解决方案

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

详细说一下:

  1. 桥接模式需要将抽象和实现区分开;
  2. 桥接模式需要将“渠道”和“学习通发送方式”这两种类别区分开;
  3. 最后再“系统发送方式”的雷利调用“渠道”的抽象接口,使他们从继承关系转变为关联关系。

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

代码

package main

import "fmt"

// 声明消息接口
type SendMessage interface {
	send(text, to string)
}

// 声明第一种消息
type sms struct {
}

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

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

// 声明第二种消息
type email struct {
}

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

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

// 声明两种发送渠道,这两种发送渠道都要支持sms和email消息(通过组合SendMessage接口来实现
// 本质是因为在组合SendMessage接口后,sms、emali都可以放到systemA中的method字段,当method字段值不同时就可以发送不同的消息)
type systemA struct {
	method SendMessage
}

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

func (s *systemA) SystemASendMes(text, to string) {
	s.method.send("SystemA "+text, to)
}

type systemB struct {
	method SendMessage
}

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

func (b *systemB) SystemBSendMes(text, to string) {
	b.method.send("SystemB " + text, to)
}

func main() {
	// 声明要发送的类型
	sms := NewSms()
	email := NewEmail()

	// 在SystemA中发送两种消息
	systemA1 := NewSystemA(sms)
	systemA2 := NewSystemA(email)

	systemA1.SystemASendMes("ni hao !", "qiliang")
	systemA2.SystemASendMes("ni hao !", "qiliang")

	systemB1 := NewSystemB(sms)
	systemB2 := NewSystemB(email)

	systemB1.SystemBSendMes("gan xie !", "xiaoming")
	systemB2.SystemBSendMes("gan xie !", "xiaoming")
}

装饰器模式

有时候我们对一个类进行封装,形成一个新类,这个新类在原类的基础上拥有额外的功能。

例如一个披萨类,你可以在对披萨类进行封装,形成新的类如番茄披萨类和芝士披萨类。此时这个封装就是装饰器模式的核心思想。

简单来说,装饰器模式就是将对象封装到形成一个新对象,这个信息对象功能更丰富(可以理解为:为源对象绑定了新的行为功能)

如果你希望在无需修改对象的情况下使用对象,并且希望对象新增额外的行为,就可以考虑使用装饰器模式。

代码

package main

import "fmt"

type pizza interface {
	getPrice() int
}

type basePizza struct {
}

func (p basePizza) getPrice() int {
	return 15
}

type tomatoPizza struct {
	pizza pizza
}

func (p tomatoPizza) getPrice() int {
	return p.pizza.getPrice() + 10
}

type cheesePizza struct {
	pizza pizza
}

func (p cheesePizza) getPrice() int {
	return p.pizza.getPrice() + 20
}

func main() {
	tomPizza := tomatoPizza{}
	tomPizza.pizza = basePizza{}
	price := tomPizza.getPrice()
	fmt.Println(price)
}

代理模式

如果你需要在访问一个对象时,有一个像“代理”一样的角色,它可以在访问对象之前为你进行缓存检查、权限判断等访问控制,在访问对象之后为你进行结果缓存、日志记录等处理,那么可以考试率使用代码模式(Proxy Pattern)。

会议一下一些web框架的router模块,当客户端访问一个接口时,在最终执行对应的接口之前,router模块会执行一些事前操作,进行权限判断操作,在执行之后会记录日志,这就是典型的代理模式。

代理模式需要一个代理类,其包含执行真正对象所需要的成员变量,并由代理类管理整个声明周期。

代码

package main

import "fmt"

type Subject interface {
	ProxyFun() string
}

// 声明代理类
type Proxy struct {
	real RealSubject
}

func (p Proxy) ProxyFun() string {
	var ans string

	// 在低啊用真实对象之前,检查缓存、判断权限等
	p.real.PreFun()
	p.real.RealFun()
	p.real.AfterFun()
	// 在调用完操作之后,可以缓存结果、对结果进行处理等(如脱敏)、记录日志等

	return ans
}

type RealSubject struct {
}

func (s RealSubject) RealFun() {
	fmt.Println("real...")
}

func (s RealSubject) PreFun() {
	fmt.Println("Pre...")
}

func (s RealSubject) AfterFun() {
	fmt.Println("After...")
}

func main() {
	rs := RealSubject{}
	proxy := Proxy{real: rs}
	proxy.ProxyFun()
}

行为行模式

观察者模式

如果你需要在对一个对象的状态被改变时,其他对象能作为“观察者”被通知,就可以使用观察者模式。

我们将自身状态改变就回通知其他对象的对象称为“发布者”,关注发布者状态变化的对象称为“订阅者”。

代码

package main

import "fmt"

// 发布者-主题
type Subject struct {
	observers []Observer
	content string
}

func NewSubject() *Subject {
	return &Subject{
		observers: make([]Observer, 0),
	}
}

// 添加订阅者
func (s *Subject) AddObserver(o Observer) {
	s.observers = append(s.observers, o)
}

// 通知消费者
func (s *Subject) Notify() {
	for _, o := range s.observers {
		o.SendMessage(s)
	}
}

// 发布消息
func (s *Subject) UpdateContent(content string) {
	s.content = content
	s.Notify()
}

// 观察者-订阅者接口
type Observer interface {
	SendMessage(*Subject)
}

// 订阅者
type Reader struct {
	name string
}

func NewReader(name string) *Reader {
	return &Reader{
		name: name,
	}
}

func (r Reader) SendMessage(s *Subject) {
	fmt.Println(r.name + " " + s.content)
}

func main() {
	subject := NewSubject()
	reader1 := NewReader("qiliang")
	reader2 := NewReader("xiaolin")

	subject.AddObserver(reader1)
	subject.AddObserver(reader2)

	subject.UpdateContent("ni hao !")

}

策略模式

假设需要实现一个根据出行方式规划路线导航功能,出行的方式可以选择不行、骑行、开车,最简单的方式就是分别实现这个3种方法,供客户端调用。但是这样做会使客户端选择路线的代码和出行方式耦合了起来,如果新增一种出行的方式,就要修改客户端路线选择方案代码,这不符合开闭原则。

解决办法是使用策略模式(Strategy Pattern),它会将每种出行方案都抽取到一组新的类中,组中的类都会实现一个出行Interface,这个出行Interface约定了出行接口函数。客户端只需要指定指定所需要的出行方案即可。

代码

package main

import "fmt"

// 策略维护对象,包含了路线信息name 和 所用的的策略strategy
type Travel struct {
	name     string
	strategy Strategy
}

func NewTravel(name string, strategy Strategy) *Travel {
	return &Travel{
		name:     name,
		strategy: strategy,
	}
}

// 执行路线策略
func (p *Travel) getTraffic() {
	p.strategy.traffic(p)
}

// 路线策略接口
type Strategy interface {
	traffic(*Travel)
}

// 实现走路方式
type walk struct {
}

func (w *walk) traffic(t *Travel) {
	fmt.Println(t.name + " walk...")
}
// 实现开车方式
type drive struct {
}

func (w *drive) traffic(t *Travel) {
	fmt.Println(t.name + " drive...")
}


func main() {
	// 生成走路路线策略
	travel1 := NewTravel("qiliang", &walk{})
	// 生成开车路线策略
	travel2 := NewTravel("xiaolin", &drive{})

	// 运行策略
	travel1.getTraffic()
	travel2.getTraffic()
}


// OutPut
qiliang walk...
xiaolin drive...

模板方法模式 

模板方法建议将过程/算法分解为一系列步骤(就比如:盖房子这个过程,对盖普通的房子或者盖别墅,它的大致步骤是相同,我们可以简单考虑的将盖房子的过程分为:步骤1 打地基、步骤2 建围墙、步骤3 盖房顶 这个三个基类中的方法)

然后将这些步骤改写成基类中的方法, 当我子类去继承基类时,子类也拥有了这些方法,并且在子类中我们可以重写基类中的某些方法,让子类更适配某个具体的流程(比如这个子类表示的是 “盖别墅类”, 那么我们可以重写“盖别墅类中”的“步骤1 打地基”方法,因为盖别墅打的地基,可能和基类中默认实现的打地基的方式不太一样,通过重写这个基类中的“步骤1 打地基”方法)。

如果以后新增加 “盖小区的”这个过程,我们仍然可以使用 基类中的定义的模板方法(就是盖房子的三个步骤),并对某些方法进行重写,因为我们只重写了部分方法,其它的步骤是用基类的默认方法,这样就减少了我们的代码量工作。

举例:

让我们来考虑一个一次性密码功能 (OTP) 的例子。 将 OTP 传递给用户的方式多种多样 (短信、 邮件等)。 但无论是短信还是邮件, 整个 OTP 流程都是相同的:

  1. 生成随机的 n 位数字。
  2. 在缓存中保存这组数字以便进行后续验证。
  3. 准备内容。
  4. 发送通知。

后续引入的任何新 OTP 类型都很有可能需要进行相同的上述步骤。

因此, 我们会有这样的一个场景, 其中某个特定操作的步骤是相同的, 但实现方式却可能有所不同。 这正是适合考虑使用模板方法模式的情况。

首先, 我们定义一个由固定数量的方法组成的基础模板算法。 这就是我们的模板方法。 然后我们将实现每一个步骤方法, 但不会改变模板方法。

代码

  package main

import "fmt"

// 定义OTP步骤接口
type IOtp interface {
	genRandomOTP(int) string
	saveOTPCache(string)
	getMessage(string) string
	sendNotification(string) error
}

// 对IOtp封装一下
type Otp struct {
	iOtp IOtp
}

// 相同OTP执行步骤调用
func (o *Otp) genAndSendOTP(otpLength int) error {
	otp := o.iOtp.genRandomOTP(otpLength)
	o.iOtp.saveOTPCache(otp)
	message := o.iOtp.getMessage(otp)
	err := o.iOtp.sendNotification(message)
	if err != nil {
		return err
	}
	return nil
}

// 定义Sms实现IOtp接口
type Sms struct {
}

func (s *Sms) genRandomOTP(len int) string {
	randomOTP := "1234"
	fmt.Printf("SMS: generating random otp %s\n", randomOTP)
	return randomOTP
}

func (s *Sms) saveOTPCache(otp string) {
	fmt.Printf("SMS: saving otp: %s to cache\n", otp)
}

func (s *Sms) getMessage(otp string) string {
	return "SMS OTP for login is " + otp
}

func (s *Sms) sendNotification(message string) error {
	fmt.Printf("SMS: sending sms: %s\n", message)
	return nil
}

// 定义Email实现IOtp接口
type Email struct {
}

func (s *Email) genRandomOTP(len int) string {
	randomOTP := "1234"
	fmt.Printf("EMAIL: generating random otp %s\n", randomOTP)
	return randomOTP
}

func (s *Email) saveOTPCache(otp string) {
	fmt.Printf("EMAIL: saving otp: %s to cache\n", otp)
}

func (s *Email) getMessage(otp string) string {
	return "EMAIL OTP for login is " + otp
}

func (s *Email) sendNotification(message string) error {
	fmt.Printf("EMAIL: sending email: %s\n", message)
	return nil
}

func main() {
	smsOTP := &Sms{}
	o := Otp{
		iOtp: smsOTP,			// 接口实现多态
	}
	o.genAndSendOTP(4)

	fmt.Println("")
	emailOTP := &Email{}
	o = Otp{
		iOtp: emailOTP,			// 接口实现多态
	}
	o.genAndSendOTP(4)
}

文章参考:

Go 常用设计模式 

图解九种常见的设计模式 – SegmentFault 思否

用Go语言实现23种设计模式 – 掘金


版权声明:本文为qq_34261446原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。