熟悉的陌生人或再次关于使用设计模式

关于设计模式的主题,已撰写了大量文章,并出版了许多书籍。 但是,这个话题并没有停止,因为这些模式使我们能够使用现成的,经过时间检验的解决方案,这使我们能够通过提高代码质量和减少技术债务来减少项目开发的时间。


自设计模式问世以来,就出现了有效使用它们的新示例。 这太好了。 但是,美中不足的是:每种语言都有自己的特点。 还有golang-甚至更多(它甚至没有经典的OOP模型)。 因此,与各个编程语言有关,模式也有所不同。 在本文中,我想谈谈与golang相关的设计模式。


装饰器


Decorator模板允许您将其他行为(静态或动态)连接到对象,而不会影响同一类的其他对象的行为。 模板通常用于遵守“单一职责原则”,因为它允许您在类之间拆分功能以解决特定问题。

众所周知的DECORATOR模式已在许多编程语言中广泛使用。 因此,在golang中,所有中间件都是在其基础上构建的。 例如,查询分析可能如下所示:


func ProfileMiddleware(next http.Handler) http.Handler { started := time.Now() next.ServeHTTP() elapsed := time.Now().Sub(started) fmt.Printf("HTTP: elapsed time %d", elapsed) } 

在这种情况下,装饰器接口是唯一的功能。 通常应寻求这一点。 但是,具有更广泛接口的装饰器有时可能会有用。 例如,考虑访问数据库(包数据库/ sql)。 假设我们需要对数据库查询进行相同的分析。 在这种情况下,我们需要:


  • 代替通过指针与数据库直接交互,我们需要通过接口进行交互(以将行为与实现分开)。
  • 为每个执行SQL数据库查询的方法创建一个包装器。

结果,我们得到了一个装饰器,该装饰器允许您将所有查询分析到数据库中。 这种方法的优点不可否认:


  • 维护核心数据库访问组件的代码清洁度。
  • 每个装饰器实现一个需求。 因此,实现了其易于实现。
  • 由于装饰器的组成,我们得到了可轻松适应我们需求的可扩展模型。
  • 由于事件探查器的简单关闭,我们在生产模式下获得了零性能开销。

因此,例如,您可以实现以下类型的装饰器:


  • 心跳 对数据库执行ping操作以保持与该数据库的连接。
  • 探查器。 请求主体及其执行时间的输出。
  • 嗅探器 数据库指标的集合。
  • 克隆 克隆原始数据库以进行调试。

通常,在实现丰富的装饰器时,不需要所有方法的实现:将未实现的方法委托给内部对象就足够了。


假设我们需要实现一个高级记录器来跟踪数据库的DML查询(以跟踪INSERT / UPDATE / DELETE查询)。 在这种情况下,我们不需要实现整个数据库接口-只需重叠Exec方法即可。


 type MyDatabase interface{ Query(...) (sql.Rows, error) QueryRow(...) error Exec(query string, args ...interface) error Ping() error } type MyExecutor struct { MyDatabase } func (e *MyExecutor) Exec(query string, args ...interface) error { ... } 

因此,我们看到即使使用golang语言创建丰富的装饰器也不是特别困难。


模板方法


模板方法(Eng。Template method)-一种行为设计模式,它定义了算法的基础,并允许继承人重新定义算法的某些步骤,而不会整体上改变其结构。

golang语言支持OOP范例,因此无法以其纯格式实现此模板。 但是,没有什么可以阻止我们即兴使用适当的函数的构造函数。


假设我们需要使用以下签名定义模板方法:


 func Method(s string) error 

在声明时,使用函数类型的字段就足够了。 为了方便使用它,我们可以使用wrapper函数使用缺少的参数来补充调用,并创建一个特定的实例,即对应的构造函数。


 type MyStruct struct { MethodImpl func (me *MyStruct, s string) error } // Wrapper for template method func (ms *MyStruct) Method(s string) error { return ms.MethodImpl(ms, s) } // First constructor func NewStruct1() *MyStruct { return &MyStruct{ MethodImpl: func(me *MyStruct, s string) error { // Implementation 1 ... }, } } // Second constructor func NewStruct2() *MyStruct { return &MyStruct{ MethodImpl: func(me *MyStruct, s string) error { // Implementation 2 ... }, } } func main() { // Create object instance o := NewStruct2() // Call the template method err := o.Method("hello") ... } 

从示例中可以看到,使用模式的语义几乎与经典的OOP没有什么不同。


转接头


“适配器”设计模式允许您将现有类的接口用作另一个接口。 该模板通常用于确保某些类与其他类一起使用而不更改其源代码。

通常,适配器可以充当单独的功能以及整个接口。 如果使用接口,一切或多或少是清晰可预测的,那么从单个功能的角度来看,就会有一些微妙之处。


假设我们编写了一些具有一些内部API的服务:


 type MyService interface { Create(ctx context.Context, order int) (id int, err error) } 

