Visitor
Publié le 31 Mars 2025Visitor (visiteur en français) est un design pattern comportemental qui permet de séparer un algorithme de la structure de données sur laquelle il opère. Il est très utile pour ajouter de nouvelles fonctionnalités à un objet sans en modifier la structure.
Principe
Exemples d’utilisation du pattern Visitor
Génération de contenus dynamiques (HTML, JSON, etc..)
Système de validation des données
Points d’attentions
public function render($element) {
$method = 'toHTML';
if (method_exists($element, $method)) {
return $element->$method();
}
throw new Exception("Méthode introuvable");
}
class Element {
public function __call($name, $arguments) {
if ($name === 'renderHTML') {
return "Contenu générique";
}
}
}
$element = new Element();
echo $element->renderHTML(); // Appelle dynamiquement la méthode
Implémenter un Visitor
Exemple d'utilisation de Visitor en Go
package fieldValidator
// Visitable définit l'interface que chaque champ doit implémenter
type Visitable interface {
Accept(Visitor)
}
package fieldValidator
type Visitor interface {
VisitTextField(*TextField)
VisitEmailField(*EmailField)
VisitNumberField(*NumberField)
}
package fieldValidator
type TextField struct {
Value string
MinLength int
}
func (t *TextField) Accept(v Visitor) {
v.VisitTextField(t)
}
type EmailField struct {
Email string
}
func (e *EmailField) Accept(v Visitor) {
v.VisitEmailField(e)
}
type NumberField struct {
Number int
Min int
Max int
}
func (n *NumberField) Accept(v Visitor) {
v.VisitNumberField(n)
}
package fieldValidator
import (
"fmt"
"regexp"
)
type ValidationVisitor struct {
Errors []string
}
func (v *ValidationVisitor) VisitTextField(t *TextField) {
if len(t.Value) < t.MinLength {
v.Errors = append(v.Errors, fmt.Sprintf("Le texte doit contenir au moins %d caractères.", t.MinLength))
}
}
func (v *ValidationVisitor) VisitEmailField(e *EmailField) {
matched, _ := regexp.MatchString(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`, e.Email)
if !matched {
v.Errors = append(v.Errors, "L'email n'est pas valide.")
}
}
func (v *ValidationVisitor) VisitNumberField(n *NumberField) {
if n.Number < n.Min || n.Number > n.Max {
v.Errors = append(v.Errors, fmt.Sprintf("Le nombre doit être compris entre %d et %d.", n.Min, n.Max))
}
}
package main
import (
"fmt"
"designPatterns/visitor/fieldValidator"
)
func main() {
fields := []fieldValidator.Visitable{
&fieldValidator.TextField{Value: "Hi", MinLength: 5},
&fieldValidator.EmailField{Email: "invalid-email"},
&fieldValidator.NumberField{Number: 150, Min: 1, Max: 100},
}
validator := &fieldValidator.ValidationVisitor{}
for _, field := range fields {
field.Accept(validator)
}
if len(validator.Errors) > 0 {
fmt.Println("Erreurs de validation :")
for _, err := range validator.Errors {
fmt.Println("-", err)
}
} else {
fmt.Println("Toutes les données sont valides !")
}
}
namespace Practice\DesignPatterns\Visitor;
interface VisitableInterface
{
public function accept(VisitorInterface $visitor);
}
namespace Practice\DesignPatterns\Visitor;
interface VisitorInterface
{
public function visitTextField(TextField $text);
public function visitEmailField(EmailField $email);
public function visitNumberField(NumberField $number);
}
namespace Practice\DesignPatterns\Visitor;
class TextField implements VisitableInterface
{
public string $value;
public int $minLength;
public function __construct(string $value, int $minLength)
{
$this->value = $value;
$this->minLength = $minLength;
}
public function accept(VisitorInterface $visitor)
{
$visitor->visitTextField($this);
}
}
namespace Practice\DesignPatterns\Visitor;
class EmailField implements VisitableInterface
{
public string $email;
public function __construct(string $email)
{
$this->email = $email;
}
public function accept(VisitorInterface $visitor)
{
$visitor->visitEmailField($this);
}
}
namespace Practice\DesignPatterns\Visitor;
class NumberField implements VisitableInterface
{
public int $number;
public int $min;
public int $max;
public function __construct(int $number, int $min, int $max)
{
$this->number = $number;
$this->min = $min;
$this->max = $max;
}
public function accept(VisitorInterface $visitor)
{
$visitor->visitNumberField($this);
}
}
namespace Practice\DesignPatterns\Visitor;
class ValidationVisitor implements VisitorInterface
{
public array $errors = [];
public function visitTextField(TextField $text)
{
if (strlen($text->value) < $text->minLength) {
$this->errors[] = "Le texte doit contenir au moins {$text->minLength} caractères.";
}
}
public function visitEmailField(EmailField $email)
{
if (!filter_var($email->email, FILTER_VALIDATE_EMAIL)) {
$this->errors[] = "L'email n'est pas valide.";
}
}
public function visitNumberField(NumberField $number)
{
if ($number->number < $number->min || $number->number > $number->max) {
$this->errors[] = "Le nombre doit être compris entre {$number->min} et {$number->max}.";
}
}
}
declare(strict_types=1);
use Practice\DesignPatterns\Visitor\EmailField;
use Practice\DesignPatterns\Visitor\NumberField;
use Practice\DesignPatterns\Visitor\TextField;
use Practice\DesignPatterns\Visitor\ValidationVisitor;
$fields = [
new TextField("Hi", 5),
new EmailField("invalid-email"),
new NumberField(150, 1, 100),
];
$validator = new ValidationVisitor();
foreach ($fields as $field) {
$field->accept($validator);
}
if (!empty($validator->errors)) {
echo "Erreurs de validation :\n";
foreach ($validator->errors as $error) {
echo "- $error\n";
}
} else {
echo "Toutes les données sont valides !\n";
}
Bonus
namespace Practice\DesignPatterns\Visitor;
interface VisitableInterface
{
public function accept(VisitorInterface $visitor);
}
namespace Practice\DesignPatterns\Visitor;
interface VisitorInterface
{
public function visit(Field $field);
}
namespace Practice\DesignPatterns\Visitor;
class Field implements VisitableInterface
{
public string $name;
public mixed $value;
public function __construct(string $name, mixed $value)
{
$this->name = $name;
$this->value = $value;
}
public function accept(VisitorInterface $visitor)
{
$visitor->visit($this);
}
}
namespace Practice\DesignPatterns\Visitor;
class ValidationVisitor implements VisitorInterface
{
private array $rules = [];
public array $errors = [];
public function __construct()
{
$this->rules = [
'text' => fn($value) => strlen($value) >= 5 ?: "Le texte doit contenir au moins 5 caractères.",
'email' => fn($value) => filter_var($value, FILTER_VALIDATE_EMAIL) ?: "L'email n'est pas valide.",
'number' => fn($value) => ($value >= 1 && $value <= 100) ?: "Le nombre doit être entre 1 et 100.",
];
}
public function visit(Field $field)
{
$type = $field->name;
if (isset($this->rules[$type])) {
$result = $this->rules[$type]($field->value);
if ($result !== true) {
$this->errors[] = $result;
}
}
}
}
declare(strict_types=1);
use Practice\DesignPatterns\Visitor\Field;
use Practice\DesignPatterns\Visitor\ValidationVisitor;
$fields = [
new Field('text', "Hi"), // Trop court
new Field('email', "invalid-email"), // Invalide
new Field('number', 150), // Hors limite
];
$validator = new ValidationVisitor();
foreach ($fields as $field) {
$field->accept($validator);
}
if (!empty($validator->errors)) {
echo "Erreurs de validation :\n";
foreach ($validator->errors as $error) {
echo "- $error\n";
}
} else {
echo "Toutes les données sont valides !\n";
}