go设计模式 | 青训营

117 阅读11分钟

介绍

基于对go语言的基础学习,本文来简单说一下golang中常见的设计模式

首先,设计模式的原则和目的是高内聚,低耦合,是解决某类重复问题成功的解决方案 。本文将会介绍单例模式,工厂模式,以及观察者模式。

单例模式

单例模式是一种创建型设计模式,它的主要目的是确保一个类只有一个实例,并提供一个全局访问点来访问该实例。

在单例模式中,类的构造函数被限制为私有,这样其他类就无法直接实例化该类。然后,该类提供一个静态方法或静态属性,用于获取该类的唯一实例。如果实例不存在,则创建一个新实例;如果实例已经存在,则返回现有的实例。

在实现单例模式时,可以采用多种方式,包括懒汉式(Lazy Initialization)、饿汉式(Eager Initialization)、双重检查锁(Double-Checked Locking)等。每种方式都有其适用的场景和注意事项。

  • 一个类只能有一个实例
  • 必须自行创建这个实例
  • 必须自行向整个系统提供这个实例

饿汉式单例模式

饿汉式:在初始化单例唯一指针的时候,就已经提前开辟好了一个对象,申请了内存。

饿汉式的好处是,不会出现线程并发创建,导致多个单例的出现,但是缺点是如果这个单例对象在业务逻辑没有被使用,也会客观的创建一块内存对象。

package main

import "fmt"

// 这是一个单例实体
type singleton struct {
}

func (s *singleton) singletonPrint() {
	fmt.Println("我是一个实例的方法")
}

// 创建一个全局的单例
var instance *singleton = new(singleton)

func getInstance() *singleton {
	return instance
}
func main() {
	admin1 := getInstance()
	admin1.singletonPrint()
}

懒汉式单例模式

下面是懒汉式实现非线程安全的设计方式,只有第一次调用的时候才会创建实例

如果多个线程或者协程同时首次调用GetInstance()方法有概率导致多个实例被创建,则违背了单例的设计初衷。

package main

import "fmt"

type singleton2 struct {
}

func (s *singleton2) singletonPrint() {
	fmt.Println("我是实例2")
}

var instance2 *singleton2

func getInstance2() *singleton2 {
	if instance2 == nil {
		instance2 = new(singleton2)
		return instance2
	}
	return instance2
}
func main() {
	s := getInstance2()
	s.singletonPrint()
}

线程安全的懒汉式设计模式

使用sync/atomic来进行内存的状态存留,atomic就可以自动加载和设置标记

package main

import (
	"fmt"
	"sync"
	"sync/atomic"
)

var initialized int32
var lock sync.Mutex

type singleton2 struct {
}

func (s *singleton2) singletonPrint() {
	fmt.Println("我是实例2")
}

var instance2 *singleton2

func getInstance2() *singleton2 {

	//如果被标记为1 则代表已经被创建了实例
	if atomic.LoadInt32(&initialized) == 1 {
		return instance2
	}

	//如果没有 加锁申请
	lock.Lock()
	defer lock.Unlock()
	if initialized == 0 {
		instance2 = new(singleton2)
		atomic.StoreInt32(&initialized, 1)
	}
	return instance2
}
func main() {
	s := getInstance2()
	s.singletonPrint()
}

优缺点

优点:
(1) 单例模式提供了对唯一实例的受控访问。
(2) 节约系统资源。由于在系统内存中只存在一个对象。

缺点:
(1) 扩展略难。单例模式中没有抽象层。
(2) 单例类的职责过重。

工厂模式

工厂模式是一种创建型设计模式,它提供了一种创建对象的方式,而无需直接暴露对象的实例化逻辑。工厂模式通过使用共同的接口或基类来创建对象,使得代码更加灵活、可扩展和可维护。

工厂模式的主要目标是封装对象的创建过程,将对象的实例化与使用代码分离开来。这样做的好处是,在需要创建对象的地方,只需要知道使用相应的工厂方法即可,而无需了解对象的具体实现细节。

为什么需要工厂模式

如果没有工厂模式,在开发者创建一个类的对象时,如果有很多不同种类的对象将会如何实现:

package main

import "fmt"

//水果类
type Fruit struct {
	//...
	//...
	//...
}

func (f *Fruit) Show(name string) {
	if name == "apple" {
		fmt.Println("我是苹果")
	} else if name == "banana" {
		fmt.Println("我是香蕉")
	} else if name == "pear" {
		fmt.Println("我是梨")
	}
}

//创建一个Fruit对象
func NewFruit(name string) *Fruit {
	fruit := new(Fruit)

	if name == "apple" {
		//创建apple逻辑
	} else if name == "banana" {
		//创建banana逻辑
	} else if name == "pear" {
		//创建pear逻辑
	}

	return fruit
}

func main() {
	apple := NewFruit("apple")
	apple.Show("apple")

	banana := NewFruit("banana")
	banana.Show("banana")

	pear := NewFruit("pear")
	pear.Show("pear")
}