如果我们需要为公共API提供不同的接口(例如与gRPC配合使用),那么我们可以简单地使用处理接口转换的适配器函数。 为此目的使用闭包非常方便。


 type Endpoint func(ctx context.Context, request interface{}) (interface{}, error) type CreateRequest struct { Order int } type CreateResponse struct { ID int, Err error } func makeCreateEndpoint(s MyService) Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { // Decode request req := request.(CreateRequest) // Call service method id, err := s.Create(ctx, req.Order) // Encode response return CreateResponse{ID: id, Err: err}, nil } } 

makeCreateEndpoint函数具有三个标准步骤:


  • 解码值
  • 从正在实现的服务的内部API调用方法
  • 值编码

gokit软件包中的所有端点均基于此原理构建。


参观者


“访问者”模板是一种将算法与操作对象的结构分离的方法。 分离的结果是能够在不修改现有对象结构的情况下为其添加新操作。 这是遵守开放/封闭原则的一种方法。

在几何形状示例中考虑著名的访客模式。


 type Geometry interface { Visit(GeometryVisitor) (interface{}, error) } type GeometryVisitor interface { VisitPoint(p *Point) (interface{}, error) VisitLine(l *Line) (interface{}, error) VisitCircle(c *Circle) (interface{}, error) } type Point struct{ X, Y float32 } func (point *Point) Visit(v GeometryVisitor) (interface{}, error) { return v.VisitPoint(point) } type Line struct{ X1, Y1 float32 X2, Y2 float32 } func (line *Line) Visit(v GeometryVisitor) (interface{}, error) { return v.VisitLine(line) } type Circle struct{ X, Y, R float32 } func (circle *Circle) Visit(v GeometryVisitor) (interface{}, error) { return v.VisitCircle(circle) } 

假设我们要编写一种策略来计算从给定点到指定形状的距离。


 type DistanceStrategy struct { X, Y float32 } func (s *DistanceStrategy) VisitPoint(p *Point) (interface{}, error) { // Evaluate distance from point(X, Y) to point p } func (s *DistanceStrategy) VisitLine(l *Line) (interface{}, error) { // Evaluate distance from point(X, Y) to line l } func (s *DistanceStrategy) VisitCircle(c *Circle) (interface{}, error) { // Evaluate distance from point(X, Y) to circle c } func main() { s := &DistanceStrategy{X: 1, Y: 2} p := &Point{X: 3, Y: 4} res, err := p.Visit(s) if err != nil { panic(err) } fmt.Printf("Distance is %g", res.(float32)) } 

同样,我们可以实施所需的其他策略:


  • 垂直范围
  • 对象的水平范围
  • 建立最小跨度(MBR)
  • 我们需要的其他原语。

此外,先前定义的图形(点,线,圆...)对这些策略一无所知。 他们唯一的知识仅限于GeometryVisitor界面。 这使您可以将它们隔离在单独的程序包中。


一次,在进行制图项目时,我的任务是编写一个确定两个任意地理对象之间距离的函数。 解决方案是非常不同的,但是所有这些解决方案都不够高效和优雅。 考虑到访问者模式,我注意到它用于选择目标方法,并且有点让人想起单独的递归步骤,正如您所知,该步骤简化了任务。 这促使我使用Double Visitor。 当我发现在互联网上根本没有提到这种方法时,请想象一下我的惊讶。


 type geometryStrategy struct{ G Geometry } func (s *geometryStrategy) VisitPoint(p *Point) (interface{}, error) { return sGVisit(&pointStrategy{Point: p}) } func (d *geometryStrategy) VisitLine(l *Line) (interface{}, error) { return sGVisit(&lineStrategy{Line: l}) } func (d *geometryStrategy) VisitCircle(c *Circle) (interface{}, error) { return sGVisit(&circleStrategy{Circle: c}) } type pointStrategy struct{ *Point } func (point *pointStrategy) Visit(p *Point) (interface{}, error) { // Evaluate distance between point and p } func (point *pointStrategy) Visit(l *Line) (interface{}, error) { // Evaluate distance between point and l } func (point *pointStrategy) Visit(c *Circle) (interface{}, error) { // Evaluate distance between point and c } type lineStrategy struct { *Line } func (line *lineStrategy) Visit(p *Point) (interface{}, error) { // Evaluate distance between line and p } func (line *lineStrategy) Visit(l *Line) (interface{}, error) { // Evaluate distance between line and l } func (line *lineStrategy) Visit(c *Circle) (interface{}, error) { // Evaluate distance between line and c } type circleStrategy struct { *Circle } func (circle *circleStrategy) Visit(p *Point) (interface{}, error) { // Evaluate distance between circle and p } func (circle *circleStrategy) Visit(l *Line) (interface{}, error) { // Evaluate distance between circle and l } func (circle *circleStrategy) Visit(c *Circle) (interface{}, error) { // Evaluate distance between circle and c } func Distance(a, b Geometry) (float32, error) { return a.Visit(&geometryStrategy{G: b}) } 

因此,我们建立了一个两级选择机制,作为其工作的结果,它将调用用于计算两个基元之间距离的适当方法。 我们只能编写这些方法,并且目标得以实现。 这就是如何将优美的不确定性问题简化为许多基本函数的方法。


结论


尽管事实上在golang中没有经典的OOP,但该语言仍会产生自己的模式方言,从而发挥其优势。 这些模式是从拒绝到普遍接受的标准方式,并随着时间的流逝成为最佳实践。


如果尊敬的habrozhiteli对模式有任何想法,请不要害羞并表达您的想法。

Source: https://habr.com/ru/post/zh-CN448590/


All Articles