行为型设计模式

行为型设计模式 #

策略模式 #

介绍 #

策略模式可以让开发者定义一系列的算法,并且将每种算法分别放入独立的类,从而使算法的对象能够相互替换。策略模式可以将一组行为转换为对象,并且使其在原始对象内部能够相互替换。策略模式可以将一组行为转换为对象,并且使其在原始对象内部能够相互替换。原始对象称为上下文,包含指向策略对象的引用并将执行行为的任务分派给策略对象。为了改变上下文完成其工作的方式,其他对象可以使用另一个对象替换当前链接的策略对象。

  • 当开发者需要使用对象中各种不同算法的变体,并且希望能在运行时切换算法时,可以使用策略模式。策略模式让开发者能够将对象关联至能以不同方式执行特定子任务的不同子对象,从而以间接方式在运行时更改对象行为。
  • 当开发者有许多仅在执行某些行为时略有不同的相似类时,可以使用策略模式。策略模式让开发者能够将不同的行为抽取到一个独立类层次结构中,并且将原始类组合成同一个类,从而减少重复代码。
  • 如果算法在上下文的逻辑中不是特别重要,那么使用策略模式可以将类的业务逻辑与算法实现细节分开。
  • 如果类中使用了复杂条件运算符,用于在同一个算法的不同变体中切换,则可以使用策略模式。策略模式将所有继承自同一个接口的算法抽取到独立类中,因此不需要条件语句。

(1)从上下文类中找出修改频率较高的算法,定义该算法所有变体的通用策略接口

//策略接口
type Strategy interface {
	Execute()
}

(2)定义具体策略类及其方法

// 具体策略 A
type strategyA struct {
}

// 具体策略 A 的方法
func (s *strategyA) Execute() {
	fmt.Println("执行策略 A")
}

// 具体策略B
type strategyB struct {
}

// 具体策略B的方法
func (s *strategyB) Execute() {
	fmt.Println("执行策略 B")
}

// 创建策略 A 的新对象
func NewStrategyA() Strategy {
	return &strategyA{}
}

// 创建策略 B 的新对象
func NewStrategyB() Strategy {
	return &strategyB{}
}

(3)定义上下文类及其方法

// 上下文
type Context struct {
	strategy Strategy
}

// 设置上下文执行的策略
func (c *Context) SetStrategy(strategy Strategy) {
	c.strategy = strategy
}

// 上下文的方法
func (c *Context) Execute() {
	c.strategy.Execute()
}

// 创建一个新的上下文对象
func NewContext() *Context {
	return &Context{}
}
func main() {
	strategyB := NewStrategyB()
	context := NewContext()
	context.SetStrategy(strategyB)
	strategyA := NewStrategyA()
	context.SetStrategy(strategyA)
	context.Execute()
}

优点 #

  • 策略模式的部分算法可以重用。策略接口的层次结构定义了一系列算法或行为,以供上下文重用。
  • 子类化的代替方法。继承支持各种算法或行为的另一种方式。
  • 策略模式消除了条件语句。策略模式为选择所需算法或行为提供了条件语句的替代方案。在将不同的算法或行为归为一类时,很难避免使用条件语句选择正确的算法或行为。
  • 策略模式可以提供相同行为的不同实现。客户端可以选择具有不同时间和空间权衡的策略。
  • 策略模式符合开闭区间原则。开发者无须对上下文进行修改,就能够引入新的策略。

缺点 #

  • 如果开发者的算法极少发生改变,则没有任何理由引入新的类或接口。使用策略模式会让程序变得复杂。
  • 只有当行为变化与客户端相关时,才应该使用策略模式。
  • 策略模式会使对象数量增加。策略模式会增加应用程序中的对象数量。

示例 #

使用go语言构建内存缓存。由于缓存位于内存中,因此其大小会存在限制,在达到一定上限后,必须将一些条目移除,以便留出内存空间。此类操作可以通过多种算法实现:

  • 最少最近使用
  • 先进先出
  • 最少使用

