上一篇:【Go实现】实践GoF的23种设计模式:抽象工厂模式
简单的分布式应用系统(示例代码工程):https://github.com/ruanrunxue/Practice-Design-Pattern--Go-Implementation
原型模式(Prototype Pattern)主要解决对象复制的问题,它的核心就是Clone()方法,返回原型对象的复制品。
最简单直接的对象复制方式是这样的:重新实例化一个该对象的实例,然后遍历原始对象的所有成员变量, 并将成员变量值复制到新实例中。但这种方式的缺点也很明显:
更好的方法是使用原型模式,将复制逻辑委托给对象本身,这样,上述两个问题也都解决了。
在简单的分布式应用系统(示例代码工程)中,我们设计了一个服务消息中介(Service Mediator)服务,可以把它看成是一个消息路由器,负责服务发现和消息转发:
消息转发也就意味着它必须将上游服务的请求原封不动地转发给下游服务,这是一个典型的对象复制场景。不过,在我们的实现里,服务消息中介会先修改上行请求的 URI,之后再转发给下游服务。因为上行请求 URI 中携带了下游服务的类型信息,用来做服务发现,在转发给下游服务时必须剔除。
比如,订单服务(order service)要发请求给库存服务(stock service),那么:
如果按照简单直接的对象复制方式,实现是这样的:
// 服务消息中介 type ServiceMediator struct { registryEndpoint network.Endpoint localIp string server *http.Server sidecarFactory sidecar.Factory } // Forward 转发请求,请求URL为 /{serviceType}+ServiceUri 的形式,如/serviceA/api/v1/task func (s *ServiceMediator) Forward(req *http.Request) *http.Response { // 提取上行请求URI中的服务类型 svcType := s.svcTypeOf(req.Uri()) // 剔除服务类型之后的请求URI svcUri := s.svcUriOf(req.Uri()) // 根据服务类型做服务发现 dest, err := s.discovery(svcType) if err != nil { ... // 异常处理 } // 复制上行请求,将URI更改为剔除服务类型之后的URI forwardReq := http.EmptyRequest(). AddUri(svcUri). AddMethod(req.Method()). AddHeaders(req.Headers()). AddQueryParams(req.QueryParams()). AddBody(req.Body()) // 转发请求给下游服务 client, err := http.NewClient(s.sidecarFactory.Create(), s.localIp) if err != nil { ... // 异常处理 } defer client.Close() resp, err := client.Send(dest, forwardReq) if err != nil { ... // 异常处理 } // 复制下行响应,将ReqId更改为上行请求的ReqId,其他保持不变 return http.NewResponse(req.ReqId()). AddHeaders(resp.Headers()). AddStatusCode(resp.StatusCode()). AddProblemDetails(resp.ProblemDetails()). AddBody(resp.Body()) } ...
上述实现中有 2 处进行了对象的复制:上行请求的复制和下行响应的复制。且不说直接进行对象复制具有前文提到的 3 种缺点,就代码可读性上来看也是稍显冗余。下面,我们使用原型模式进行优化。
首先,为http.Request和http.Response定义Clone方法:
// demo/network/http/http_request.go package http type Request struct { reqId ReqId method Method uri Uri queryParams map[string]string headers map[string]string body interface{} } // 关键点1: 定义原型复制方法Clone func (r *Request) Clone() *Request { // reqId重新生成,其他都拷贝原来的值 reqId := rand.Uint32() % 10000 return &Request{ reqId: ReqId(reqId), method: r.method, uri: r.uri, queryParams: r.queryParams, headers: r.headers, body: r.body, } } ... // demo/network/http/http_response.go type Response struct { reqId ReqId statusCode StatusCode headers map[string]string body interface{} problemDetails string } func (r *Response) Clone() *Response { return &Response{ reqId: r.reqId, statusCode: r.statusCode, headers: r.headers, body: r.body, problemDetails: r.problemDetails, } } ...
最后,在客户端程序处通过Clone方法来完成对象的复制:
// demo/service/mediator/service_mediator.go type ServiceMediator struct {...} func (s *ServiceMediator) Forward(req *http.Request) *http.Response { ... dest, err := s.discovery(svcType) if err != nil { ... } // 关键点2: 通过Clone方法完成对象的复制,然后在此基础上进行进一步的修改 forwardReq := req.Clone().AddUri(svcUri) ... resp, err := client.Send(dest, forwardReq) if err != nil { ... } return resp.Clone().AddReqId(req.ReqId()) }
原型模式的实现相对简单,可总结为 2 个关键点:
需要注意的是,我们不一定非得遵循标准的原型模式 UML 结构定义一个原型接口,然后让原型对象实现它,比如:
// Cloneable 原型复制接口 type Cloneable interface { Clone() Cloneable } type Response struct {...} // 实现原型复制接口 func (r *Response) Clone() Cloneable { return &Response{ reqId: r.reqId, statusCode: r.statusCode, headers: r.headers, body: r.body, problemDetails: r.problemDetails, } }
在当前场景下,这样并不会给程序带来任何好处,反而新增一次类型强转,让程序变得更复杂了:
func (s *ServiceMediator) Forward(req *http.Request) *http.Response { ... resp, err := client.Send(dest, forwardReq) if err != nil { ... } // 因为Clone方法返回的是Cloneable接口,因此需要转型为*http.Response return resp.Clone().(*http.Response).AddReqId(req.ReqId()) }
所以,运用设计模式,最重要的是学得其中精髓,而不是仿照其形式,否则很容易适得其反。
原型模式和建造者模式相结合,也是常见的场景。还是以http.Request为例:
首先,我们先为它新增一个requestBuilder对象来完成对象的构造:
// demo/network/http/http_request_builder.go type requestBuilder struct { req *Request } // 普通Builder工厂方法,新创建一个Request对象 func NewRequestBuilder() *requestBuilder { return &requestBuilder{req: EmptyRequest()} } func (r *requestBuilder) AddMethod(method Method) *requestBuilder { r.req.method = method return r } func (r *requestBuilder) AddUri(uri Uri) *requestBuilder { r.req.uri = uri return r } ... // 一系列 Addxxx 方法 func (r *requestBuilder) Builder() *Request { return r.req }
下面,我们为requestBuilder新增一个NewRequestBuilderCopyFrom工厂方法来达到原型复制的效果:
// demo/network/http/http_request_builder.go // 实现原型模式的Builder工厂方法,复制已有的Request对象 func NewRequestBuilderCopyFrom(req *Request) *requestBuilder { reqId := rand.Uint32() % 10000 replica := &Request{ reqId: ReqId(reqId), method: req.method, uri: req.uri, queryParams: req.queryParams, headers: req.headers, body: req.body, } // 将复制后的对象赋值给requestBuilder return &requestBuilder{req: replica} }
用法如下:
func (s *ServiceMediator) Forward(req *http.Request) *http.Response { ... dest, err := s.discovery(svcType) if err != nil { ... } // 原型模式和建造者模式相结合的实现 forwardReq := http.NewRequestBuilderCopyFrom(req).Builder().AddUri(svcUri) ... resp, err := client.Send(dest, forwardReq) if err != nil { ... } // 普通原型模式的实现 return resp.Clone().AddReqId(req.ReqId()) }
如果原型对象的成员属性包含了指针类型,那么就会存在浅拷贝和深拷贝两种复制方式,比如对于原型对象ServiceProfile,其中的Region属性为指针类型:
// demo/service/registry/model/service_profile.go package model // ServiceProfile 服务档案,其中服务ID唯一标识一个服务实例,一种服务类型可以有多个服务实例 type ServiceProfile struct { Id string // 服务ID Type ServiceType // 服务类型 Status ServiceStatus // 服务状态 Endpoint network.Endpoint // 服务Endpoint Region *Region // 服务所属region Priority int // 服务优先级,范围0~100,值越低,优先级越高 Load int // 服务负载,负载越高表示服务处理的业务压力越大 }
浅拷贝的做法是直接复制指针:
// 浅拷贝实现 func (s *ServiceProfile) Clone() Cloneable { return &ServiceProfile{ Id: s.Id, Type: s.Type, Status: s.Status, Endpoint: s.Endpoint, Region: s.Region, // 指针复制,浅拷贝 Priority: s.Priority, Load: s.Load, } }
深拷贝的做法则是创建新的Region对象:
// 深拷贝实现 func (s *ServiceProfile) Clone() Cloneable { return &ServiceProfile{ Id: s.Id, Type: s.Type, Status: s.Status, Endpoint: s.Endpoint, Region: &Region{ // 新创建一个Region对象,深拷贝 Id: s.Region.Id, Name: s.Region.Name, Country: s.Region.Country, }, Priority: s.Priority, Load: s.Load, } }
具体使用哪种方式,因不同业务场景而异。浅拷贝直接复制指针,在性能上会好点;但某些场景下,引用同一个对象实例可能会导致业务异常,这时候就必须使用深拷贝了。
如前文提到的,原型模式和建造者模式相结合也是一种常见的应用场景。
参考
[1] 【Go实现】实践GoF的23种设计模式:SOLID原则, 元闰子
[2] 【Go实现】实践GoF的23种设计模式:建造者模式, 元闰子
[3] Design Patterns, Chapter 3. Creational Patterns, GoF
更多文章请关注微信公众号:元闰子的邀请