面开发被问麻了,记录一下
按照三种类型分类
创建型模式(Creational Pattern)、结构型模式(Structural Pattern)和行为型模式(Behavioral Pattern)
然后主要问基本都是问创建型模式,实际上我就用过工厂模式。。记点理论开始扯皮就行了
单例模式(Singleton Pattern)
简述
单例模式算是23中设计模式里最简单的一个了,它主要用于保证一个类仅有一个实例,并提供一个访问它的全局访问点。
在程序设计中,有一些对象通常我们只需要一个共享的实例,比如线程池、全局缓存、对象池等,这种场景下就适合使用单例模式。
但是,并非所有全局唯一的场景都适合使用单例模式。比如,考虑需要统计一个API调用的情况,有两个指标,成功调用次数和失败调用次数。这两个指标都是全局唯一的,所以有人可能会将其建模成两个单例SuccessApiMetric和FailApiMetric。按照这个思路,随着指标数量的增多,你会发现代码里类的定义会越来越多,也越来越臃肿。这也是单例模式最常见的误用场景,更好的方法是将两个指标设计成一个对象ApiMetric下的两个实例ApiMetic success和ApiMetic fail。
如何判断一个对象是否应该被建模成单例?
通常,被建模成单例的对象都有“中心点”的含义,比如线程池就是管理所有线程的中心。所以,在判断一个对象是否适合单例模式时,先思考下,这个对象是一个中心点吗?
Go实现
在对某个对象实现单例模式时,有两个点必须要注意:(1)限制调用者直接实例化该对象;(2)为该对象的单例提供一个全局唯一的访问方法。
对于C++/Java而言,只需把类的构造函数设计成私有的,并提供一个static方法去访问该类点唯一实例即可。但对于Go语言来说,即没有构造函数的概念,也没有static方法,所以需要另寻出路。
我们可以利用Go语言package的访问规则来实现,将单例结构体设计成首字母小写,就能限定其访问范围只在当前package下,模拟了C++/Java中的私有构造函数;再在当前package下实现一个首字母大写的访问函数,就相当于static方法的作用了。
在实际开发中,我们经常会遇到需要频繁创建和销毁的对象。频繁的创建和销毁一则消耗CPU,二则内存的利用率也不高,通常我们都会使用对象池技术来进行优化。考虑我们需要实现一个消息对象池,因为是全局的中心点,管理所有的Message实例,所以将其实现成单例,实现代码如下:
package msgpool
...
// 消息池
type messagePool struct {
pool *sync.Pool
}
// 消息池单例
var msgPool = &messagePool{
// 如果消息池里没有消息,则新建一个Count值为0的Message实例
pool: &sync.Pool{New: func() interface{} { return &Message{Count: 0} }},
}
// 访问消息池单例的唯一方法
func Instance() *messagePool {
return msgPool
}
// 往消息池里添加消息
func (m *messagePool) AddMsg(msg *Message) {
m.pool.Put(msg)
}
// 从消息池里获取消息
func (m *messagePool) GetMsg() *Message {
return m.pool.Get().(*Message)
}
...
测试代码如下:
package test
...
func TestMessagePool(t *testing.T) {
msg0 := msgpool.Instance().GetMsg()
if msg0.Count != 0 {
t.Errorf("expect msg count %d, but actual %d.", 0, msg0.Count)
}
msg0.Count = 1
msgpool.Instance().AddMsg(msg0)
msg1 := msgpool.Instance().GetMsg()
if msg1.Count != 1 {
t.Errorf("expect msg count %d, but actual %d.", 1, msg1.Count)
}
}
// 运行结果
=== RUN TestMessagePool
--- PASS: TestMessagePool (0.00s)
PASS
以上的单例模式就是典型的“饿汉模式”,实例在系统加载的时候就已经完成了初始化。对应地,还有一种“懒汉模式”,只有等到对象被使用的时候,才会去初始化它,从而一定程度上节省了内存。众所周知,“懒汉模式”会带来线程安全问题,可以通过普通加锁,或者更高效的双重检验锁来优化。对于“懒汉模式”,Go语言有一个更优雅的实现方式,那就是利用sync.Once,它有一个Do方法,其入参是一个方法,Go语言会保证仅仅只调用一次该方法。
// 单例模式的“懒汉模式”实现
package msgpool
...
var once = &sync.Once{}
// 消息池单例,在首次调用时初始化
var msgPool *messagePool
// 全局唯一获取消息池pool到方法
func Instance() *messagePool {
// 在匿名函数中实现初始化逻辑,Go语言保证只会调用一次
once.Do(func() {
msgPool = &messagePool{
// 如果消息池里没有消息,则新建一个Count值为0的Message实例
pool: &sync.Pool{New: func() interface{} { return &Message{Count: 0} }},
}
})
return msgPool
}
...
建造者模式(Builder Pattern)
简述
在程序设计中,我们会经常遇到一些复杂的对象,其中有很多成员属性,甚至嵌套着多个复杂的对象。这种情况下,创建这个复杂对象就会变得很繁琐。对于C++/Java而言,最常见的表现就是构造函数有着长长的参数列表:
MyObject obj = new MyObject(param1, param2, param3, param4, param5, param6, ...)
而对于Go语言来说,最常见的表现就是多层的嵌套实例化:
obj := &MyObject{
Field1: &Field1 {
Param1: &Param1 {
Val: 0,
},
Param2: &Param2 {
Val: 1,
},
...
},
Field2: &Field2 {
Param3: &Param3 {
Val: 2,
},
...
},
...
}
上述的对象创建方法有两个明显的缺点:(1)对对象使用者不友好,使用者在创建对象时需要知道的细节太多;(2)代码可读性很差。
针对这种对象成员较多,创建对象逻辑较为繁琐的场景,就适合使用建造者模式来进行优化。
建造者模式的作用有如下几个:
1、封装复杂对象的创建过程,使对象使用者不感知复杂的创建逻辑。
2、可以一步步按照顺序对成员进行赋值,或者创建嵌套对象,并最终完成目标对象的创建。
3、对多个对象复用同样的对象创建逻辑。
其中,第1和第2点比较常用,下面对建造者模式的实现也主要是针对这两点进行示例。
Go实现
考虑如下的一个Message结构体,其主要有Header和Body组成:
package msg
...
type Message struct {
Header *Header
Body *Body
}
type Header struct {
SrcAddr string
SrcPort uint64
DestAddr string
DestPort uint64
Items map[string]string
}
type Body struct {
Items []string
}
...
如果按照直接的对象创建方式,创建逻辑应该是这样的:
// 多层的嵌套实例化
message := msg.Message{
Header: &msg.Header{
SrcAddr: "192.168.0.1",
SrcPort: 1234,
DestAddr: "192.168.0.2",
DestPort: 8080,
Items: make(map[string]string),
},
Body: &msg.Body{
Items: make([]string, 0),
},
}
// 需要知道对象的实现细节
message.Header.Items["contents"] = "application/json"
message.Body.Items = append(message.Body.Items, "record1")
message.Body.Items = append(message.Body.Items, "record2")
虽然Message结构体嵌套的层次不多,但是从其创建的代码来看,确实存在对对象使用者不友好和代码可读性差的缺点。下面我们引入建造者模式对代码进行重构:
package msg
...
// Message对象的Builder对象
type builder struct {
once *sync.Once
msg *Message
}
// 返回Builder对象
func Builder() *builder {
return &builder{
once: &sync.Once{},
msg: &Message{Header: &Header{}, Body: &Body{}},
}
}
// 以下是对Message成员对构建方法
func (b *builder) WithSrcAddr(srcAddr string) *builder {
b.msg.Header.SrcAddr = srcAddr
return b
}
func (b *builder) WithSrcPort(srcPort uint64) *builder {
b.msg.Header.SrcPort = srcPort
return b
}
func (b *builder) WithDestAddr(destAddr string) *builder {
b.msg.Header.DestAddr = destAddr
return b
}
func (b *builder) WithDestPort(destPort uint64) *builder {
b.msg.Header.DestPort = destPort
return b
}
func (b *builder) WithHeaderItem(key, value string) *builder {
// 保证map只初始化一次
b.once.Do(func() {
b.msg.Header.Items = make(map[string]string)
})
b.msg.Header.Items[key] = value
return b
}
func (b *builder) WithBodyItem(record string) *builder {
b.msg.Body.Items = append(b.msg.Body.Items, record)
return b
}
// 创建Message对象,在最后一步调用
func (b *builder) Build() *Message {
return b.msg
}
测试代码如下:
package test
...
func TestMessageBuilder(t *testing.T) {
// 使用消息建造者进行对象创建
message := msg.Builder().
WithSrcAddr("192.168.0.1").
WithSrcPort(1234).
WithDestAddr("192.168.0.2").
WithDestPort(8080).
WithHeaderItem("contents", "application/json").
WithBodyItem("record1").
WithBodyItem("record2").
Build()
if message.Header.SrcAddr != "192.168.0.1" {
t.Errorf("expect src address 192.168.0.1, but actual %s.", message.Header.SrcAddr)
}
if message.Body.Items[0] != "record1" {
t.Errorf("expect body item0 record1, but actual %s.", message.Body.Items[0])
}
}
// 运行结果
=== RUN TestMessageBuilder
--- PASS: TestMessageBuilder (0.00s)
PASS
从测试代码可知,使用建造者模式来进行对象创建,使用者不再需要知道对象具体的实现细节,代码可读性也更好。
工厂方法模式(Factory Method Pattern)
简述
工厂方法模式跟上一节讨论的建造者模式类似,都是将对象创建的逻辑封装起来,为使用者提供一个简单易用的对象创建接口。两者在应用场景上稍有区别,建造者模式更常用于需要传递多个参数来进行实例化的场景。
使用工厂方法来创建对象主要有两个好处:
1、代码可读性更好。相比于使用C++/Java中的构造函数,或者Go中的{}来创建对象,工厂方法因为可以通过函数名来表达代码含义,从而具备更好的可读性。比如,使用工厂方法productA := CreateProductA()创建一个ProductA对象,比直接使用productA := ProductA{}的可读性要好。
2、与使用者代码解耦。很多情况下,对象的创建往往是一个容易变化的点,通过工厂方法来封装对象的创建过程,可以在创建逻辑变更时,避免霰弹式修改。
工厂方法模式也有两种实现方式:(1)提供一个工厂对象,通过调用工厂对象的工厂方法来创建产品对象;(2)将工厂方法集成到产品对象中(C++/Java中对象的static方法,Go中同一package下的函数)
Go实现
考虑有一个事件对象Event,分别有两种有效的时间类型Start和End:
package event
...
type Type uint8
// 事件类型定义
const (
Start Type = iota
End
)
// 事件抽象接口
type Event interface {
EventType() Type
Content() string
}
// 开始事件,实现了Event接口
type StartEvent struct{
content string
}
...
// 结束事件,实现了Event接口
type EndEvent struct{
content string
}
...
1、按照第一种实现方式,为Event提供一个工厂对象,具体代码如下:
package event
...
// 事件工厂对象
type Factory struct{}
// 更具事件类型创建具体事件
func (e *Factory) Create(etype Type) Event {
switch etype {
case Start:
return &StartEvent{
content: "this is start event",
}
case End:
return &EndEvent{
content: "this is end event",
}
default:
return nil
}
}
测试代码如下:
package test
...
func TestEventFactory(t *testing.T) {
factory := event.Factory{}
e := factory.Create(event.Start)
if e.EventType() != event.Start {
t.Errorf("expect event.Start, but actual %v.", e.EventType())
}
e = factory.Create(event.End)
if e.EventType() != event.End {
t.Errorf("expect event.End, but actual %v.", e.EventType())
}
}
// 运行结果
=== RUN TestEventFactory
--- PASS: TestEventFactory (0.00s)
PASS
2、按照第二种实现方式,分别给Start和End类型的Event单独提供一个工厂方法,代码如下:
package event
...
// Start类型Event的工厂方法
func OfStart() Event {
return &StartEvent{
content: "this is start event",
}
}
// End类型Event的工厂方法
func OfEnd() Event {
return &EndEvent{
content: "this is end event",
}
}
测试代码如下:
package event
...
func TestEvent(t *testing.T) {
e := event.OfStart()
if e.EventType() != event.Start {
t.Errorf("expect event.Start, but actual %v.", e.EventType())
}
e = event.OfEnd()
if e.EventType() != event.End {
t.Errorf("expect event.End, but actual %v.", e.EventType())
}
}
// 运行结果
=== RUN TestEvent
--- PASS: TestEvent (0.00s)
PASS
抽象工厂模式(Abstract Factory Pattern)
简述
在工厂方法模式中,我们通过一个工厂对象来创建一个产品族,具体创建哪个产品,则通过swtich-case的方式去判断。这也意味着该产品组上,每新增一类产品对象,都必须修改原来工厂对象的代码;而且随着产品的不断增多,工厂对象的职责也越来越重,违反了单一职责原则。
抽象工厂模式通过给工厂类新增一个抽象层解决了该问题,如上图所示,FactoryA和FactoryB都实现·抽象工厂接口,分别用于创建ProductA和ProductB。如果后续新增了ProductC,只需新增一个FactoryC即可,无需修改原有的代码;因为每个工厂只负责创建一个产品,因此也遵循了单一职责原则。
Go实现
考虑需要如下一个插件架构风格的消息处理系统,pipeline是消息处理的管道,其中包含了input、filter和output三个插件。我们需要实现根据配置来创建pipeline ,加载插件过程的实现非常适合使用工厂模式,其中input、filter和output三类插件的创建使用抽象工厂模式,而pipeline的创建则使用工厂方法模式。
各类插件和pipeline的接口定义如下:
package plugin
...
// 插件抽象接口定义
type Plugin interface {}
// 输入插件,用于接收消息
type Input interface {
Plugin
Receive() string
}
// 过滤插件,用于处理消息
type Filter interface {
Plugin
Process(msg string) string
}
// 输出插件,用于发送消息
type Output interface {
Plugin
Send(msg string)
}
package pipeline
...
// 消息管道的定义
type Pipeline struct {
input plugin.Input
filter plugin.Filter
output plugin.Output
}
// 一个消息的处理流程为 input -> filter -> output
func (p *Pipeline) Exec() {
msg := p.input.Receive()
msg = p.filter.Process(msg)
p.output.Send(msg)
}
接着,我们定义input、filter、output三类插件接口的具体实现:
package plugin
...
// input插件名称与类型的映射关系,主要用于通过反射创建input对象
var inputNames = make(map[string]reflect.Type)
// Hello input插件,接收“Hello World”消息
type HelloInput struct {}
func (h *HelloInput) Receive() string {
return "Hello World"
}
// 初始化input插件映射关系表
func init() {
inputNames["hello"] = reflect.TypeOf(HelloInput{})
}
package plugin
...
// filter插件名称与类型的映射关系,主要用于通过反射创建filter对象
var filterNames = make(map[string]reflect.Type)
// Upper filter插件,将消息全部字母转成大写
type UpperFilter struct {}
func (u *UpperFilter) Process(msg string) string {
return strings.ToUpper(msg)
}
// 初始化filter插件映射关系表
func init() {
filterNames["upper"] = reflect.TypeOf(UpperFilter{})
}
package plugin
...
// output插件名称与类型的映射关系,主要用于通过反射创建output对象
var outputNames = make(map[string]reflect.Type)
// Console output插件,将消息输出到控制台上
type ConsoleOutput struct {}
func (c *ConsoleOutput) Send(msg string) {
fmt.Println(msg)
}
// 初始化output插件映射关系表
func init() {
outputNames["console"] = reflect.TypeOf(ConsoleOutput{})
}
然后,我们定义插件抽象工厂接口,以及对应插件的工厂实现:
package plugin
...
// 插件抽象工厂接口
type Factory interface {
Create(conf Config) Plugin
}
// input插件工厂对象,实现Factory接口
type InputFactory struct{}
// 读取配置,通过反射机制进行对象实例化
func (i *InputFactory) Create(conf Config) Plugin {
t, _ := inputNames[conf.Name]
return reflect.New(t).Interface().(Plugin)
}
// filter和output插件工厂实现类似
type FilterFactory struct{}
func (f *FilterFactory) Create(conf Config) Plugin {
t, _ := filterNames[conf.Name]
return reflect.New(t).Interface().(Plugin)
}
type OutputFactory struct{}
func (o *OutputFactory) Create(conf Config) Plugin {
t, _ := outputNames[conf.Name]
return reflect.New(t).Interface().(Plugin)
}
最后定义pipeline的工厂方法,调用plugin.Factory抽象工厂完成pipelien对象的实例化:
package pipeline
...
// 保存用于创建Plugin的工厂实例,其中map的key为插件类型,value为抽象工厂接口
var pluginFactories = make(map[plugin.Type]plugin.Factory)
// 根据plugin.Type返回对应Plugin类型的工厂实例
func factoryOf(t plugin.Type) plugin.Factory {
factory, _ := pluginFactories[t]
return factory
}
// pipeline工厂方法,根据配置创建一个Pipeline实例
func Of(conf Config) *Pipeline {
p := &Pipeline{}
p.input = factoryOf(plugin.InputType).Create(conf.Input).(plugin.Input)
p.filter = factoryOf(plugin.FilterType).Create(conf.Filter).(plugin.Filter)
p.output = factoryOf(plugin.OutputType).Create(conf.Output).(plugin.Output)
return p
}
// 初始化插件工厂对象
func init() {
pluginFactories[plugin.InputType] = &plugin.InputFactory{}
pluginFactories[plugin.FilterType] = &plugin.FilterFactory{}
pluginFactories[plugin.OutputType] = &plugin.OutputFactory{}
}
测试代码如下:
package test
...
func TestPipeline(t *testing.T) {
// 其中pipeline.DefaultConfig()的配置内容见【抽象工厂模式示例图】
// 消息处理流程为 HelloInput -> UpperFilter -> ConsoleOutput
p := pipeline.Of(pipeline.DefaultConfig())
p.Exec()
}
// 运行结果
=== RUN TestPipeline
HELLO WORLD
--- PASS: TestPipeline (0.00s)
PASS
原型模式(Prototype Pattern)
简述
原型模式主要解决对象复制的问题,它的核心就是clone()方法,返回Prototype对象的复制品。在程序设计过程中,往往会遇到有一些场景需要大量相同的对象,如果不使用原型模式,那么我们可能会这样进行对象的创建:新创建一个相同对象的实例,然后遍历原始对象的所有成员变量, 并将成员变量值复制到新对象中。这种方法的缺点很明显,那就是使用者必须知道对象的实现细节,导致代码之间的耦合。另外,对象很有可能存在除了对象本身以外不可见的变量,这种情况下该方法就行不通了。
对于这种情况,更好的方法就是使用原型模式,将复制逻辑委托给对象本身,这样,上述两个问题也都迎刃而解了。
Go实现
还是以建造者模式一节中的Message作为例子,现在设计一个Prototype抽象接口:
package prototype
...
// 原型复制抽象接口
type Prototype interface {
clone() Prototype
}
type Message struct {
Header *Header
Body *Body
}
func (m *Message) clone() Prototype {
msg := *m
return &msg
}
测试代码如下:
package test
...
func TestPrototype(t *testing.T) {
message := msg.Builder().
WithSrcAddr("192.168.0.1").
WithSrcPort(1234).
WithDestAddr("192.168.0.2").
WithDestPort(8080).
WithHeaderItem("contents", "application/json").
WithBodyItem("record1").
WithBodyItem("record2").
Build()
// 复制一份消息
newMessage := message.Clone().(*msg.Message)
if newMessage.Header.SrcAddr != message.Header.SrcAddr {
t.Errorf("Clone Message failed.")
}
if newMessage.Body.Items[0] != message.Body.Items[0] {
t.Errorf("Clone Message failed.")
}
}
// 运行结果
=== RUN TestPrototype
--- PASS: TestPrototype (0.00s)
PASS
暂无评论内容