Sobre o tema dos padrões de design, toneladas de artigos foram escritos e muitos livros foram publicados. No entanto, esse tópico não deixa de ser relevante, porque os padrões nos permitem usar soluções prontas e testadas pelo tempo, o que nos permite reduzir o tempo de desenvolvimento do projeto, melhorando a qualidade do código e reduzindo as dívidas técnicas.
Desde o advento dos padrões de design, há sempre novos exemplos de seu uso efetivo. E isso é maravilhoso. No entanto, houve uma mosca na pomada: cada idioma tem suas próprias especificidades. E golang - e mais ainda (nem sequer tem um modelo clássico de POO). Portanto, existem variações dos padrões em relação às linguagens de programação individuais. Neste artigo, gostaria de abordar o tópico dos padrões de design em relação ao golang.
Decorador
O modelo Decorator permite conectar comportamento adicional ao objeto (estaticamente ou dinamicamente) sem afetar o comportamento de outros objetos da mesma classe. Um modelo é frequentemente usado para aderir ao Princípio de Responsabilidade Única, pois permite dividir a funcionalidade entre as classes para resolver problemas específicos.
O conhecido padrão DECORATOR é amplamente usado em muitas linguagens de programação. Portanto, no golang, todo o middleware é construído com base em ele. Por exemplo, a criação de perfil de consulta pode ser assim:
func ProfileMiddleware(next http.Handler) http.Handler { started := time.Now() next.ServeHTTP() elapsed := time.Now().Sub(started) fmt.Printf("HTTP: elapsed time %d", elapsed) }
Nesse caso, a interface do decorador é a única função. Como regra, isso deve ser buscado. No entanto, um decorador com uma interface mais ampla às vezes pode ser útil. Por exemplo, considere o acesso a um banco de dados (pacote database / sql). Suponha que precisamos fazer o mesmo perfil de consultas ao banco de dados. Nesse caso, precisamos:
- Em vez de interagir diretamente com o banco de dados por meio de um ponteiro, precisamos ir para a interação pela interface (para separar o comportamento da implementação).
- Crie um wrapper para cada método que executa uma consulta ao banco de dados SQL.
Como resultado, obtemos um decorador que permite o perfil de todas as consultas no banco de dados. As vantagens dessa abordagem são inegáveis:
- Mantém a limpeza do código do componente de acesso ao banco de dados núcleo.
- Cada decorador implementa um único requisito. Devido a isso, sua facilidade de implementação é alcançada.
- Devido à composição dos decoradores, obtemos um modelo extensível que se adapta facilmente às nossas necessidades.
- O desempenho é zero no modo de produção devido a um desligamento simples do criador de perfil.
Portanto, por exemplo, você pode implementar os seguintes tipos de decoradores:
- Batimento cardíaco Efetuando ping em um banco de dados para manter viva uma conexão com ele.
- Profiler. A saída do corpo da solicitação e seu tempo de execução.
- Sniffer. Coleção de métricas de banco de dados.
- Clonar Clonando o banco de dados original para fins de depuração.
Como regra, ao implementar ricos decoradores, a implementação de todos os métodos não é necessária: basta delegar métodos não implementados a um objeto interno.
Suponha que precisamos implementar um criador de logs avançado para rastrear consultas DML para um banco de dados (para rastrear consultas INSERT / UPDATE / DELETE). Nesse caso, não precisamos implementar toda a interface do banco de dados - apenas sobreponha apenas o método 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 { ... }
Assim, vemos que criar até um rico decorador na língua golang não é particularmente difícil.
Método de modelo
Método de modelo (método de modelo Eng.) - um padrão de design comportamental que define a base do algoritmo e permite que os herdeiros redefinam algumas etapas do algoritmo sem alterar sua estrutura como um todo.
A linguagem golang suporta o paradigma OOP, portanto, este modelo não pode ser implementado em sua forma pura. No entanto, nada nos impede de improvisar construtores usando funções adequadas.
Suponha que precisamos definir um método de modelo com a seguinte assinatura:
func Method(s string) error
Ao declarar, basta usar um campo de um tipo funcional. Para a conveniência de trabalhar com ele, podemos usar a função wrapper para complementar a chamada com o parâmetro ausente e criar uma instância específica, a função construtora correspondente.
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) }
Como você pode ver no exemplo, a semântica do uso do padrão quase não é diferente da OOP clássica.
Adaptador
O padrão de design “Adaptador” permite usar a interface de uma classe existente como outra interface. Esse modelo geralmente é usado para garantir que algumas classes funcionem com outras sem alterar seu código-fonte.
Em geral, os adaptadores podem servir como funções separadas e interfaces inteiras. Se nas interfaces tudo é mais ou menos claro e previsível, do ponto de vista das funções individuais existem sutilezas.
Suponha que escrevamos algum serviço que tenha alguma API interna:
type MyService interface { Create(ctx context.Context, order int) (id int, err error) }
Se precisarmos fornecer uma API pública com uma interface diferente (digamos, trabalhar com gRPC), podemos simplesmente usar as funções do adaptador que lidam com a conversão da interface. É muito conveniente usar tampas para esse fim.
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) {
A função makeCreateEndpoint possui três etapas padrão:
- valores de decodificação
- chamando um método da API interna do serviço que está sendo implementado
- codificação de valor
Todos os terminais no pacote gokit são construídos com base neste princípio.
Visitante
O modelo "Visitor" é uma maneira de separar o algoritmo da estrutura do objeto em que ele opera. O resultado da separação é a capacidade de adicionar novas operações às estruturas de objetos existentes sem modificá-las. Essa é uma maneira de cumprir o princípio de aberto / fechado.
Considere o padrão de visitante bem conhecido no exemplo de formas geométricas.
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) }
Suponha que desejemos escrever uma estratégia para calcular a distância de um determinado ponto até uma forma especificada.
type DistanceStrategy struct { X, Y float32 } func (s *DistanceStrategy) VisitPoint(p *Point) (interface{}, error) {
Da mesma forma, podemos implementar outras estratégias que precisamos:
- Extensão vertical
- A extensão horizontal do objeto
- Construindo um quadrado mínimo de abrangência (MBR)
- Outras primitivas que precisamos.
Além disso, figuras previamente definidas (Ponto, Linha, Círculo ...) não sabem nada sobre essas estratégias. Seu único conhecimento é limitado à interface GeometryVisitor. Isso permite isolá-los em um pacote separado.
Ao mesmo tempo, enquanto trabalhava em um projeto cartográfico, tive a tarefa de escrever uma função para determinar a distância entre dois objetos geográficos arbitrários. As soluções eram muito diferentes, mas nem todas eram suficientemente eficientes e elegantes. Considerando de alguma forma o padrão Visitor, notei que ele serve para selecionar o método de destino e se assemelha um pouco a uma etapa de recursão separada, que, como você sabe, simplifica a tarefa. Isso me levou a usar o Double Visitor. Imagine minha surpresa quando descobri que essa abordagem não é mencionada na 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) {
Assim, construímos um mecanismo seletivo de dois níveis que, como resultado de seu trabalho, chamará o método apropriado para calcular a distância entre duas primitivas. Só podemos escrever esses métodos e o objetivo é alcançado. É assim que um problema elegantemente não determinístico pode ser reduzido a várias funções elementares.
Conclusão
Apesar do fato de não haver POO clássica no golang, o idioma produz seu próprio dialeto de padrões que se baseiam nos pontos fortes do idioma. Esses padrões seguem o caminho padrão da negação à aceitação universal e se tornam melhores práticas ao longo do tempo.
Se o habrozhiteli respeitado tem alguma opinião sobre padrões, não seja tímido e expresse sua opinião sobre isso.