不难看出,Fruit类是一个巨大的类,在该类的设计中存在如下几个问题:

Fruit类中包含很多if…else…代码块,整个类的代码相当冗长,代码越长,阅读难度、维护难度和测试难度也越大
Fruit类的职责过重,它负责初始化和显示所有的水果对象,将各种水果对象的初始化代码和显示代码集中在一个类中实现,违反了单一职责原则,不利于类的重用和维护

当需要增加新类型的水果时,必须修改Fruit类的构造函数NewFruit()和其他相关方法源代码,违反了 “开闭原则”

简单工厂模式

简单工厂模式并不属于GoF的23种设计模式。他是开发者自发认为的一种非常简易的设计模式,其角色和职责如下:

  • 具体工厂
    • 简单工厂模式的核心,它负责实现创建所有实例的内部逻辑。工厂类可以被外界直接调用,创建所需的产品对象。
    • 为什么叫具体工厂? 因为接下来还有一个抽象工厂
  • 抽象产品
    • 简单工厂模式所创建的所有对象的父类,它负责描述所有实例所共有的公共接口
  • 具体产品
    • 简单工厂模式所创建的具体实例对象
package main

import (
	"fmt"
)

type fruit interface {
	show()
}

// 具体类的实现
type apple struct {
}

func (a *apple) show() {
	fmt.Println("我是苹果")
}

type banana struct {
}

func (b *banana) show() {
	fmt.Println("我是香蕉")
}

type pear struct {
}

func (p *pear) show() {
	fmt.Println("我是梨子")
}

// 具体工厂
type factory struct {
}

func (f *factory) create(s string) fruit {
	var product fruit
	if s == "apple" {
		product = new(apple)
	} else if s == "banana" {
		product = new(banana)
	} else if s == "pear" {
		product = new(pear)
	}
	return product
}
func main() {
	factory := new(factory)

	apple := factory.create("apple")
	apple.show()

	banana := factory.create("banana")
	banana.show()

	pear := factory.create("pear")
	pear.show()
}

至此,完成了新建水果和水果方法的分离,同时新增水果只需要新增新的具体水果类即可,但是每一次新增水果还得修改工厂类的源代码,不够完美

优缺点

优点:

  1. 实现了对象创建和使用的分离。
  2. 不需要记住具体类名,记住参数即可,减少使用者记忆量。

缺点:

  1. 对工厂类职责过重,一旦不能工作,系统受到影响。
  2. 增加系统中类的个数,复杂度和理解度增加。3. 违反“开闭原则”,添加新产品需要修改工厂逻辑,工厂越来越复杂。

适用场景:

  1. 工厂类负责创建的对象比较少,由于创建的对象较少,不会造成工厂方法中的业务逻辑太过复杂。
  2. 客户端只知道传入工厂类的参数,对于如何创建对象并不关心。

工厂方法模式

  • 抽象工厂:工厂方法模式的核心,任何工厂类都必须实现这个接口。
    • 比简单工厂模式还多出一条
  • 工厂:具体工厂类是抽象工厂的一个实现,负责实例化产品对象。
  • 抽象产品:工厂方法模式所创建的所有对象的父类,它负责描述所有实例所共有的公共接口。
  • 具体产品:工厂方法模式所创建的具体实例对象。
package main

import (
	"fmt"
)

type fruit interface {
	show()
}

// 具体类的实现
type apple struct {
}

func (a *apple) show() {
	fmt.Println("我是苹果")
}

type banana struct {
}

func (b *banana) show() {
	fmt.Println("我是香蕉")
}

type pear struct {
}

func (p *pear) show() {
	fmt.Println("我是梨子")
}

// 抽象工厂
type factory interface {
	create() fruit
}

// 具体工厂
type factoryApple struct {
}

func (f *factoryApple) create() fruit {
	var fruit fruit
	fruit = new(apple)
	return fruit
}

type factoryBanana struct {
}

func (f *factoryBanana) create() fruit {
	var fruit fruit
	fruit = new(banana)
	return fruit
}

type factoryPear struct {
}

func (f *factoryPear) create() fruit {
	var fruit fruit
	fruit = new(pear)
	return fruit
}

func main() {

	//新建一个抽象类 new一个苹果工厂
	var absFactory factory
	absFactory = new(factoryApple)

	//新建一个抽象水果 利用抽象工厂实例的具体工厂new一个苹果
	var absFruit fruit
	absFruit = absFactory.create()
	absFruit.show()

	//新建一个抽象类 new一个香蕉工厂
	var absFactory2 factory
	absFactory2 = new(factoryBanana)

	//新建一个抽象水果 利用抽象工厂实例的具体工厂new一个香蕉
	var absFruit2 fruit
	absFruit2 = absFactory2.create()
	absFruit2.show()
}

优缺点

优点:

不需要记住具体类名,甚至连具体参数都不用记忆。

实现了对象创建和使用的分离。

系统的可扩展性也就变得非常好,无需修改接口和原类。