我们需要解决的问题时如何将缓存类与这些算法解耦,以便在运行时更改算法。此外,在添加新算法时,缓存类不应该发生改变。

(1)定义策略接口

type AlgorithmType interface {
	Delete(c *Cache)
}

(2)定义具体策略类

// FIFO算法类型
type Fifo struct {
}

// 删除缓存
func (l *Fifo) Delete(c *Cache) {
	fmt.Println("Deleting by fifo strategy")
}
// LRU算法类型
type Lru struct {
}

// 删除缓存
func (l *Lru) Delete(c *Cache) {
	fmt.Println("Deleting by lru strategy")
}
// LFU算法类型
type Lfu struct {
}

// 删除缓存
func (l *Lfu) Delete(c *Cache) {
	fmt.Println("Deleting by lfu strategy")
}

(3)定义上下文类(缓存类)及其方法

type Cache struct {
	storage       map[string]string
	AlgorithmType AlgorithmType
	capacity      int
	maxCapacity   int
}

func InitCache(e AlgorithmType) *Cache {
	storage := make(map[string]string)
	return &Cache{
		storage:       storage,
		AlgorithmType: e,
		capacity:      0,
		maxCapacity:   2,
	}
}

func (c *Cache) SetAlgorithmType(e AlgorithmType) {
	c.AlgorithmType = e
}

func (c *Cache) Add(key, value string) {
	if c.capacity == c.maxCapacity {
		c.Delete()
	}
	c.capacity++
	c.storage[key] = value
}

func (c *Cache) Get(key string) {
	delete(c.storage, key)
}

func (c *Cache) Delete() {
	c.AlgorithmType.Delete(c)
	c.capacity--
}

(4)客户端

func main() {
	//声明Lfu对象
	lfu := &Lfu{}
	//初始化缓存对象
	cache := InitCache(lfu)

	//添加缓存
	cache.Add("one", "1")
	cache.Add("two", "2")

	cache.Add("three", "3")

	//声明Lru对象
	lru := &Lru{}
	//设置lru算法类型
	cache.SetAlgorithmType(lru)

	//添加缓存
	cache.Add("four", "4")

	//声明Fifo对象
	fifo := &Fifo{}
	//设置Fifo算法类型
	cache.SetAlgorithmType(fifo)

	//添加缓存
	cache.Add("five", "5")
}

责任链模式 #

介绍 #

责任链模式允许开发者请求沿着链进行发送,直至其中一个处理者对象对其进行处理。责任链模式可以根据请求的类型将请求的发送者和接收者解耦。当有请求发生时,可以将请求沿着这条链传递,知道有处理者对象处理它为止。

责任链模式允许多个处理者对象对请求进行处理,无须让发送者类与具体接收者类相耦合。

  • 如果程序需要使用不同的方式处理不同种类的请求,并且请求类型和顺序预先未知,则可以使用责任链模式。
  • 如果必须按顺序执行多个具体处理者对象,则可以使用责任链模式。
  • 如果所需的具体处理对象及其顺序必须在运行时发生改变,则可以使用责任链模式。如果在具体处理者类中有成员变量的引用,那么开发者可以动态的插入和移除具体处理者对象或改变其顺序。

(1)定义处理者接口及其处理方法,确定客户端如何将请求传递给方法。最灵活的方式是将请求转换为对象,然后将其以参数的形式传递给处理函数。

// Handler 定义了一个处理程序来处理给定的 handleID
type Handler interface {
	SetNext(handler Handler)
	Handle(handleID int) int
}

(2)定义基础处理者类及其方法

// 基础处理者
type BaseHandler struct {
	name     string
	next     Handler
	handleID int
}

// NewHandler 返回一个新的处理程序
func NewBaseHandler(name string, next Handler, handleID int) Handler {
	return &BaseHandler{name, next, handleID}
}

// Handle 处理给定的 handleID
func (h *BaseHandler) Handle(handleID int) int {
	if handleID < 4 {
		ch := &ConcreteHandler{}
		ch.Handle(handleID)
		fmt.Println(h.name)

		if h.next != nil {
			h.next.Handle(handleID + 1)
		}

		return handleID + 1
	}
	return 0
}

