Vous avez probablement lu un milliard d'explications différentes sur le schéma de visite, et vous vous dites probablement encore "mais quand l'utiliseriez-vous ?".
Traditionnellement, les visiteurs sont utilisés pour mettre en œuvre le test de type sans sacrifier la sécurité de type, à condition que les types soient bien définis et connus à l'avance. Supposons que nous ayons quelques classes comme suit :
abstract class Fruit { }
class Orange : Fruit { }
class Apple : Fruit { }
class Banana : Fruit { }
Et disons que nous créons un Fruit[]
:
var fruits = new Fruit[]
{ new Orange(), new Apple(), new Banana(),
new Banana(), new Banana(), new Orange() };
Je veux diviser la liste en trois listes, chacune contenant des oranges, des pommes ou des bananes. Comment faire ? Eh bien, la liste facile serait un test de type :
List<Orange> oranges = new List<Orange>();
List<Apple> apples = new List<Apple>();
List<Banana> bananas = new List<Banana>();
foreach (Fruit fruit in fruits)
{
if (fruit is Orange)
oranges.Add((Orange)fruit);
else if (fruit is Apple)
apples.Add((Apple)fruit);
else if (fruit is Banana)
bananas.Add((Banana)fruit);
}
Cela fonctionne, mais ce code pose de nombreux problèmes :
- Tout d'abord, il est laid.
- Il n'est pas sûr du point de vue du type, nous ne détectons pas les erreurs de type jusqu'à l'exécution.
- Il n'est pas possible de le maintenir. Si nous ajoutons une nouvelle instance dérivée de Fruit, nous devons effectuer une recherche globale pour chaque endroit qui effectue un test de type de fruit, sinon nous risquons de manquer des types.
Le modèle du visiteur résout le problème de manière élégante. Commencez par modifier notre classe de base Fruit :
interface IFruitVisitor
{
void Visit(Orange fruit);
void Visit(Apple fruit);
void Visit(Banana fruit);
}
abstract class Fruit { public abstract void Accept(IFruitVisitor visitor); }
class Orange : Fruit { public override void Accept(IFruitVisitor visitor) { visitor.Visit(this); } }
class Apple : Fruit { public override void Accept(IFruitVisitor visitor) { visitor.Visit(this); } }
class Banana : Fruit { public override void Accept(IFruitVisitor visitor) { visitor.Visit(this); } }
On a l'impression de copier-coller du code, mais il faut noter que les classes dérivées appellent toutes des surcharges différentes (la fonction Apple
appels Visit(Apple)
, le Banana
appels Visit(Banana)
et ainsi de suite).
Mettre en œuvre le visiteur :
class FruitPartitioner : IFruitVisitor
{
public List<Orange> Oranges { get; private set; }
public List<Apple> Apples { get; private set; }
public List<Banana> Bananas { get; private set; }
public FruitPartitioner()
{
Oranges = new List<Orange>();
Apples = new List<Apple>();
Bananas = new List<Banana>();
}
public void Visit(Orange fruit) { Oranges.Add(fruit); }
public void Visit(Apple fruit) { Apples.Add(fruit); }
public void Visit(Banana fruit) { Bananas.Add(fruit); }
}
Vous pouvez désormais répartir vos fruits sans test de type :
FruitPartitioner partitioner = new FruitPartitioner();
foreach (Fruit fruit in fruits)
{
fruit.Accept(partitioner);
}
Console.WriteLine("Oranges.Count: {0}", partitioner.Oranges.Count);
Console.WriteLine("Apples.Count: {0}", partitioner.Apples.Count);
Console.WriteLine("Bananas.Count: {0}", partitioner.Bananas.Count);
Cela présente les avantages suivants
- Un code relativement propre et facile à lire.
- Sécurité de type, les erreurs de type sont détectées au moment de la compilation.
- La maintenabilité. Si j'ajoute ou supprime une classe Fruit concrète, je peux modifier mon interface IFruitVisitor pour gérer le type en conséquence, et le compilateur trouvera immédiatement tous les endroits où nous implémentons l'interface afin que nous puissions apporter les modifications appropriées.
Cela dit, les visiteurs sont généralement exagérés et ont tendance à compliquer considérablement les API, et il peut être très fastidieux de définir un nouveau visiteur pour chaque nouveau type de comportement.
En général, des modèles plus simples comme l'héritage devraient être utilisés à la place des visiteurs. Par exemple, en principe, je pourrais écrire une classe comme :
class FruitPricer : IFruitVisitor
{
public double Price { get; private set; }
public void Visit(Orange fruit) { Price = 0.69; }
public void Visit(Apple fruit) { Price = 0.89; }
public void Visit(Banana fruit) { Price = 1.11; }
}
Cela fonctionne, mais quel est l'avantage de cette modification triviale ?
abstract class Fruit
{
public abstract void Accept(IFruitVisitor visitor);
public abstract double Price { get; }
}
Vous devez donc utiliser les visiteurs lorsque les conditions suivantes sont réunies :
-
Vous disposez d'un ensemble de classes bien définies et connues qui seront visitées.
-
Les opérations sur ces classes ne sont pas bien définies ou connues à l'avance. Par exemple, si quelqu'un consomme votre API et que vous voulez donner aux consommateurs un moyen d'ajouter de nouvelles fonctionnalités ad hoc aux objets. Ils constituent également un moyen pratique d'étendre les classes scellées avec des fonctionnalités ad hoc.
-
Vous effectuez des opérations sur une classe d'objets et souhaitez éviter les tests de type à l'exécution. C'est généralement le cas lorsque vous traversez une hiérarchie d'objets disparates ayant des propriétés différentes.
N'utilisez pas les visiteurs lorsque :
-
Vous prenez en charge des opérations sur une classe d'objets dont les types dérivés ne sont pas connus à l'avance.
-
Les opérations sur les objets sont bien définies à l'avance, en particulier si elles peuvent être héritées d'une classe de base ou définies dans une interface.
-
Il est plus facile pour les clients d'ajouter de nouvelles fonctionnalités aux classes en utilisant l'héritage.
-
Vous parcourez une hiérarchie d'objets ayant les mêmes propriétés ou la même interface.
-
Vous souhaitez une API relativement simple.
6 votes
Avez-vous consulté Wikipédia sur le sujet ? fr.wikipedia.org/wiki/Visitor_pattern Quelles sont les parties qui ne sont pas claires ? Ou cherchez-vous des exemples concrets ?