定义

将一个复杂对象的构建与它的表示分离,使得同样的构建 过程可以创建不同的表示。

复杂的构造函数

创建一个对象最常用的方式是,使用 new 关键字调用类的构造函数来完成。举个常用资源连接池的例子

type ResourcePool struct {
	name     string
	maxTotal int
	maxIdle  int
	minIdle  int
}
resourcePool := NewResourcePool("dbconnectionpool", 16, 8, 
10)

在可配置项不多的时候没什么问题,但是,如果可配置项逐渐增多,以gorm的配置文件为例

// Config GORM config
type Config struct {
	// GORM perform single create, update, delete operations in transactions by default to ensure database data integrity
	// You can disable it by setting `SkipDefaultTransaction` to true
	SkipDefaultTransaction bool
	// NamingStrategy tables, columns naming strategy
	NamingStrategy schema.Namer
	// FullSaveAssociations full save associations
	FullSaveAssociations bool
	// Logger
	Logger logger.Interface
	// NowFunc the function to be used when creating a new timestamp
	NowFunc func() time.Time
	// DryRun generate sql without execute
	DryRun bool
	// PrepareStmt executes the given query in cached statement
	PrepareStmt bool
	// DisableAutomaticPing
	DisableAutomaticPing bool
	// DisableForeignKeyConstraintWhenMigrating
	DisableForeignKeyConstraintWhenMigrating bool
	// DisableNestedTransaction disable nested transaction
	DisableNestedTransaction bool
	// AllowGlobalUpdate allow global update
	AllowGlobalUpdate bool
	// QueryFields executes the SQL query with all fields of the table
	QueryFields bool
	// CreateBatchSize default create batch size
	CreateBatchSize int

	// ClauseBuilders clause builder
	ClauseBuilders map[string]clause.ClauseBuilder
	// ConnPool db conn pool
	ConnPool ConnPool
	// Dialector database dialector
	Dialector
	// Plugins registered plugins
	Plugins map[string]Plugin
    // ... 省略一些参数
}

这时,这个构造函数就变得复杂了。构造函数参数列表会变得很长,容易搞错各参数的顺序,造成隐蔽的bug。

resourcePool := NewResourcePool("dbconnectionpool",   16, null, 8, null, false , true, 10, 20false true )

开始重构,这里使用 set() 函数来给成员变量赋值,以替代冗长的构造函数,并在set()函数里做参数校验。

func NewResourcePool(name string) *ResourcePool {
	this := &ResourcePool{}
	this.SetName(name)
	return this
}
func (this *ResourcePool) SetName(name string) *ResourcePool {
	if len(name) <= 0 {
		panic(any("name should not be empty."))
	}
	this.name = name
	return this
}
func (this *ResourcePool) SetMaxTotal(maxTotal int) *ResourcePool {
	if maxTotal <= 0 {
		panic(any("maxTotal should be positive."))
	}
	this.maxTotal = maxTotal
	return this
}
func (this *ResourcePool) SetMinIdle(minIdle int) *ResourcePool {
	if minIdle < 0 {
		panic(any("minIdle should not be negative."))
	}
	this.minIdle = minIdle
	return this
}

重构后的构造函数:

resourcePool := NewResourcePool("dbconnectionpool").SetMaxTotal(16).SetMinIdle(8)

但是,这里还是有几个问题:

  1. 如果必填的配置项有很多,那构造函数就又会出现参数列表很长的问题。
  2. 如果配置项之间有一定的依赖关系,并校验参数的合法性。
  3. 如果希望ResourcePool的配置项不对外提供修改方法。

使用构造者重构

  1. 首先,创建一个构造者类Builder,并把参数改为私有,提供set()函数修改。
  2. 其次build() 方法真正创建对象之前,做集中的校验,校验通过之后才会创建对象。

func NewResourcePool(builder *ResourcePoolBuilder) *ResourcePool {
	this := &ResourcePool{}
	this.name = builder.name
	this.maxTotal = builder.maxTotal
	this.maxIdle = builder.maxIdle
	this.minIdle = builder.minIdle
	return this
}

type ResourcePoolBuilder struct {
	name     string
	maxTotal int
	maxIdle  int
	minIdle  int
}