// 设置下一个处理者
func (h *BaseHandler) SetNext(handler Handler) {
	h.next = handler
}

(3)定义具体处理者类及其方法

// 具体处理者
type ConcreteHandler struct {
}

// 具体处理者的处理方法
func (ch *ConcreteHandler) Handle(handleID int) {
	fmt.Println("ConcreteHandler handleID:", handleID)
}

(4)客户端

func main() {
	barry := NewBaseHandler("Barry", nil, 1)
	shirdon := NewBaseHandler("Shirdon", barry, 2)
	jack := NewBaseHandler("Shirdon", shirdon, 3)
	res := shirdon.Handle(2)
	res1 := jack.Handle(3)
	fmt.Println(res)
	fmt.Println(res1)
}

优点 #

缺点 #

示例 #

命令模式 #

介绍 #

优点 #

缺点 #

示例 #

迭代器模式 #

介绍 #

优点 #

缺点 #

示例 #

中介者模式 #

介绍 #

优点 #

缺点 #

示例 #

备忘录模式 #

介绍 #

优点 #

缺点 #

示例 #

观察者模式 #

介绍 #

优点 #

缺点 #

示例 #

状态模式 #

介绍 #

优点 #

缺点 #

示例 #

模板方法模式 #

介绍 #

模板方法模式可以在基类中定义一个算法的框架,允许子类在不修改框架结构的情况下重写算法的特定步骤。

  • 如果开发者只希望客户端扩展某个特定的算法步骤,而不是整个算法或其结构,则可以使用模板方法模式。模板方法模式可以将算法转换为一系列独立的步骤,以便子类可以对其进行扩展,并且使父类中定义的结构保持完整。
  • 如果多个类的算法几乎完全相同,则可以使用模板方法模式。但其后果是,如果算法发生变化,那么开发者可能需要修改所有的类。在将算法转换为一系列独立的步骤时,开发者可以将相似的步骤提取到父类中,从而去除重复的代码。子类之间各不相同的代码可以继续保留在子类中。

(1)分析目标算法,确定能否将其分解为多个步骤

(2)定义抽象类接口,然后定义抽象类及其方法。

// 抽象类接口
type AbstractClassInterface interface {
	Step1()
	Step2()
	Step3()
}

// 抽象类
type AbstractClass struct {
	AbstractClassInterface
}

// 初始化抽象类对象
func NewAbstractClass(aci AbstractClassInterface) *AbstractClass {
	return &AbstractClass{aci}
}

// 模版方法
func (cc *AbstractClass) TemplateMethod() {
	cc.Step1()
	cc.Step2()
	cc.Step3()
}

(3)为每个算法变体都新建一个具体类,该类必须实现所有的抽象步骤,也可以重写部分可选步骤

// 具体类A
type ConcreteClassA struct {
}

// 具体类A的方法1
func (cc *ConcreteClassA) Step1() {
	fmt.Println("ConcreteClassA Step1")
}

// 具体类A的方法2
func (cc *ConcreteClassA) Step2() {
	fmt.Println("ConcreteClassA Step2")
}

// 具体类A的方法3
func (cc *ConcreteClassA) Step3() {
	fmt.Println("ConcreteClassA Step3")
}

// 具体类B
type ConcreteClassB struct {
}

// 具体类B的方法1
func (cc *ConcreteClassB) Step1() {
	fmt.Println("ConcreteClassB Step1")
}

// 具体类B的方法2
func (cc *ConcreteClassB) Step2() {
	fmt.Println("ConcreteClassB Step2")
}

// 具体类B的方法3
func (cc *ConcreteClassB) Step3() {
	fmt.Println("ConcreteClassB Step3")
}
func main() {
	concreteClassA := NewAbstractClass(&ConcreteClassA{})
	concreteClassA.TemplateMethod()
	concreteClassB := NewAbstractClass(&ConcreteClassB{})
	concreteClassB.TemplateMethod()
}

