赞
踩
设计模式是面向对象软件的设计经验,是解决特定问题的一系列套路。它不是语法规定,而是一套用来提高代码可复用性、可维护性、可读性、稳健性以及安全性的解决方案。每一种设计模式系统的命名、解释和评价了面向对象中一个重要的和重复出现的设计。
行为模式 主要关注对象之间的通信,有以下几种:
看过《如何说服女生上床》这部经典文章吗?女生从认识到上床的不变的步骤分为巧遇、打破僵局、展开追求、接吻、前戏、动手、爱抚、进去八大步骤 (Template method),但每个步骤针对不同的情况,都有不一样的做法,这就要看你随机应变啦 (具体实现);
模板模式:模板模式准备一个抽象类,将部分逻辑以具体方法以及具体构造子的形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法,从而对剩余的逻辑有不同的实现。先制定一个顶级逻辑框架,而将逻辑的细节留给具体的子类去实现。
定义一个模板结构,将具体内容延迟到子类去实现。模板控制流程,子类负责实现。
**模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。**TemplateMethod是算法骨架,PrimitiveMethod1和PrimitiveMethod2是骨架中的某些步骤。
在模板模式经典的实现中,模板方法定义为 final,可以避免被子类重写。需要子类重写的方法定义为 abstract,可以强迫子类去实现。
以前用这种定义好算法骨架,具体实现在不同子类的方案时,一般使用的是工厂方法加代理模式。工厂方法能够提供更多的灵活性,但如果一个算法骨架中有10个具体算法,总不能让工厂生产10个不同的对象吧。所以如果算法骨架中有多个具体算法,而这些算法又是高内聚的,用模板模式就很合适。
业务开发场景中,模板模式使用频率并不高,但是在框架方面,还是使用的比较频繁的。
每一个不同的实现都需要一个子类来实现,导致类的个数增加,使得系统更加庞大
假设我现在要做一个短信推送的系统,那么需要
我们可以发现,在发送短信的时候由于不同的供应商调用的接口不同,所以会有一些实现上的差异,但是他的算法(业务逻辑)是固定的
代码实现:
package template import "fmt" // ISMS ISMS type ISMS interface { send(content string, phone int) error } // SMS 短信发送基类 type sms struct { ISMS } // Valid 校验短信字数 func (s *sms) Valid(content string) error { if len(content) > 63 { return fmt.Errorf("content is too long") } return nil } // Send 发送短信 func (s *sms) Send(content string, phone int) error { if err := s.Valid(content); err != nil { return err } // 调用子类的方法发送短信 return s.send(content, phone) } // TelecomSms 走电信通道 type TelecomSms struct { *sms } // NewTelecomSms NewTelecomSms func NewTelecomSms() *TelecomSms { tel := &TelecomSms{ } // 这里有点绕,是因为 go 没有继承,用嵌套结构体的方法进行模拟 // 这里将子类作为接口嵌入父类,就可以让父类的模板方法 Send 调用到子类的函数 // 实际使用中,我们并不会这么写,都是采用组合+接口的方式完成类似的功能 tel.sms = &sms{ ISMS: tel} return tel } func (tel *TelecomSms) send(content string, phone int) error { fmt.Println("send by telecom success") return nil }
单元测试:
package template
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_sms_Send(t *testing.T) {
tel := NewTelecomSms()
err := tel.Send("test", 1239999)
assert.NoError(t, err)
}
模板模式有两大作用:复用和扩展。其中,复用指的是,所有的子类可以复用父类中提供的模板方法的代码。扩展指的是,框架通过模板模式提供功能扩展点,让框架用户可以在不修改框架源码的情况下,基于扩展点定制化框架的功能。
俺有一个 MM 家里管得特别严,没法见面,只好借助于她弟弟在我们俩之间传送信息,她对我有什么指示,就写一张纸条让她弟弟带给我。这不,她弟弟又传送过来一个 COMMAND,为了感谢他,我请他吃了碗杂酱面,哪知道他说:“我同时给我姐姐三个男朋友送 COMMAND,就数你最小气,才请我吃面。”
命令模式:命令模式把一个请求或者操作封装到一个对象中。命令模式把发出命令的责任和执行命令的责任分割开,委派给不同的对象。命令模式允许请求的一方和发送的一方独立开来,使得请求的一方不必知道接收请求的一方的接口,更不必知道请求是怎么被接收,以及操作是否执行,何时被执行以及是怎么被执行的。系统支持命令的撤消。
命令模式是一个高内聚的模式,将一个请求封装成一个对象,从而让你使用不同的请求把客户端参数化,对请求排队或者记录日志,可以提供命令的撤销和恢复功能。命令模式的核心在于引入了命令类,通过命令类来降低发送者和接收者的耦合度,请求发送者只需指定一个命令对象,再通过命令对象来调用请求接收者的处理方法。
首先我们需要明白什么是命令。命令包括指令和数据。指令是行为,数据影响到指令。如前进3米,前进是指令,3米是数据。
然后我们再看一下各个类的含义。
Command
和ConcreteCommand
是命令,有Excute
函数,代表要做的行为。
ConcreteCommand
调用Excute()
,最终调用Receiver
的Action
。这意味ConcreteCommand
只是一个容器,真正的操作逻辑在Receiver中。
Invoker
包含了所有Command
,控制Command
何时执行Excute()
。
现在我们将上图简化,把Invoker
、Receiver
去掉,看看是否容易理解了:
通过这个简洁版示意图,我们来看一下为什么要用命令模式:
为什么要将函数包装成对象呢?C、C++、Go支持函数指针,但并不是所有语言都有这种特性,这时命令模式就起作用了。而且即使语言支持函数指针,命令的数据部分怎么存放仍是一个问题。
所以简单理解,命令模式就是把请求打包成一个一个Command对象,存储起来,系统根据实际需求进行处理。
大家可能感觉命令模式与MQ、工厂模式一样,其实在细节上是有区别的:
如果有N个命令,那么Command子类就有N个,这个类将膨胀得非常大。
接下来会有两个例子,第一个是按照原文定义的方式,将函数封装成对象,第二个例子我们直接将函数作为参数传递。
假设现在有一个游戏服务,我们正在实现一个游戏后端,使用一个 goroutine 不断接收来自客户端请求的命令,并且将它放置到一个队列当中,然后我们在另外一个 goroutine 中来执行它
代码实现:
package command import "fmt" // ICommand 命令 type ICommand interface { Execute() error } // StartCommand 游戏开始运行 type StartCommand struct{ } // NewStartCommand NewStartCommand func NewStartCommand( /*正常情况下这里会有一些参数*/ ) *StartCommand { return &StartCommand{ } } // Execute Execute func (c *StartCommand) Execute() error { fmt.Println("game start") return nil } // ArchiveCommand 游戏存档 type ArchiveCommand struct{ } // NewArchiveCommand NewArchiveCommand func NewArchiveCommand( /*正常情况下这里会有一些参数*/ ) *ArchiveCommand { return &ArchiveCommand{ } } // Execute Execute func (c *ArchiveCommand) Execute() error { fmt.Println("game archive") return nil }
单元测试:
package command import ( "fmt" "testing" "time" ) func TestDemo(t *testing.T) { // 用于测试,模拟来自客户端的事件 eventChan := make(chan string) go func() { events := []string{ "start", "archive", "start", "archive", "start", "start"} for _, e := range events { eventChan <- e } }() defer close(eventChan) // 使用命令队列缓存命令 commands := make(chan ICommand, 1000) defer close(commands) go func() { for { // 从请求或者其他地方获取相关事件参数 event, ok := <-eventChan if !ok { return } var command ICommand switch event { case "start": command = NewStartCommand() case "archive": command = NewArchiveCommand() } // 将命令入队 commands <- command } }() for { select { case c := <-commands: c.Execute() case <-time.After(1 * time.Second): fmt.Println("timeout 1s") return } } }
假设现在有一个游戏服务,我们正在实现一个游戏后端,使用一个 goroutine 不断接收来自客户端请求的命令,并且将它放置到一个队列当中,然后我们在另外一个 goroutine 中来执行它
代码实现:
package command import "fmt" // Command 命令 type Command func() error // StartCommandFunc 返回一个 Command 命令 // 是因为正常情况下不会是这么简单的函数 // 一般都会有一些参数 func StartCommandFunc() Command { return func() error { fmt.Println("game start") return nil } } // ArchiveCommandFunc ArchiveCommandFunc func ArchiveCommandFunc() Command { return func() error { fmt.Println("game archive") return nil } }
单元测试:
package command import ( "fmt" "testing" "time" ) func TestDemoFunc(t *testing.T) { // 用于测试,模拟来自客户端的事件 eventChan := make(chan string) go func() { events := []string{ "start", "archive", "start", "archive", "start", "start"} for _, e := range events { eventChan <- e } }() defer close(eventChan) // 使用命令队列缓存命令 commands := make(chan Command, 1000) defer close(commands) go func() { for { // 从请求或者其他地方获取相关事件参数 event, ok := <-eventChan if !ok { return } var command Command switch event { case "start": command = StartCommandFunc() case "archive": command = ArchiveCommandFunc() } // 将命令入队 commands <- command } }() for { select { case c := <-commands: c() case <-time.After(1 * time.Second): fmt.Println("timeout 1s") return } } }
设计模式是为了解决现实中的问题,我们需要和具体场景相绑定。在解决问题的时候,采用的是不是标准的设计模式并不重要,模式只是手段,手段需要为达成目的服务。
我爱上了 Mary,不顾一切的向她求婚。Mary:“想要我跟你结婚,得答应我的条件” 我:“什么条件我都答应,你说吧” Mary:“我看上了那个一克拉的钻石” 我:“我买,我买,还有吗?” Mary:“我看上了湖边的那栋别墅” 我:“我买,我买,还有吗?” Mary:“我看上那辆法拉利跑车” 我脑袋嗡的一声,坐在椅子上,一咬牙:“我买,我买,还有吗?”
迭代模式:迭代模式可以顺序访问一个聚集中的元素而不必暴露聚集的内部表象。多个对象聚在一起形成的总体称之为聚集,聚集对象是能够包容一组对象的容器对象。迭代子模式将迭代逻辑封装到一个独立的子对象中,从而与聚集本身隔开。
迭代模式简化了聚集的界面。每一个聚集对象都可以有一个或一个以上的迭代子对象,每一个迭代子的迭代状态可以是彼此独立的。迭代算法可以独立于聚集角色变化。
迭代器模式提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部的表示。把游走的任务放在迭代器上,而不是聚合上。这样简化了聚合的接口和实现,也让责任各得其所。
角色:
分析:
通过上图可以看出,对于集合Aggregate,其遍历能力被拆了出来,由Iterator负责遍历。
大家可能有疑问,可以用for循环解决的问题,为啥要搞得这么复杂呢?
其实主要看集合结构的复杂性,如果是普通的数组,可以不需要Iterator,直接使用for循环即可。如果是复杂的集合呢?对于这个集合需要有多种遍历方案呢?
如对于图结构,有广度优先、深度优先两种遍历方式,都在图集合里实现,是否感觉违背了职责单一原则。
所以对于复杂结构,迭代器有如下优势:
通过上面示意图可发现设计思路:迭代器中需要定义first()、isDone()、currentItem()、next() 四个最基本的方法。待遍历的集合对象通过依赖注入传递到迭代器类中。集合可通过CreateIterator() 方法来创建迭代器。
迭代器模式一般在library中使用的比较多,毕竟library提供的大多是基础结构。实际业务场景中,很少需要自己编写迭代器。但代码还是要写的,这次写图集合与深度优先遍历迭代器,大家如果对其它类型的图迭代器感兴趣的话,可自行编写。
由于迭代器模式将存储数据和遍历数据的职责分离,增加新的聚合类需要对应增加新的迭代器类,类的个数成对增加,这在一定程度上增加了系统的复杂性。
下面是一个简单的自定义数组类型的例子
代码实现:
package iterator // Iterator 迭代器接口 type Iterator interface { HasNext() bool Next() // 获取当前元素,由于 Go 1.15 中还没有泛型,所以我们直接返回 interface{} CurrentItem() interface{ } } // ArrayInt 数组 type ArrayInt []int // Iterator 返回迭代器 func (a ArrayInt) Iterator() Iterator { return &ArrayIntIterator{ arrayInt: a, index: 0, } } // ArrayIntIterator 数组迭代 type ArrayIntIterator struct { arrayInt ArrayInt index int } // HasNext 是否有下一个 func (iter *ArrayIntIterator) HasNext() bool { return iter.index < len(iter.arrayInt)-1 } // Next 游标加一 func (iter *ArrayIntIterator) Next() { iter.index++ } // CurrentItem 获取当前元素 func (iter *ArrayIntIterator) CurrentItem() interface{ } { return iter.arrayInt[iter.index] }
单元测试ÿ
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。