func Builder() *ResourcePoolBuilder {
	builder := &ResourcePoolBuilder{}
	builder.maxTotal = 16
	builder.minIdle = 5
	return builder
}

func (b *ResourcePoolBuilder) Build() *ResourcePool {
	// 校验逻辑放到这里来做,包括必填项校验、依赖关系校验、约束条件校验等
	if len(b.name) <= 0 {
		panic(any("name should not be empty."))
	}
	if b.maxIdle > b.maxTotal {
		panic(any("maxIdle should bigger than maxTotal."))
	}
	return NewResourcePool(b)
}


func (this *ResourcePoolBuilder) SetName(name string) *ResourcePoolBuilder {
	if len(name) <= 0 {
		panic(any("name should not be empty."))
	}
	this.name = name
	return this
}
func (this *ResourcePoolBuilder) SetMaxTotal(maxTotal int) *ResourcePoolBuilder {
	if maxTotal <= 0 {
		panic(any("maxTotal should be positive."))
	}
	this.maxTotal = maxTotal
	return this
}
func (this *ResourcePoolBuilder) SetMinIdle(minIdle int) *ResourcePoolBuilder {
	if minIdle < 0 {
		panic(any("minIdle should not be negative."))
	}
	this.minIdle = minIdle
	return this
}

重构后的构造函数

resourcePool := Builder().SetName("test").SetMaxTotal(0).SetMinIdle(5).Build()

如此,

  • 这样我们就只能通过建造者Builder 来创建 ResourcePool 类对象。
  • 要修改resourcePool只能通过 Builder提供的 set()函数,配置项不对外提供修改方法。
  • 参数校验或者提供默认参数可以放在build()函数内。

小结

构造者模式原理并不复杂,主要适当的场景中灵活使用。通过构造者类Builder、提供的set()函数设置必选项,最终调用build()处理构造类之前的一些逻辑。如此,可以以通过设置不同的可选参数,“定制化”地创建不同的复杂对象.

参考

基础概念

来自Head First设计模式一书的定义

观察者模式 Observer 观察者模式 定义了一系列对象之间的一对多的关系,当一个对象的状态改变, 其他依赖者都会收到到通知。

经常观察者模式也称发布订阅模式,一般观察者模式有以下组成:

  • Subject-被观察者,亦或是发布者-Publisher
  • Observer-观察者,亦或是订阅者-Subscribe

经典的实现方式

以下是golang的具体实现:observer.go

package design_mode

import (
	"fmt"
)

type Subject interface {
	RegisterObserver(Observer)
	NotifyObservers(message interface{})
}

type Observer interface {
	Update(message interface{})
}

type ConcreteSubject struct {
	observerList []Observer
}

func NewConcreteSubject() Subject {
	return &ConcreteSubject{}
}

func (o *ConcreteSubject) RegisterObserver(observer Observer) {
	o.observerList = append(o.observerList, observer)
}

func (o *ConcreteSubject) NotifyObservers(message interface{}) {
	for _, observer := range o.observerList {
		observer.Update(message)
	}
}

type ConcreteObserverOne struct {
}

func (b *ConcreteObserverOne) Update(message interface{}) {
	fmt.Printf("ConcreteObserverOne is notified.%v \n", message)
}

type ConcreteObserverTwo struct {
}

func (b *ConcreteObserverTwo) Update(message interface{}) {
	fmt.Printf("ConcreteObserverOne is notified.%v \n", message)
}

observer_test.go

package design_mode

import "testing"

func TestObserver(t *testing.T) {
	concreteSubject := NewConcreteSubject()
	concreteSubject.RegisterObserver(&ConcreteObserverOne{})
	concreteSubject.RegisterObserver(&ConcreteObserverTwo{})
	concreteSubject.NotifyObservers("hello every one")
}

OutPut:

ConcreteObserverOne is notified.hello every one 
ConcreteObserverOne is notified.hello every one 

可以看到例子里包含了以下接口和实现:

  • Subject 主题接口: 注册实现、通知所有观察者
  • Observer 观察者接口: 接收主体通知的接口。
  • ConcreteSubject 主题的具体实现
  • ConcreteObserverOne/ConcreteObserverTwo 不同的观察者实现

当你需要增加一个观察者时,只需要实现 Update()接口和注册Register到subject即可。

应用场景

