📜 ⬆️ ⬇️

Familiar strangers or again about the use of design patterns

Tons of articles have been written on the topic of design patterns, and many books have been published. However, this topic does not cease to be relevant, since the patterns allow us to use ready-made, time-tested solutions, which allows us to reduce project development time by improving the quality of the code and reducing technical debts.


Since the advent of design patterns, new examples of their effective use have appeared. And that's great. However, it was not done without a fly in the ointment: each language has its own specifics. And golang - and even more so (it does not even have the classical model of the PLO). Therefore, there are variations of patterns in relation to individual programming languages. In this article I would like to touch on the theme of design patterns in relation to golang.


Decorator


The “Decorator” template allows you to connect additional behavior to an object (statically or dynamically) without affecting the behavior of other objects of the same class. The template is often used to comply with the Single Responsibility Principle, since it allows to divide the functionality between classes for solving specific tasks.

The well-known DECORATOR pattern is widely used in many programming languages. So, in golang, all middleware is built on its basis. For example, query profiling might look like this:


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

In this case, the decorator interface is the only function. As a rule, this is what you should strive for. However, a decorator with a wider interface can sometimes be useful. Consider, for example, access to a database (database / sql package). Suppose we need to do all the same profiling of database queries. In this case, we need to:



As a result, we get a decorator that allows you to profile all database queries. The merits of this approach are indisputable:



For example, you can implement the following types of decorators:



As a rule, when implementing rich decorators, implementation of all methods is not required: it is enough to delegate non-implementable methods to an internal object.


Suppose we need to implement an advanced logger to keep track of DML database queries (to track INSERT / UPDATE / DELETE requests). In this case, we do not need to implement the entire database interface - it is enough to block only the Exec method.


 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 { ... } 

Thus, we see that creating even a rich decorator in the golang language does not present any particular difficulties.


Template method


Template method (eng. Template method) is a behavioral design pattern that defines the basis of the algorithm and allows successors to redefine certain steps of the algorithm without changing its structure as a whole.

The golang language supports the OOP paradigm, so this pattern cannot be implemented in its pure form. However, nothing prevents us from improvising using appropriate constructor functions.


Suppose we need to define a template method with the following signature:


 func Method(s string) error 

At the declaration it is enough for us to use a field of a functional type. For the convenience of working with it, we can use the wrapper function that complements the call with the missing parameter, and to create a specific instance, the corresponding constructor function.


 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") ... } 

As can be seen from the example, the semantics of using a pattern is almost the same as the classical OOP.


Adapter


The Adapter design pattern allows you to use the interface of an existing class as another interface. This template is often used to ensure that some classes work with others without changing their source code.

In general, both individual functions and entire interfaces can serve as adapters. If the interfaces are more or less clear and predictable, then from the point of view of individual functions there are some subtleties.


Suppose we are writing some service that has some internal API:


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

If we need to provide a public API with a different interface (say for working with gRPC), then we can simply use the adapter functions that are involved in the conversion of the interface. For this purpose it is very convenient to use closures.


 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 } } 

The action of the makeCreateEndpoint function consists of three standard steps:



By this principle, all the endpoints in the gokit package are built.


Visitor


Template "Visitor" is a way to separate the algorithm from the structure of the object in which it operates. The result of separation is the ability to add new operations to existing structures of objects without modifying them. This is one of the ways to comply with the open / closed principle.

Consider the well-known visitor pattern on the example of geometric shapes.


 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) } 

Suppose we want to write a strategy for calculating the distance from a given point to the specified shape.


 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)) } 

Similarly, we can implement other strategies we need:



Moreover, the previously defined figures (Point, Line, Circle ...) do not know anything about these strategies. Their only knowledge is limited to the GeometryVisitor interface. This allows you to isolate them in a separate package.


At one time, while working on a cartographic project, I had the task to write a function for determining the distance between two arbitrary geographical objects. The solutions were very different, but all of them were not effective enough and elegant. Considering the Visitor pattern somehow, I noticed that it serves to select a target method and somewhat resembles a separate recursion step, which, as is well known, simplifies the task. This gave me the idea of ​​using Double Visitor. Imagine my surprise when I discovered that this approach is not mentioned at all on the Internet.


 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}) } 

Thus, we have built a two-level selective mechanism, which, as a result of its work, will trigger an appropriate method for calculating the distance between two primitives. We can only write these methods and the goal is achieved. This is how an elegantly nondeterministic task can be reduced to a number of elementary functions.


Conclusion


Despite the fact that golang lacks a classic OOP, the language produces its own dialect of patterns that play on the strengths of the language. These patterns go the standard way from denial to universal acceptance and eventually become best practics.


If dear habrozhiteli have any thoughts on patterns, please do not be shy and express your thoughts on this.


')

Source: https://habr.com/ru/post/448590/


All Articles