Visitor Pattern
- Type : Comportemental
- Difficulté : 8/10
- Définition succincte : Le Visitor Pattern est un design pattern comportemental qui permet de séparer l'algorithme de la structure de données sur laquelle il opère. Ce pattern permet d'ajouter de nouvelles opérations à des objets sans modifier leur structure. Il est particulièrement utile lorsque tu veux effectuer des actions spécifiques sur une collection d'objets de différentes classes, tout en maintenant l'encapsulation.
Objectif du Visitor Pattern
L'objectif du Visitor Pattern est de permettre d'ajouter de nouvelles opérations à une hiérarchie d'objets sans modifier ces objets. En isolant les comportements dans une classe Visitor, tu peux ajouter ou modifier des comportements spécifiques à ces objets sans avoir besoin de toucher leur structure interne. Cela rend le code plus modulaire et extensible.
Structure du Visitor Pattern
- Visitor : Interface ou classe abstraite qui déclare une méthode pour chaque type d'élément que le visiteur peut traiter.
- ConcreteVisitor : Implémentation concrète du Visitor qui définit les actions à effectuer sur chaque type d'élément.
- Element : Interface ou classe abstraite qui déclare une méthode
accept()qui prend un Visitor en paramètre. - ConcreteElement : Classes concrètes qui implémentent
accept()pour permettre au Visitor d'interagir avec elles.
Implémentation avec Laravel
Imaginons que tu développes un système de facturation dans Laravel, où différents types de factures doivent être traités. Ces factures peuvent être des factures de produits, des factures de services, ou des factures de sous-traitance. Tu souhaites appliquer différents types de calculs (comme les taxes, les remises ou les commissions) à ces différentes factures sans altérer leur structure interne. Le Visitor Pattern te permet d'encapsuler ces différents comportements dans des visiteurs distincts.
a) Créer l'interface Invoice (Element)
Les factures (éléments) doivent implémenter une méthode accept() qui permettra au Visitor d'interagir avec elles.
// app/Contracts/Invoice.php
namespace App\Contracts;
use App\Contracts\InvoiceVisitor;
interface Invoice
{
public function accept(InvoiceVisitor $visitor): void;
}
b) Implémenter les différentes factures concrètes (ConcreteElement)
Chaque type de facture implémente l'interface Invoice et sa méthode accept(), qui permet de traiter chaque type de facture avec un visiteur.
1. Facture de produit (ProductInvoice)
// app/Models/ProductInvoice.php
namespace App\Models;
use App\Contracts\Invoice;
use App\Contracts\InvoiceVisitor;
class ProductInvoice implements Invoice
{
public $amount;
public function __construct(float $amount)
{
$this->amount = $amount;
}
public function accept(InvoiceVisitor $visitor): void
{
$visitor->visitProductInvoice($this);
}
}
2. Facture de service (ServiceInvoice)
// app/Models/ServiceInvoice.php
namespace App\Models;
use App\Contracts\Invoice;
use App\Contracts\InvoiceVisitor;
class ServiceInvoice implements Invoice
{
public $amount;
public function __construct(float $amount)
{
$this->amount = $amount;
}
public function accept(InvoiceVisitor $visitor): void
{
$visitor->visitServiceInvoice($this);
}
}
3. Facture de sous-traitance (SubcontractInvoice)
// app/Models/SubcontractInvoice.php
namespace App\Models;
use App\Contracts\Invoice;
use App\Contracts\InvoiceVisitor;
class SubcontractInvoice implements Invoice
{
public $amount;
public function __construct(float $amount)
{
$this->amount = $amount;
}
public function accept(InvoiceVisitor $visitor): void
{
$visitor->visitSubcontractInvoice($this);
}
}
c) Créer l'interface InvoiceVisitor (Visitor)
L'interface InvoiceVisitor définit les méthodes à implémenter pour chaque type de facture que le visiteur va traiter.
// app/Contracts/InvoiceVisitor.php
namespace App\Contracts;
use App\Models\ProductInvoice;
use App\Models\ServiceInvoice;
use App\Models\SubcontractInvoice;
interface InvoiceVisitor
{
public function visitProductInvoice(ProductInvoice $invoice): void;
public function visitServiceInvoice(ServiceInvoice $invoice): void;
public function visitSubcontractInvoice(SubcontractInvoice $invoice): void;
}
d) Implémenter des visiteurs concrets (ConcreteVisitor)
Chaque visiteur effectue un calcul ou une opération spécifique sur chaque type de facture. Voici deux exemples de visiteurs concrets : un pour calculer les taxes, et un autre pour appliquer des remises.
1. Visiteur pour le calcul des taxes (TaxVisitor)
// app/Services/TaxVisitor.php
namespace App\Services;
use App\Contracts\InvoiceVisitor;
use App\Models\ProductInvoice;
use App\Models\ServiceInvoice;
use App\Models\SubcontractInvoice;
class TaxVisitor implements InvoiceVisitor
{
public function visitProductInvoice(ProductInvoice $invoice): void
{
$tax = $invoice->amount * 0.2; // 20% de taxe
echo "Tax for Product Invoice: {$tax}" . PHP_EOL;
}
public function visitServiceInvoice(ServiceInvoice $invoice): void
{
$tax = $invoice->amount * 0.15; // 15% de taxe
echo "Tax for Service Invoice: {$tax}" . PHP_EOL;
}
public function visitSubcontractInvoice(SubcontractInvoice $invoice): void
{
$tax = $invoice->amount * 0.25; // 25% de taxe
echo "Tax for Subcontract Invoice: {$tax}" . PHP_EOL;
}
}
2. Visiteur pour les remises (DiscountVisitor)
// app/Services/DiscountVisitor.php
namespace App\Services;
use App\Contracts\InvoiceVisitor;
use App\Models\ProductInvoice;
use App\Models\ServiceInvoice;
use App\Models\SubcontractInvoice;
class DiscountVisitor implements InvoiceVisitor
{
public function visitProductInvoice(ProductInvoice $invoice): void
{
$discount = $invoice->amount * 0.05; // 5% de remise
echo "Discount for Product Invoice: {$discount}" . PHP_EOL;
}
public function visitServiceInvoice(ServiceInvoice $invoice): void
{
$discount = $invoice->amount * 0.10; // 10% de remise
echo "Discount for Service Invoice: {$discount}" . PHP_EOL;
}
public function visitSubcontractInvoice(SubcontractInvoice $invoice): void
{
$discount = $invoice->amount * 0.08; // 8% de remise
echo "Discount for Subcontract Invoice: {$discount}" . PHP_EOL;
}
}
e) Utilisation du Visitor Pattern dans une commande CLI
Nous allons maintenant créer une commande CLI pour appliquer ces visiteurs aux différentes factures.
// app/Console/Commands/ProcessInvoices.php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\ProductInvoice;
use App\Models\ServiceInvoice;
use App\Models\SubcontractInvoice;
use App\Services\TaxVisitor;
use App\Services\DiscountVisitor;
class ProcessInvoices extends Command
{
protected $signature = 'invoices:process';
protected $description = 'Appliquer des opérations aux factures via des visiteurs';
public function handle()
{
// Créer des factures
$productInvoice = new ProductInvoice(1000);
$serviceInvoice = new ServiceInvoice(2000);
$subcontractInvoice = new SubcontractInvoice(1500);
// Créer les visiteurs
$taxVisitor = new TaxVisitor();
$discountVisitor = new DiscountVisitor();
// Appliquer les visiteurs aux factures
$this->info("Applying TaxVisitor:");
$productInvoice->accept($taxVisitor);
$serviceInvoice->accept($taxVisitor);
$subcontractInvoice->accept($taxVisitor);
$this->info("\nApplying DiscountVisitor:");
$productInvoice->accept($discountVisitor);
$serviceInvoice->accept($discountVisitor);
$subcontractInvoice->accept($discountVisitor);
}
}
f) Exécution de la commande
Tu peux exécuter cette commande pour voir comment les visiteurs appliquent des opérations spécifiques aux différentes factures.
php artisan invoices:process
Résultat attendu :
Applying TaxVisitor:
Tax for Product Invoice: 200
Tax for Service Invoice: 300
Tax for Subcontract Invoice: 375
Applying DiscountVisitor:
Discount for Product Invoice: 50
Discount for Service Invoice: 200
Discount for Subcontract Invoice: 120
Avantages du Visitor Pattern
Extension facile : Tu peux ajouter de nouvelles opérations (nouveaux
visiteurs) sans modifier les classes des objets sur lesquels elles opèrent.
Séparation des responsabilités : Le comportement est séparé des objets eux-mêmes, ce qui permet de respecter le principe de responsabilité unique.
Flexibilité : Ce pattern permet d'ajouter de nouvelles fonctionnalités à des objets sans toucher à leur code source.
Inconvénients du Visitor Pattern
- Couplage : Il y a un couplage entre le Visitor et les classes spécifiques de ConcreteElement, car le Visitor doit connaître toutes les classes d'éléments.
- Maintenance : Si la hiérarchie d'objets évolue fréquemment, il peut être fastidieux de maintenir les méthodes du Visitor pour chaque nouveau type d'élément.
Conclusion
Le Visitor Pattern est particulièrement utile lorsque tu souhaites appliquer plusieurs opérations différentes à une hiérarchie d'objets sans modifier ces objets eux-mêmes. En Laravel, ce pattern peut être utilisé dans des situations complexes où plusieurs comportements doivent être appliqués à différents types d'objets, comme des calculs financiers ou des analyses de données. Il te permet de rendre le système plus modulaire et de séparer les comportements des objets concernés.