那么观察者模式在什么场景下适用呢?接着我们以一个游戏用户注册的例子来套用一下。

场景: 玩家在注册成功后,将在“地图服务”创建一个出生点位,同时“邮件服务”会发送一份新手礼包。

在没有使用观察者模式时,可能是这么写的。

package main

import "fmt"

type WorldMapService struct {
}
func NewWorldMapService() WorldMapService {
	return WorldMapService{}
}
func (w WorldMapService) Join(u User) {
	fmt.Println(fmt.Sprintf("欢迎%v来到新手村", u.Name))
}

type MailService struct {
}
func NewMailService() MailService {
	return MailService{}
}
func (w MailService) Send(u User) {
	fmt.Println(fmt.Sprintf("恭喜勇士%v,获得金币999", u.Name))
}

type UserService struct {
}
func NewUserService() UserService {
	return UserService{}
}
type User struct {
	Name string
}
func (u UserService) Register(name string) User {
	return User{Name: name}
}
func (m User) Start() {
	fmt.Printf("欢迎来到元宇宙")
}

type UserController struct {
	User     UserService
	WorldMap WorldMapService
	Mail     MailService
}

func NewUserController() UserController {
	user := NewUserService()
	worldMap := NewWorldMapService()
	mail := NewMailService()
	return UserController{
		User:     user,
		WorldMap: worldMap,
		Mail:     mail,
	}
}

func (c UserController) Do() {
	user := c.User.Register("好奇的小明")
	c.WorldMap.Join(user)
	c.Mail.Send(user)
	user.Start()
}

func main() {
	controller := NewUserController()
	controller.Do()
}

OutPut
MapService: 欢迎好奇的小明来到新手村
MailService: 恭喜勇士好奇的小明,获得金币999
UserService: 让我们开始愉快的旅程吧

UserController.Do 注册、增加出生点位、发送邮件,违反单一职责原则。如果模块越来越多,比如增加一个任务系统(登入过游戏赠送金币),坐骑模块(登入赠送初始坐骑)之类,那么这里的代码就会变得越来越长,不好拓展等。这时候,使用观察者模式,进行解耦。

package main

import "fmt"

type WorldMapService struct {
}

func NewWorldMapService() WorldMapService {
	return WorldMapService{}
}
func (w WorldMapService) HandleRegSuccess(u User) {
	fmt.Println(fmt.Sprintf("MapService: 欢迎%v来到新手村", u.Name))
}

type MailService struct {
}

func NewMailService() *MailService {
	return &MailService{}
}
func (w MailService) HandleRegSuccess(u User) {
	fmt.Println(fmt.Sprintf("MailService: 恭喜勇士%v,获得金币999", u.Name))
}

type UserService struct {
}

func NewUserService() UserService {
	return UserService{}
}

type User struct {
	Name string
}

func (u UserService) Register(name string) User {
	return User{Name: name}
}

func (m User) Start() {
	fmt.Printf("UserService: 让我们开始愉快的旅程吧")
}

type UserController struct {
	obsrvers []RegObserver
}

func NewUserController(obsrvers ...RegObserver) UserController {
	return UserController{obsrvers: obsrvers}
}

func (c UserController) Do() {
	userSvc := NewUserService()
	user := userSvc.Register("好奇的小明")
	for _, ob := range c.obsrvers {
		ob.HandleRegSuccess(user)
	}
	user.Start()
}

type RegObserver interface {
	HandleRegSuccess(User)
}

func main() {
	controller := NewUserController(NewWorldMapService(), NewMailService())
	controller.Do()
}

这里

  1. 定义了一个RegObserver接口,不同的服务都实现了这个接口。并注册到了userController,控制器保存了所有的观察者,在登入userController的执行函数Do里,会去遍历所有的观察者,执行HandleRegSuccess。

  2. 当拓展需求时,只需要再添加一个实现了RegObserver接口的类并注册到控制器即可。

如此,各模块间耦合性就降低了。

小结

本文主要介绍观察者模式和使用场景,观察者模式和发布订阅模式的思路是差不多的,主要是为了解耦。应用场景还是非常广泛的,在同一进程的编码上, 在进程间的消息队列,还是在产品的订阅模式都有异曲同工之处。

参考

jefffff

Stay hungry. Stay Foolish COOL

Go backend developer

China Amoy