优点 #

  • 开发者可以只允许客户端重写一个大型算法中的特定步分,使算法其他部分的修改对其造成的影响减少。
  • 开发者可以将重复的代码提取到父类中
  • 可以减少代码重复
  • 代码重用发生在模板方法模式中,因为它使用继承而不是组合。只有少数方法需要被覆盖。
  • 模板方法模式的灵活性可以让子类决定如何在算法中实现步骤。

缺点 #

  • 部分客户端可能会受到算法框架的限制
  • 通过子类抑制默认步骤实现可能会违反里氏代换原则:子类可以扩展父类的功能,但不能改变父类原有的功能。
  • 模板方法模式中的步骤越多,其维护工作可能越空难。

示例 #

使用模板模式实现一次性密码功能,将一次性密码传递给用户的方式有很多种,如短信、邮件,但无论那种方式,实现一次性密码的流程都是相同的。

  • 生成随机的n位数字
  • 在缓存中存储这组数字,以便进行后续验证。
  • 准备工作
  • 发送通知
  • 发布

(1)定义模板方法的一次性密码接口IOtp、一次性密码类Otp及其方法

// 定义一次性密码接口
type IOtp interface {
	GenRandomOTP(int) string
	SaveOTPCache(string)
	GetMessage(string) string
	SendNotification(string) error
	Publish()
}

// 定义一次性密码类
type Otp struct {
	IOtp IOtp
}

// 生成验证码并发送
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
	}
	o.IOtp.Publish()
	return nil
}

(2)具体实施

// 短信类
type Sms struct {
	Otp   //这里可以注销,定义自己想要的结构
}

func (s *Sms) GenRandomOTP(len int) string {
	randomOTP := "1688"
	fmt.Printf("SMS: 生成随机验证码:%s\n", randomOTP)
	return randomOTP
}

func (s *Sms) SaveOTPCache(otp string) {
	fmt.Printf("SMS: 保存验证码:%s 到缓存\n", otp)
}

func (s *Sms) GetMessage(otp string) string {
	return "登录的短信验证码是:" + otp
}

func (s *Sms) SendNotification(message string) error {
	fmt.Printf("SMS: 发送消息:%s\n", message)
	return nil
}

func (s *Sms) Publish() {
	fmt.Printf("SMS: 发布完成\n")
}
// 邮箱类
type Email struct {
	Otp  //这里可以注销,定义自己想要的结构
}

func (s *Email) GenRandomOTP(len int) string {
	randomOTP := "3699"
	fmt.Printf("EMAIL: 生成随机验证码:%s\n", randomOTP)
	return randomOTP
}

func (s *Email) SaveOTPCache(otp string) {
	fmt.Printf("EMAIL: 保存验证码:%s 到缓存\n", otp)
}

func (s *Email) GetMessage(otp string) string {
	return "登录的短信验证码是:" + otp
}

func (s *Email) SendNotification(message string) error {
	fmt.Printf("EMAIL: 发送消息:%s\n", message)
	return nil
}

func (s *Email) Publish() {
	fmt.Printf("EMAIL:发布完成\n")
}

(3)客户端

func main() {
	//创建短信对象
	smsOTP := &Sms{}
	o := Otp{
		IOtp: smsOTP,   //结构体给接口
	}
	//生成短信验证码并发送
	o.GenAndSendOTP(4)  //调这个结构体的方法,这个方法里面就是模板,按这个模板走

	fmt.Println("")
	//创建邮件对象
	EmailOTP := &Email{}
	o = Otp{
		IOtp: EmailOTP,
	}
	//生成邮件验证码并发送
	o.GenAndSendOTP(4)
}
SMS: 生成随机验证码:1688
SMS: 保存验证码:1688 到缓存
SMS: 发送消息:登录的短信验证码是:1688
SMS: 发布完成

EMAIL: 生成随机验证码:3699
EMAIL: 保存验证码:3699 到缓存
EMAIL: 发送消息:登录的短信验证码是:3699
EMAIL:发布完成

访问者模式 #

介绍 #

优点 #

缺点 #

示例 #