对于新产品的创建,符合开闭原则。

缺点:

  1. 增加系统中类的个数,复杂度和理解度增加。
  2. 增加了系统的抽象性和理解难度。

适用场景:

  1. 客户端不知道它所需要的对象的类。
  2. 抽象工厂类通过其子类来指定创建哪个对象。

观察者模式

观察者模式是一种行为型设计模式,它建立了对象之间的一对多依赖关系,当一个对象的状态发生变化时,它的所有依赖对象都会收到通知并自动更新。

观察者模式的主要目标是实现对象之间的松耦合,以便于对象之间的交互和通信。在观察者模式中,存在两种核心角色:观察者(Observer)和被观察者(Subject)。

  • Subject: 被观察的对象。当需要被观察的状态发生变化时,需要通知队列中所有观察者对象。Subject需要维持(添加,删除,通知)一个观察者对象的队列列表。
  • ConcreteSubject: 被观察者的具体实现。包含一些基本的属性状态及其他操作。
  • Observer: 接口或抽象类。当Subject的状态发生变化时,Observer对象将通过一个callback函数得到通知。
  • ConcreteObserver: 观察者的具体实现。得到通知后将完成一些具体的业务逻辑处理。

类似于发布订阅模式

package main

import "fmt"

// Listener 抽象的通知
type Listener interface {
	OnTeacherComing()
}

type StuZhangSan struct {
	BadThing string
}

func (stu *StuZhangSan) DoBadThing() {
	fmt.Println("张三,正在", stu.BadThing)
}
func (stu *StuZhangSan) OnTeacherComing() {
	fmt.Println("张三,停止", stu.BadThing)
}

type StuLiSi struct {
	BadThing string
}

func (stu *StuLiSi) DoBadThing() {
	fmt.Println("李四,正在", stu.BadThing)
}
func (stu *StuLiSi) OnTeacherComing() {
	fmt.Println("李四,停止", stu.BadThing)
}

type StuWangWu struct {
	BadThing string
}

func (stu *StuWangWu) DoBadThing() {
	fmt.Println("王五,正在", stu.BadThing)
}
func (stu *StuWangWu) OnTeacherComing() {
	fmt.Println("王五,停止", stu.BadThing)
}

// Monitor 具体的观察者
type Monitor struct {
	listenerList []Listener
}

// AddListener 添加监听者
func (m *Monitor) AddListener(listener Listener) {
	m.listenerList = append(m.listenerList, listener)
}

// RemoveListener 删除监听者
func (m *Monitor) RemoveListener(listener Listener) {
	for i, v := range m.listenerList {
		if listener == v {
			m.listenerList = append(m.listenerList[:i], m.listenerList[i+1:]...)
			break
		}
	}
}

func (m *Monitor) Notify() {
	for _, v := range m.listenerList {
		v.OnTeacherComing()
	}
}

func main() {
	z3 := &StuZhangSan{BadThing: "抄作业"}
	l4 := &StuLiSi{BadThing: "打王者"}
	w5 := &StuWangWu{BadThing: "看李四打王者"}

	fmt.Println("上课了,但是老师没来")
	z3.DoBadThing()
	l4.DoBadThing()
	w5.DoBadThing()
	monitor := new(Monitor)
	monitor.AddListener(z3)
	monitor.AddListener(l4)
	monitor.AddListener(w5)

	fmt.Println("老师来了,班长通知学生")
	monitor.Notify()

}

优缺点

优点

(1) 观察者模式可以实现表示层和数据逻辑层的分离,定义了稳定的消息更新传递机制,并抽象了更新接口,使得可以有各种各样不同的表示层充当具体观察者角色。

(2) 观察者模式在观察目标和观察者之间建立一个抽象的耦合。观察目标只需要维持一个抽象观察者的集合,无须了解其具体观察者。由于观察目标和观察者没有紧密地耦合在一起,因此它们可以属于不同的抽象化层次。

(3) 观察者模式支持广播通信,观察目标会向所有已注册的观察者对象发送通知,简化了一对多系统设计的难度。

(4) 观察者模式满足“开闭原则”的要求,增加新的具体观察者无须修改原有系统代码,在具体观察者与观察目标之间不存在关联关系的情况下,增加新的观察目标也很方便。

缺点

(1) 如果一个观察目标对象有很多直接和间接观察者,将所有的观察者都通知到会花费很多时间。

(2) 如果在观察者和观察目标之间存在循环依赖,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。

(3) 观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。

适用场景

(1) 一个抽象模型有两个方面,其中一个方面依赖于另一个方面,将这两个方面封装在独立的对象中使它们可以各自独立地改变和复用。

(2) 一个对象的改变将导致一个或多个其他对象也发生改变,而并不知道具体有多少对象将发生改变,也不知道这些对象是谁。

(3) 需要在系统中创建一个触发链,A对象的行为将影响B对象,B对象的行为将影响C对象……,可以使用观察者模式创建一种链式触发机制。