Des inconnus familiers ou encore une fois sur l'utilisation de modèles de conception

Sur le thème des modèles de conception, des tonnes d'articles ont été écrits et de nombreux livres ont été publiés. Cependant, ce sujet ne cesse d'être pertinent, car les modèles nous permettent d'utiliser des solutions prêtes à l'emploi et éprouvées, ce qui nous permet de réduire le temps de développement du projet en améliorant la qualité du code et en réduisant les dettes techniques.


Depuis l'avènement des modèles de conception, il existe de nouveaux exemples de leur utilisation efficace. Et c'est merveilleux. Cependant, il y avait une mouche dans la pommade: chaque langue a ses propres spécificités. Et golang - et plus encore (il n'a même pas de modèle OOP classique). Par conséquent, il existe des variations des modèles, en fonction des langages de programmation individuels. Dans cet article, je voudrais aborder le sujet des modèles de conception en relation avec le golang.


Décorateur


Le modèle Decorator vous permet de connecter un comportement supplémentaire à l'objet (statiquement ou dynamiquement) sans affecter le comportement d'autres objets de la même classe. Un modèle est souvent utilisé pour adhérer au principe de responsabilité unique, car il vous permet de répartir les fonctionnalités entre les classes pour résoudre des problèmes spécifiques.

Le modèle DECORATOR bien connu est largement utilisé dans de nombreux langages de programmation. Ainsi, dans golang, tout middleware est construit sur sa base. Par exemple, le profilage des requêtes peut ressembler à ceci:


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

Dans ce cas, l'interface du décorateur est la seule fonction. En règle générale, cela doit être recherché. Cependant, un décorateur avec une interface plus large peut parfois être utile. Par exemple, envisagez l'accès à une base de données (package base de données / sql). Supposons que nous ayons besoin de faire le même profilage des requêtes de base de données. Dans ce cas, nous avons besoin de:


  • Au lieu d'interagir directement avec la base de données via un pointeur, nous devons passer à l'interaction via une interface (pour séparer le comportement de l'implémentation).
  • Créez un wrapper pour chaque méthode qui exécute une requête de base de données SQL.

En conséquence, nous obtenons un décorateur qui vous permet de profiler toutes les requêtes dans la base de données. Les avantages de cette approche sont indéniables:


  • Maintient la propreté du code du composant d'accès à la base de données principale.
  • Chaque décorateur met en œuvre une seule exigence. De ce fait, sa facilité de mise en œuvre est atteinte.
  • En raison de la composition des décorateurs, nous obtenons un modèle extensible qui s'adapte facilement à nos besoins.
  • Nous n'obtenons aucune surcharge de performances en mode production en raison d'un simple arrêt du profileur.

Ainsi, par exemple, vous pouvez implémenter les types de décorateurs suivants:


  • Battement de coeur Pinging d'une base de données pour maintenir en vie une connexion à celle-ci.
  • Profiler. La sortie du corps de la demande et de son temps d'exécution.
  • Renifleur. Collection de métriques de base de données.
  • Clone Clonage de la base de données d'origine à des fins de débogage.

En règle générale, lors de l'implémentation de décorateurs riches, l'implémentation de toutes les méthodes n'est pas requise: il suffit de déléguer des méthodes non implémentées à un objet interne.


Supposons que nous devons implémenter un enregistreur avancé pour suivre les requêtes DML d'une base de données (pour suivre les requêtes INSERT / UPDATE / DELETE). Dans ce cas, nous n'avons pas besoin d'implémenter l'intégralité de l'interface de la base de données - chevauchons uniquement la méthode 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 { ... } 

Ainsi, nous voyons que créer même un riche décorateur dans la langue golang n'est pas particulièrement difficile.


Méthode de modèle


Méthode de modèle (méthode de modèle Eng) - un modèle de conception comportementale qui définit la base de l'algorithme et permet aux héritiers de redéfinir certaines étapes de l'algorithme sans changer sa structure dans son ensemble.

Le langage golang prend en charge le paradigme OOP, ce modèle ne peut donc pas être implémenté dans sa forme pure. Cependant, rien ne nous empêche d'improviser des constructeurs en utilisant des fonctions adaptées.


Supposons que nous devons définir une méthode de modèle avec la signature suivante:


 func Method(s string) error 

Lors de la déclaration, il nous suffit d'utiliser un champ de type fonctionnel. Pour plus de commodité, nous pouvons utiliser la fonction wrapper pour compléter l'appel avec le paramètre manquant et pour créer une instance spécifique, la fonction constructeur correspondante.


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

Comme vous pouvez le voir dans l'exemple, la sémantique d'utilisation du modèle n'est presque pas différente de la POO classique.


Adaptateur


Le modèle de conception «Adaptateur» vous permet d'utiliser l'interface d'une classe existante comme une autre interface. Ce modèle est souvent utilisé pour garantir que certaines classes fonctionnent avec d'autres sans modifier leur code source.

En général, les adaptateurs peuvent servir de fonctions distinctes et d'interfaces entières. Si avec les interfaces tout est plus ou moins clair et prévisible, alors du point de vue des fonctions individuelles il y a des subtilités.


Supposons que nous écrivions un service qui possède une API interne:


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

Si nous devons fournir une API publique avec une interface différente (par exemple pour travailler avec gRPC), nous pouvons simplement utiliser les fonctions d'adaptateur qui traitent de la conversion de l'interface. Il est très pratique d'utiliser des fermetures à cet effet.


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

La fonction makeCreateEndpoint comporte trois étapes standard:


  • valeurs de décodage
  • appel d'une méthode à partir de l'API interne du service implémenté
  • encodage de valeur

Tous les points de terminaison dans le package gokit sont construits sur ce principe.


Visiteur


Le modèle «Visiteur» est un moyen de séparer l'algorithme de la structure de l'objet dans lequel il opère. Le résultat de la séparation est la possibilité d'ajouter de nouvelles opérations aux structures d'objets existantes sans les modifier. C'est une façon de se conformer au principe ouvert / fermé.

Considérez le modèle de visiteur bien connu sur l'exemple des formes géométriques.


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

Supposons que nous voulons écrire une stratégie pour calculer la distance d'un point donné à une forme spécifiée.


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

De même, nous pouvons mettre en œuvre d'autres stratégies dont nous avons besoin:


  • Étendue verticale
  • L'étendue horizontale de l'objet
  • Construire un carré couvrant minimum (MBR)
  • D'autres primitives dont nous avons besoin.

De plus, les figures préalablement définies (Point, Ligne, Cercle ...) ne savent rien de ces stratégies. Leur seule connaissance se limite à l'interface GeometryVisitor. Cela vous permet de les isoler dans un package séparé.


À une certaine époque, alors que je travaillais sur un projet cartographique, j'ai eu la tâche d'écrire une fonction pour déterminer la distance entre deux objets géographiques arbitraires. Les solutions étaient très différentes, mais toutes n'étaient pas assez efficaces et élégantes. Considérant en quelque sorte le modèle Visitor, j'ai remarqué qu'il sert à sélectionner la méthode cible et ressemble un peu à une étape de récursivité distincte, ce qui, comme vous le savez, simplifie la tâche. Cela m'a incité à utiliser Double Visitor. Imaginez ma surprise quand j'ai découvert qu'une telle approche n'est pas du tout mentionnée sur 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}) } 

Ainsi, nous avons construit un mécanisme sélectif à deux niveaux, qui, à la suite de ses travaux, appellera la méthode appropriée pour calculer la distance entre deux primitives. Nous ne pouvons qu'écrire ces méthodes et l'objectif est atteint. C'est ainsi qu'un problème élégamment non déterministe peut être réduit à un certain nombre de fonctions élémentaires.


Conclusion


Malgré le fait qu'il n'y ait pas de POO classique dans le golang, la langue produit son propre dialecte de motifs qui jouent sur les forces de la langue. Ces modèles vont de la manière standard du déni à l'acceptation universelle et deviennent les meilleures pratiques au fil du temps.


Si les habrozhiteli respectés ont des réflexions sur les modèles, n'hésitez pas à exprimer vos pensées à ce sujet.

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


All Articles