摘要:本文咱们次要介绍结构型模式中的代理模式、装璜模式、外观模式和享元模式。
本文分享自华为云社区《快来,这里有23种设计模式的Go语言实现(三)》,原文作者:元闰子 。
设计模式(Design Pattern)是一套被重复应用、少数人通晓的、通过分类编目标、代码设计教训的总结,应用设计模式是为了可重用代码、让代码更容易被别人了解并且保障代码可靠性。本文将介绍几种结构型模式:代理模式、装璜模式、外观模式和享元模式。
代理模式(Proxy Pattern)
简介
代理模式为一个对象提供一种代理以管制对该对象的拜访,它是一个使用率十分高的设计模式,即便在现实生活中,也是很常见,比方演唱会门票黄牛。假如你须要看一场演唱会,然而官网上门票曾经售罄,于是就当天到现场通过黄牛高价买了一张。在这个例子中,黄牛就相当于演唱会门票的代理,在正式渠道无奈购买门票的状况下,你通过代理实现了该指标。
从演唱会门票的例子咱们也能够看出,应用代理模式的关键在于当Client不不便间接拜访一个对象时,提供一个代理对象管制该对象的拜访。Client实际上拜访的是代理对象,代理对象会将Client的申请转给本体对象去解决。
在程序设计中,代理模式也分为好几种:
1、近程代理(remote proxy),近程代理实用于提供服务的对象处在近程的机器上,通过一般的函数调用无奈应用服务,须要通过近程代理来实现。因为并不能间接拜访本体对象,所有近程代理对象通常不会间接持有本体对象的援用,而是持有远端机器的地址,通过网络协议去拜访本体对象。
2、虚构代理(virtual proxy),在程序设计中经常会有一些重量级的服务对象,如果始终持有该对象实例会十分耗费系统资源,这时能够通过虚构代理来对该对象进行提早初始化。
3、爱护代理(protection proxy),爱护代理用于管制对本体对象的拜访,罕用于须要给Client的拜访加上权限验证的场景。
4、缓存代理(cache proxy),缓存代理次要在Client与本体对象之间加上一层缓存,用于减速本体对象的拜访,常见于连贯数据库的场景。
5、智能援用(smart reference),智能援用为本体对象的拜访提供了额定的动作,常见的实现为C++中的智能指针,为对象的拜访提供了计数性能,当拜访对象的计数为0时销毁该对象。
这几种代理都是一样的实现原理,上面咱们将介绍近程代理的Go语言实现。
Go实现
思考要将音讯解决零碎输入到数据存储到一个数据库中,数据库的接口如下:
package db...// Key-Value数据库接口type KvDb interface { // 存储数据 // 其中reply为操作后果,存储胜利为true,否则为false // 当连贯数据库失败时返回error,胜利则返回nil Save(record Record, reply *bool) error // 依据key获取value,其中value通过函数参数中指针类型返回 // 当连贯数据库失败时返回error,胜利则返回nil Get(key string, value *string) error}type Record struct { Key string Value string}
数据库是一个Key-Value数据库,应用map存储数据,上面为数据库的服务端实现,db.Server实现了db.KvDb接口:
package db...// 数据库服务端实现type Server struct { // 采纳map存储key-value数据 data map[string]string}func (s *Server) Save(record Record, reply *bool) error { if s.data == nil{ s.data = make(map[string]string) } s.data[record.Key] = record.Value *reply = true return nil}func (s *Server) Get(key string, reply *string) error { val, ok := s.data[key] if !ok { *reply = "" return errors.New("Db has no key " + key) } *reply = val return nil}
音讯解决零碎和数据库并不在同一台机器上,因而音讯解决零碎不能间接调用db.Server的办法进行数据存储,像这种服务提供者和服务使用者不在同一机器上的场景,应用近程代理再适宜不过了。
近程代理中,最常见的一种实现是近程过程调用(Remote Procedure Call,简称 RPC),它容许客户端利用能够像调用本地对象一样间接调用另一台不同的机器上服务端利用的办法。在Go语言畛域,除了赫赫有名的gRPC,Go规范库net/rpc包里也提供了RPC的实现。上面,咱们通过net/rpc对外提供数据库服务端的能力:
package db...// 启动数据库,对外提供RPC接口进行数据库的拜访func Start() { rpcServer := rpc.NewServer() server := &Server{data: make(map[string]string)} // 将数据库接口注册到RPC服务器上 if err := rpcServer.Register(server); err != nil { fmt.Printf("Register Server to rpc failed, error: %v", err) return } l, err := net.Listen("tcp", "127.0.0.1:1234") if err != nil { fmt.Printf("Listen tcp failed, error: %v", err) return } go rpcServer.Accept(l) time.Sleep(1 * time.Second) fmt.Println("Rpc server start success.")}
到目前为止,咱们曾经为数据库提供了对外拜访的形式。当初,咱们须要一个近程代理来连贯数据库服务端,并进行相干的数据库操作。对音讯解决零碎而言,它不须要,也不应该晓得近程代理与数据库服务端交互的底层细节,这样能够加重零碎之间的耦合。因而,近程代理须要实现db.KvDb:
package db...// 数据库服务端近程代理,实现db.KvDb接口type Client struct { // RPC客户端 cli *rpc.Client}func (c *Client) Save(record Record, reply *bool) error { var ret bool // 通过RPC调用服务端的接口 err := c.cli.Call("Server.Save", record, &ret) if err != nil { fmt.Printf("Call db Server.Save rpc failed, error: %v", err) *reply = false return err } *reply = ret return nil}func (c *Client) Get(key string, reply *string) error { var ret string // 通过RPC调用服务端的接口 err := c.cli.Call("Server.Get", key, &ret) if err != nil { fmt.Printf("Call db Server.Get rpc failed, error: %v", err) *reply = "" return err } *reply = ret return nil}// 工厂办法,返回近程代理实例func CreateClient() *Client { rpcCli, err := rpc.Dial("tcp", "127.0.0.1:1234") if err != nil { fmt.Printf("Create rpc client failed, error: %v.", err) return nil } return &Client{cli: rpcCli}}
作为近程代理的db.Client并没有间接持有db.Server的援用,而是持有了它的ip:port,通过RPC客户端调用了它的办法。
接下来,咱们须要为音讯解决零碎实现一个新的Output插件DbOutput,调用db.Client近程代理,将音讯存储到数据库上。
在《应用Go实现GoF的23种设计模式(二)》中咱们为Plugin引入生命周期的三个办法Start、Stop、Status之后,每新增一个新的插件,都须要实现这三个办法。然而大多数插件的这三个办法的逻辑基本一致,因而导致了肯定水平的代码冗余。对于反复代码问题,有什么好的解决办法呢?组合模式!
上面,咱们应用组合模式将这个办法提取成一个新的对象LifeCycle,这样新增一个插件时,只需将LifeCycle作为匿名成员(嵌入组合),就能解决冗余代码问题了。
package plugin...type LifeCycle struct { name string status Status}func (l *LifeCycle) Start() { l.status = Started fmt.Printf("%s plugin started.\n", l.name)}func (l *LifeCycle) Stop() { l.status = Stopped fmt.Printf("%s plugin stopped.\n", l.name)}func (l *LifeCycle) Status() Status { return l.status}
DbOutput的实现如下,它持有一个近程代理,通过后者将音讯存储到远端的数据库中。
package plugin...type DbOutput struct { LifeCycle // 操作数据库的近程代理 proxy db.KvDb}func (d *DbOutput) Send(msg *msg.Message) { if d.status != Started { fmt.Printf("%s is not running, output nothing.\n", d.name) return } record := db.Record{ Key: "db", Value: msg.Body.Items[0], } reply := false err := d.proxy.Save(record, &reply) if err != nil || !reply { fmt.Println("Save msg to db server failed.") }}func (d *DbOutput) Init() { d.proxy = db.CreateClient() d.name = "db output"}
测试代码如下:
package test...func TestDbOutput(t *testing.T) { db.Start() config := pipeline.Config{ Name: "pipeline3", Input: plugin.Config{ PluginType: plugin.InputType, Name: "hello", }, Filter: plugin.Config{ PluginType: plugin.FilterType, Name: "upper", }, Output: plugin.Config{ PluginType: plugin.OutputType, Name: "db", }, } p := pipeline.Of(config) p.Start() p.Exec() // 验证DbOutput存储的正确性 cli := db.CreateClient() var val string err := cli.Get("db", &val) if err != nil { t.Errorf("Get db failed, error: %v\n.", err) } if val != "HELLO WORLD" { t.Errorf("expect HELLO WORLD, but actual %s.", val) }}// 运行后果=== RUN TestDbOutputRpc server start success.db output plugin started.upper filter plugin started.hello input plugin started.Pipeline started.--- PASS: TestDbOutput (1.01s)PASS
装璜模式(Decorator Pattern)
简介
在程序设计中,咱们经常须要为对象增加新的行为,很多同学的第一个想法就是扩大本体对象,通过继承的形式达到目标。然而应用继承不可避免地有如下两个弊病:(1)继承时动态的,在编译期间就曾经确定,无奈在运行时扭转对象的行为。(2)子类只能有一个父类,当须要增加的新性能太多时,容易导致类的数量剧增。
对于这种场景,咱们通常会应用装璜模式(Decorator Pattern)来解决,它应用组合而非继承的形式,可能动静地为本体对象叠加新的行为。实践上,只有没有限度,它能够始终把性能叠加上来。装璜模式最经典的利用当属Java的I/O流体系,通过装璜模式,使用者能够动静地为原始的输入输出流增加性能,比方依照字符串输入输出,增加缓存等,使得整个I/O流体系具备很高的可扩展性和灵活性。
从构造上看,装璜模式和代理模式具备很高的相似性,然而两种所强调的点不一样。前者强调的是为本体对象增加新的性能,后者强调的是对本体对象的访问控制。当然,代理模式中的智能援用在笔者看来就跟装璜模式齐全一样了。
Go实现
思考为音讯解决零碎减少这样的一个性能,统计每个音讯输出源别离产生了多少条音讯,也就是别离统计每个Input产生Message的数量。最简略的办法是在每一个Input的Receive办法中进行打点统计,然而这样会导致统计代码与业务代码的耦合。如果统计逻辑产生了变动,就会产生霰弹式批改,随着Input类型的增多,相干代码也会变得越来越难保护。
更好的办法是将统计逻辑放到一个中央,并在每次调用Input的Receive办法后进行打点统计。而这恰好适宜采纳装璜模式,为Input(本体对象)提供打点统计性能(新的行为)。咱们能够设计一个InputMetricDecorator作为Input的装璜器,在装璜器中实现打点统计的逻辑。
首先,咱们须要设计一个用于统计每个Input产生Message数量的对象,该对象应该是一个全局惟一的,因而采纳单例模式进行了实现:
package metric...// 音讯输出源统计,设计为单例type input struct { // 寄存统计后果,key为Input类型如hello、kafka // value为对应Input的音讯统计 metrics map[string]uint64 // 统计打点时加锁 mu *sync.Mutex}// 给名称为inputName的Input音讯计数加1func (i *input) Inc(inputName string) { i.mu.Lock() defer i.mu.Unlock() if _, ok := i.metrics[inputName]; !ok { i.metrics[inputName] = 0 } i.metrics[inputName] = i.metrics[inputName] + 1}// 输入以后所有打点的状况func (i *input) Show() { fmt.Printf("Input metric: %v\n", i.metrics)}// 单例var inputInstance = &input{ metrics: make(map[string]uint64), mu: &sync.Mutex{},}func Input() *input { return inputInstance}
接下来咱们开始实现InputMetricDecorator,它实现了Input接口,并持有一个本体对象Input。在InputMetricDecorator在Receive办法中调用本体Input的Receive办法,并实现统计动作。
package plugin...type InputMetricDecorator struct { input Input}func (i *InputMetricDecorator) Receive() *msg.Message { // 调用本体对象的Receive办法 record := i.input.Receive() // 实现统计逻辑 if inputName, ok := record.Header.Items["input"]; ok { metric.Input().Inc(inputName) } return record}func (i *InputMetricDecorator) Start() { i.input.Start()}func (i *InputMetricDecorator) Stop() { i.input.Stop()}func (i *InputMetricDecorator) Status() Status { return i.input.Status()}func (i *InputMetricDecorator) Init() { i.input.Init()}// 工厂办法, 实现装璜器的创立func CreateInputMetricDecorator(input Input) *InputMetricDecorator { return &InputMetricDecorator{input: input}}
最初,咱们在Pipeline的工厂办法上,为本体Input加上InputMetricDecorator代理:
package 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) // 为本体Input加上InputMetricDecorator装璜器 p.input = plugin.CreateInputMetricDecorator(p.input) return p}
测试代码如下:
package test...func TestInputMetricDecorator(t *testing.T) { p1 := pipeline.Of(pipeline.HelloConfig()) p2 := pipeline.Of(pipeline.KafkaInputConfig()) p1.Start() p2.Start() p1.Exec() p2.Exec() p1.Exec() metric.Input().Show()}// 运行后果=== RUN TestInputMetricDecoratorConsole output plugin started.Upper filter plugin started.Hello input plugin started.Pipeline started.Console output plugin started.Upper filter plugin started.Kafka input plugin started.Pipeline started.Output: Header:map[content:text input:hello], Body:[HELLO WORLD]Output: Header:map[content:text input:kafka], Body:[I AM MOCK CONSUMER.]Output: Header:map[content:text input:hello], Body:[HELLO WORLD]Input metric: map[hello:2 kafka:1]--- PASS: TestInputMetricProxy (0.00s)PASS
外观模式(Facade Pattern)
简介
从构造上看,外观模式十分的简略,它次要是为子系统提供了一个更高层次的对外对立接口,使得Client可能更敌对地应用子系统的性能。图中,Subsystem Class是子系统中对象的简称,它可能是一个对象,也可能是数十个对象的汇合。外观模式升高了Client与Subsystem之间的耦合,只有Facade不变,不论Subsystem怎么变动,对于Client而言都是无感知的。
外观模式在程序设计中用的十分多,比方咱们在商城上点击购买的按钮,对于购买者而言,只看到了购买这一对立的接口,然而对于商城零碎而言,其外部则进行了一系列的业务解决,比方库存查看、订单解决、领取、物流等等。外观模式极大地晋升了用户体验,将用户从简单的业务流程中解放了进去。
外观模式常常使用于分层架构上,通常咱们都会为分层架构中的每一个层级提供一个或多个对立对外的拜访接口,这样就能让各个层级之间的耦合性更低,使得零碎的架构更加正当。
Go实现
外观模式实现起来也很简略,还是思考后面的音讯解决零碎。在Pipeline中,每一条音讯会顺次通过Input->Filter->Output的解决,代码实现起来就是这样:
p := pipeline.Of(config)message := p.input.Receive()message = p.filter.Process(message)p.output.Send(message)
然而,对于Pipeline的使用者而言,他可能并不关怀音讯具体的解决流程,他只需晓得音讯曾经通过Pipeline解决即可。因而,咱们须要设计一个简略的对外接口:
package pipeline...func (p *Pipeline) Exec() { msg := p.input.Receive() msg = p.filter.Process(msg) p.output.Send(msg)}
这样,使用者只需简略地调用Exec办法,就能实现一次音讯的解决,测试代码如下:
package test...func TestPipeline(t *testing.T) { p := pipeline.Of(pipeline.HelloConfig()) p.Start() // 调用Exec办法实现一次音讯的解决 p.Exec()}// 运行后果=== RUN TestPipelineconsole output plugin started.upper filter plugin started.hello input plugin started.Pipeline started.Output: Header:map[content:text input:hello], Body:[HELLO WORLD]--- PASS: TestPipeline (0.00s)PASS
享元模式(Flyweight Pattern)
简介
在程序设计中,咱们经常会碰到一些很重型的对象,它们通常领有很多的成员属性,当零碎中充斥着大量的这些对象时,零碎的内存将会接受微小的压力。此外,频繁的创立这些对象也极大地耗费了零碎的CPU。很多时候,这些重型对象里,大部分的成员属性都是固定的,这种场景下, 能够应用享元模式进行优化,将其中固定不变的局部设计成共享对象(享元,flyweight),这样就能节俭大量的零碎内存和CPU。
享元模式摒弃了在每个对象中保留所有数据的形式, 通过共享多个对象所共有的雷同状态, 让你能在无限的内存容量中载入更多对象。
当咱们决定对一个重型对象采纳享元模式进行优化时,首先须要将该重型对象的属性划分为两类,可能共享的和不能共享的。前者咱们称为外部状态(intrinsic state),存储在享元中,不随享元所处上下文的变动而变动;后者称为内部状态(extrinsic state),它的值取决于享元所处的上下文,因而不能共享。比方,文章A和文章B都援用了图片A,因为文章A和文章B的文字内容是不一样的,因而文字就是内部状态,不能共享;然而它们所援用的图片A是一样的,属于外部状态,因而能够将图片A设计为一个享元
工厂模式通常都会和享元模式结对呈现,享元工厂提供了惟一获取享元对象的接口,这样Client就感知不到享元是如何共享的,升高了模块的耦合性。享元模式和单例模式有些相似的中央,都是在零碎中共享对象,然而单例模式更关怀的是对象在零碎中仅仅创立一次,而享元模式更关怀的是如何在多个对象中共享雷同的状态。
Go实现
假如当初须要设计一个零碎,用于记录NBA中的球员信息、球队信息以及比赛结果。
球队Team的数据结构定义如下:
package nba...type TeamId uint8const ( Warrior TeamId = iota Laker)type Team struct { Id TeamId // 球队ID Name string // 球队名称 Players []*Player // 球队中的球员}
球员Player的数据结构定义如下:
package nba...type Player struct { Name string // 球员名字 Team TeamId // 球员所属球队ID}
比赛结果Match的数据结构定义如下:
package nba...type Match struct { Date time.Time // 较量工夫 LocalTeam *Team // 主场球队 VisitorTeam *Team // 客场球队 LocalScore uint8 // 主场球队得分 VisitorScore uint8 // 客场球队得分}func (m *Match) ShowResult() { fmt.Printf("%s VS %s - %d:%d\n", m.LocalTeam.Name, m.VisitorTeam.Name, m.LocalScore, m.VisitorScore)}
NBA中的一场较量由两个球队,主场球队和客场球队,实现较量,对应着代码就是,一个Match实例会持有2个Team实例。目前,NBA总共由30支球队,依照每个赛季每个球队打82场常规赛算,一个赛季总共会有2460场较量,对应地,就会有4920个Team实例。然而,NBA的30支球队是固定的,实际上只需30个Team实例就能残缺地记录一个赛季的所有较量信息,剩下的4890个Team实例属于冗余的数据。
这种场景下就适宜采纳享元模式来进行优化,咱们把Team设计成多个Match实例之间的享元。享元的获取通过享元工厂来实现,享元工厂teamFactory的定义如下,Client对立应用teamFactory.TeamOf办法来获取球队Team实例。其中,每个球队Team实例只会创立一次,而后增加到球队池中,后续获取都是间接从池中获取,这样就达到了共享的目标。
package nba...type teamFactory struct { // 球队池,缓存球队实例 teams map[TeamId]*Team}// 依据TeamId获取Team实例,从池中获取,如果池里没有,则创立func (t *teamFactory) TeamOf(id TeamId) *Team { team, ok := t.teams[id] if !ok { team = createTeam(id) t.teams[id] = team } return team}// 享元工厂的单例var factory = &teamFactory{ teams: make(map[TeamId]*Team),}func Factory() *teamFactory { return factory}// 依据TeamId创立Team实例,只在TeamOf办法中调用,内部不可见func createTeam(id TeamId) *Team { switch id { case Warrior: w := &Team{ Id: Warrior, Name: "Golden State Warriors", } curry := &Player{ Name: "Stephen Curry", Team: Warrior, } thompson := &Player{ Name: "Klay Thompson", Team: Warrior, } w.Players = append(w.Players, curry, thompson) return w case Laker: l := &Team{ Id: Laker, Name: "Los Angeles Lakers", } james := &Player{ Name: "LeBron James", Team: Laker, } davis := &Player{ Name: "Anthony Davis", Team: Laker, } l.Players = append(l.Players, james, davis) return l default: fmt.Printf("Get an invalid team id %v.\n", id) return nil }}
测试代码如下:
package test...func TestFlyweight(t *testing.T) { game1 := &nba.Match{ Date: time.Date(2020, 1, 10, 9, 30, 0, 0, time.Local), LocalTeam: nba.Factory().TeamOf(nba.Warrior), VisitorTeam: nba.Factory().TeamOf(nba.Laker), LocalScore: 102, VisitorScore: 99, } game1.ShowResult() game2 := &nba.Match{ Date: time.Date(2020, 1, 12, 9, 30, 0, 0, time.Local), LocalTeam: nba.Factory().TeamOf(nba.Laker), VisitorTeam: nba.Factory().TeamOf(nba.Warrior), LocalScore: 110, VisitorScore: 118, } game2.ShowResult() // 两个Match的同一个球队应该是同一个实例的 if game1.LocalTeam != game2.VisitorTeam { t.Errorf("Warrior team do not use flyweight pattern") }}// 运行后果=== RUN TestFlyweightGolden State Warriors VS Los Angeles Lakers - 102:99Los Angeles Lakers VS Golden State Warriors - 110:118--- PASS: TestFlyweight (0.00s)
总结
本文咱们次要介绍了结构型模式中的代理模式、装璜模式、外观模式和享元模式。代理模式为一个对象提供一种代理以管制对该对象的拜访,强调的是对本体对象的访问控制;装璜模式可能动静地为本体对象叠加新的行为,强调的是为本体对象增加新的性能;外观模式为子系统提供了一个更高层次的对外对立接口,强调的是分层和解耦;享元模式通过共享对象来升高零碎的资源耗费,强调的是如何在多个对象中共享雷同的状态。
点击关注,第一工夫理解华为云陈腐技术~