SOLDES de printemps
Visiteur

Visiteur en Go

Le Visiteur est un patron de conception comportemental qui permet d’ajouter de nouveaux comportements à une hiérarchie de classes sans modifier l’existant.

Découvrez pourquoi les visiteurs ne peuvent pas être remplacés par la surcharge de méthodes dans notre article Visiteur et double répartition.

Exemple conceptuel

Le patron de conception visiteur vous permet d’ajouter des comportements à une struct sans vraiment modifier la structure. Disons que vous vous occupez de maintenir une bibliothèque qui a différentes structs de formes comme :

  • Carré
  • Cercle
  • Triangle

Chaque struct forme ci-dessus implémente l’interface commune Forme.

Dès que vos collègues ont commencé à utiliser votre super bibliothèque, vous avez été enseveli sous les demandes de fonctionnalités. Penchons-nous sur l’une des plus simples : une équipe vous a demandé de rajouter le comportement getArea aux structs de formes.

Nous pouvons résoudre ce problème de plusieurs manières :

La première option qui vient à l’esprit est d’ajouter la méthode getArea directement dans l’interface forme et de l’implémenter dans chaque struct de forme. À première vue, cela semble être la meilleure solution, mais elle a un certain coût. En tant que responsable de la bibliothèque, vous ne voulez pas risquer d’endommager votre précieux code chaque fois que quelqu’un réclame un nouveau comportement. Mais vous voulez tout de même que les autres équipes aient la possibilité d’étendre votre code.

La deuxième option est que l’équipe qui demande la fonctionnalité l’implémente elle-même. Ce n’est pas toujours possible, car le comportement peut dépendre de code privé.

La troisième option est de résoudre le problème en utilisant le patron de conception visiteur. Nous commençons par définir une interface Visiteur comme ci-dessous :

type visitor interface {
   visitForSquare(square)
   visitForCircle(circle)
   visitForTriangle(triangle)
}

Les fonctions visitForSquare(square), visitForCircle(circle), visitForTriangle(triangle) nous permettent d’ajouter des fonctionnalités respectivement aux carrés, cercles et triangles.

Vous vous demandez pourquoi on ne peut pas avoir une seule méthode visit(shape) dans l’interface visiteur ? La raison est tout simplement que le Go ne gère pas la surcharge de méthodes. On ne peut donc pas définir des méthodes avec un même nom et des paramètres différents.

La deuxième partie du travail consiste à ajouter la méthode accept à l’interface Forme.

func accept(v visitor)

Toutes les structs de formes doivent définir cette méthode comme ceci :

func (obj *square) accept(v visitor){
    v.visitForSquare(obj)
}

Attendez une minute, n’ai-je pas dit que nous ne voulions pas modifier nos structs de formes existantes ? Malheureusement, lorsque l’on utilise le visiteur, nous devons forcément modifier nos structs de formes, mais cette modification ne devra être faite qu’une seule fois.

Dans le cas où nous voulons ajouter d’autres comportements comme getNumSides ou getMiddleCoordinates, nous utiliserons la même fonction accept(v visitor) sans apporter d’autres modifications aux structs de formes.

Au final, nous ne modifierons qu’une seule fois la struct forme, et toutes les futures demandes pour les nouveaux comportements seront gérées en utilisant cette même fonction accept. Si une équipe demande le comportement getArea, nous pouvons simplement définir une implémentation concrète de l’interface visiteur et y écrire la logique de calcul de la surface.

shape.go: Élément

package main

type Shape interface {
	getType() string
	accept(Visitor)
}

square.go: Élément concret

package main

type Square struct {
	side int
}

func (s *Square) accept(v Visitor) {
	v.visitForSquare(s)
}

func (s *Square) getType() string {
	return "Square"
}

circle.go: Élément concret

package main

type Circle struct {
	radius int
}

func (c *Circle) accept(v Visitor) {
	v.visitForCircle(c)
}

func (c *Circle) getType() string {
	return "Circle"
}

rectangle.go: Élément concret

package main

type Rectangle struct {
	l int
	b int
}

func (t *Rectangle) accept(v Visitor) {
	v.visitForrectangle(t)
}

func (t *Rectangle) getType() string {
	return "rectangle"
}

visitor.go: Visiteur

package main

type Visitor interface {
	visitForSquare(*Square)
	visitForCircle(*Circle)
	visitForrectangle(*Rectangle)
}

areaCalculator.go: Visiteur concret

package main

import (
	"fmt"
)

type AreaCalculator struct {
	area int
}

func (a *AreaCalculator) visitForSquare(s *Square) {
	// Calculate area for square.
	// Then assign in to the area instance variable.
	fmt.Println("Calculating area for square")
}

func (a *AreaCalculator) visitForCircle(s *Circle) {
	fmt.Println("Calculating area for circle")
}
func (a *AreaCalculator) visitForrectangle(s *Rectangle) {
	fmt.Println("Calculating area for rectangle")
}

middleCoordinates.go: Visiteur concret

package main

import "fmt"

type MiddleCoordinates struct {
	x int
	y int
}

func (a *MiddleCoordinates) visitForSquare(s *Square) {
	// Calculate middle point coordinates for square.
	// Then assign in to the x and y instance variable.
	fmt.Println("Calculating middle point coordinates for square")
}

func (a *MiddleCoordinates) visitForCircle(c *Circle) {
	fmt.Println("Calculating middle point coordinates for circle")
}
func (a *MiddleCoordinates) visitForrectangle(t *Rectangle) {
	fmt.Println("Calculating middle point coordinates for rectangle")
}

main.go: Code client

package main

import "fmt"

func main() {
	square := &Square{side: 2}
	circle := &Circle{radius: 3}
	rectangle := &Rectangle{l: 2, b: 3}

	areaCalculator := &AreaCalculator{}

	square.accept(areaCalculator)
	circle.accept(areaCalculator)
	rectangle.accept(areaCalculator)

	fmt.Println()
	middleCoordinates := &MiddleCoordinates{}
	square.accept(middleCoordinates)
	circle.accept(middleCoordinates)
	rectangle.accept(middleCoordinates)
}

output.txt: Résultat de l’exécution

Calculating area for square
Calculating area for circle
Calculating area for rectangle

Calculating middle point coordinates for square
Calculating middle point coordinates for circle
Calculating middle point coordinates for rectangle

Visiteur dans les autres langues

Visiteur en C# Visiteur en C++ Visiteur en Java Visiteur en PHP Visiteur en Python Visiteur en Ruby Visiteur en Rust Visiteur en Swift Visiteur en TypeScript