وقتی با برنامهنویسی شیگرا در PHP کار میکنیم، معمولاً کلاسها علاوه بر نگهداری داده، وظایف و رفتارهای مختلفی هم دارند. اما گاهی اوقات یک مشکل مهم ایجاد میشود:
فرض کنید در یک پروژه چند نوع Object مختلف داریم و میخواهیم عملیاتهای مختلفی مانند گزارشگیری، خروجی گرفتن، محاسبه مالیات یا تبدیل اطلاعات را روی آنها انجام دهیم.
اگر این عملیاتها را مستقیماً داخل کلاسهای اصلی قرار دهیم، کلاسها به مرور بزرگ و پیچیده میشوند.
اینجاست که Visitor Pattern میتواند مفید باشد.
Visitor یکی از الگوهای طراحی رفتاری یا Behavioral Design Patterns است که به ما اجازه میدهد رفتارهای جدید را از ساختار اصلی Objectها جدا کنیم. در نتیجه میتوانیم عملیات جدیدی به مجموعهای از کلاسها اضافه کنیم، بدون اینکه منطق آن عملیات را داخل خود کلاسها قرار دهیم.
در این مقاله ابتدا مفهوم Visitor Pattern را یاد میگیریم و سپس آن را با PHP از صفر پیادهسازی میکنیم.
Visitor Pattern چیست؟
Visitor Pattern یک الگوی طراحی رفتاری است که اجازه میدهد الگوریتمها و عملیاتها را از Objectهایی که روی آنها اجرا میشوند جدا کنیم.
به زبان ساده، به جای اینکه برای هر کلاس یکسری متد جدید اضافه کنیم، یک کلاس جداگانه به نام Visitor ایجاد میکنیم و عملیات موردنظر را در آن قرار میدهیم.
ساختار کلی تقریباً به شکل زیر است:
Visitor
│
┌──────────┼──────────┐
│ │ │
Product Invoice Customer
│ │ │
accept() accept() accept()
│ │ │
└──────────┼──────────┘
│
Concrete Visitor
در Visitor، هر Element دارای متدی مانند accept() است.
این متد Visitor مناسب را دریافت میکند و Visitor نیز عملیات مربوط به همان نوع Element را اجرا میکند.
این ساختار یکی از بخشهای مهم Visitor Pattern و دلیل استفاده از Double Dispatch است.
# چه مشکلی را با Visitor حل میکنیم؟
فرض کنید در فروشگاه خود سه کلاس داریم:
Product
Invoice
Customer
حالا میخواهیم روی این Objectها عملیاتهای مختلفی انجام دهیم:
- تولید گزارش
- خروجی JSON
- خروجی XML
- محاسبه مالیات
- محاسبه کمیسیون
- تولید گزارش مالی
- Export به فایل
یک راه ساده این است که تمام این متدها را داخل کلاسهای اصلی قرار دهیم.
مثلاً:
class Product
{
public function exportJson()
{
// ...
}
public function exportXml()
{
// ...
}
public function calculateTax()
{
// ...
}
public function generateReport()
{
// ...
}
}
اما مشکل اینجاست که کلاس Product دیگر فقط مسئول مدیریت محصول نیست.
کلاس به مرور تبدیل میشود به یک کلاس بزرگ که مسئولیتهای زیادی دارد.
این موضوع میتواند باعث افزایش پیچیدگی و کاهش Separation of Concerns شود.
Visitor پیشنهاد میکند این رفتارهای جانبی را از کلاسهای اصلی خارج کنیم و در Visitorهای جداگانه قرار دهیم.
# ساختار Visitor Pattern
Visitor Pattern معمولاً از اجزای زیر تشکیل میشود:
1. Visitor
Interface مربوط به Visitor است.
در این Interface برای هر نوع Element یک متد visit تعریف میکنیم.
مثلاً:
interface VisitorInterface
{
public function visitProduct(Product $product): void;
public function visitInvoice(Invoice $invoice): void;
}
2. Concrete Visitor
کلاس واقعیای است که عملیات موردنظر را پیادهسازی میکند.
مثلاً:
class ReportVisitor implements VisitorInterface
{
public function visitProduct(Product $product): void
{
// گزارش محصول
}
public function visitInvoice(Invoice $invoice): void
{
// گزارش فاکتور
}
}
3. Element
Element همان Objectهایی هستند که Visitor روی آنها عملیات انجام میدهد.
مثلاً:
Product
Invoice
4. accept()
هر Element معمولاً متدی به نام accept() دارد که Visitor را دریافت میکند.
مثلاً:
public function accept(VisitorInterface $visitor): void
{
$visitor->visitProduct($this);
}
این accept() مشخص میکند که برای Object فعلی باید کدام متد Visitor اجرا شود.
# پیادهسازی Visitor Pattern در PHP
حالا یک مثال واقعیتر ایجاد کنیم.
فرض کنید سیستم فروشگاهی داریم و دو نوع Object داریم:
Product
Invoice
و میخواهیم روی آنها گزارش تولید کنیم.
مرحله اول: ساخت Visitor Interface
ابتدا Interface مربوط به Visitor را ایجاد میکنیم:
<?php
interface VisitorInterface
{
public function visitProduct(Product $product): void;
public function visitInvoice(Invoice $invoice): void;
}
در اینجا دو متد داریم:
visitProduct()
visitInvoice()
بنابراین هر Visitor باید بتواند عملیات مربوط به Product و Invoice را مشخص کند.
# مرحله دوم: ساخت Interface مربوط به Element
حالا یک Interface برای Elementها ایجاد میکنیم:
<?php
interface VisitableInterface
{
public function accept(VisitorInterface $visitor): void;
}
هر کلاسی که قرار است Visitor بتواند روی آن عملیات انجام دهد، باید این Interface را پیادهسازی کند.
# مرحله سوم: ساخت کلاس Product
<?php
class Product implements VisitableInterface
{
public function __construct(
private string $name,
private float $price
) {
}
public function getName(): string
{
return $this->name;
}
public function getPrice(): float
{
return $this->price;
}
public function accept(VisitorInterface $visitor): void
{
$visitor->visitProduct($this);
}
}
نکته مهم این قسمت متد زیر است:
public function accept(VisitorInterface $visitor): void
{
$visitor->visitProduct($this);
}
Object Product خودش مشخص میکند که باید متد:
visitProduct()
روی Visitor اجرا شود.
# مرحله چهارم: ساخت کلاس Invoice
حالا یک کلاس برای فاکتور ایجاد میکنیم:
<?php
class Invoice implements VisitableInterface
{
public function __construct(
private int $number,
private float $amount
) {
}
public function getNumber(): int
{
return $this->number;
}
public function getAmount(): float
{
return $this->amount;
}
public function accept(VisitorInterface $visitor): void
{
$visitor->visitInvoice($this);
}
}
در اینجا هم:
$visitor->visitInvoice($this);
اجرا میشود.
بنابراین Visitor میفهمد که با یک Invoice مواجه است.
# مرحله پنجم: ساخت Report Visitor
حالا میتوانیم Visitor واقعی خودمان را ایجاد کنیم:
<?php
class ReportVisitor implements VisitorInterface
{
public function visitProduct(Product $product): void
{
echo "Product: {$product->getName()}";
echo " - Price: {$product->getPrice()}";
echo PHP_EOL;
}
public function visitInvoice(Invoice $invoice): void
{
echo "Invoice #{$invoice->getNumber()}";
echo " - Amount: {$invoice->getAmount()}";
echo PHP_EOL;
}
}
حالا منطق گزارشگیری دیگر داخل Product و Invoice نیست.
این منطق در کلاس:
ReportVisitor
قرار گرفته است.
# مرحله ششم: استفاده از Visitor
حالا میتوانیم Objectهای خودمان را ایجاد کنیم:
$product = new Product(
'Laptop',
50000000
);
$invoice = new Invoice(
1001,
75000000
);
سپس Visitor را ایجاد میکنیم:
$visitor = new ReportVisitor();
و Visitor را به Objectها میدهیم:
$product->accept($visitor);
$invoice->accept($visitor);
خروجی چیزی شبیه این خواهد بود:
Product: Laptop - Price: 50000000
Invoice #1001 - Amount: 75000000
# کل مثال در یک فایل PHP
برای اینکه ساختار را راحتتر متوجه شوید، میتوانیم تمام مثال را در یک فایل قرار دهیم:
<?php
interface VisitorInterface
{
public function visitProduct(Product $product): void;
public function visitInvoice(Invoice $invoice): void;
}
interface VisitableInterface
{
public function accept(VisitorInterface $visitor): void;
}
class Product implements VisitableInterface
{
public function __construct(
private string $name,
private float $price
) {
}
public function getName(): string
{
return $this->name;
}
public function getPrice(): float
{
return $this->price;
}
public function accept(VisitorInterface $visitor): void
{
$visitor->visitProduct($this);
}
}
class Invoice implements VisitableInterface
{
public function __construct(
private int $number,
private float $amount
) {
}
public function getNumber(): int
{
return $this->number;
}
public function getAmount(): float
{
return $this->amount;
}
public function accept(VisitorInterface $visitor): void
{
$visitor->visitInvoice($this);
}
}
class ReportVisitor implements VisitorInterface
{
public function visitProduct(Product $product): void
{
echo "Product: {$product->getName()}";
echo " - Price: {$product->getPrice()}";
echo PHP_EOL;
}
public function visitInvoice(Invoice $invoice): void
{
echo "Invoice #{$invoice->getNumber()}";
echo " - Amount: {$invoice->getAmount()}";
echo PHP_EOL;
}
}
$product = new Product(
'Laptop',
50000000
);
$invoice = new Invoice(
1001,
75000000
);
$visitor = new ReportVisitor();
$product->accept($visitor);
$invoice->accept($visitor);
# چرا از accept() استفاده میکنیم؟
شاید در این مرحله این سؤال برای شما ایجاد شود:
چرا مستقیماً این کار را انجام ندهیم؟
$visitor->visitProduct($product);
چرا باید بنویسیم:
$product->accept($visitor);
دلیل اصلی، نحوه تشخیص نوع Concrete Element و چیزی است که معمولاً با عنوان Double Dispatch در Visitor Pattern شناخته میشود.
در این ساختار، خود Element میداند که باید کدام متد Visitor را صدا بزند:
public function accept(VisitorInterface $visitor): void
{
$visitor->visitProduct($this);
}
در حالی که Client لازم نیست جزئیات مربوط به نوع Concrete Element را مدیریت کند.
این ساختار یکی از ویژگیهای اصلی Visitor Pattern است.
# ساخت Visitor دوم
یکی از مزیتهای مهم Visitor این است که میتوانیم عملیات جدیدی اضافه کنیم.
فرض کنید علاوه بر گزارشگیری، میخواهیم قیمت محصولات و مبلغ فاکتورها را برای خروجی JSON آماده کنیم.
میتوانیم Visitor دیگری ایجاد کنیم:
class JsonExportVisitor implements VisitorInterface
{
public function visitProduct(Product $product): void
{
echo json_encode([
'type' => 'product',
'name' => $product->getName(),
'price' => $product->getPrice(),
]) . PHP_EOL;
}
public function visitInvoice(Invoice $invoice): void
{
echo json_encode([
'type' => 'invoice',
'number' => $invoice->getNumber(),
'amount' => $invoice->getAmount(),
]) . PHP_EOL;
}
}
حالا بدون تغییر Product و Invoice میتوانیم Visitor جدید را استفاده کنیم:
$jsonVisitor = new JsonExportVisitor();
$product->accept($jsonVisitor);
$invoice->accept($jsonVisitor);
این دقیقاً یکی از کاربردهای اصلی Visitor است: اضافه کردن رفتار جدید بدون قرار دادن آن رفتار در کلاسهای اصلی.
# یک مثال کاربردیتر: محاسبه مالیات
فرض کنید در فروشگاه میخواهیم مالیات محصولات و فاکتورها را محاسبه کنیم.
به جای اینکه متدهایی مانند:
calculateTax()
را به چندین کلاس مختلف اضافه کنیم، میتوانیم Visitor مخصوص مالیات بسازیم:
class TaxVisitor implements VisitorInterface
{
public function visitProduct(Product $product): void
{
$tax = $product->getPrice() * 0.10;
echo "Product tax: {$tax}" . PHP_EOL;
}
public function visitInvoice(Invoice $invoice): void
{
$tax = $invoice->getAmount() * 0.10;
echo "Invoice tax: {$tax}" . PHP_EOL;
}
}
استفاده:
$taxVisitor = new TaxVisitor();
$product->accept($taxVisitor);
$invoice->accept($taxVisitor);
حالا یک رفتار کاملاً جدید داریم، بدون اینکه مجبور باشیم منطق مالیات را داخل Product یا Invoice قرار دهیم.
# Visitor Pattern در Laravel چه کاربردی دارد؟
Visitor Pattern در Laravel هم میتواند در پروژههای پیچیده کاربرد داشته باشد، هرچند نباید آن را برای هر مسئلهای استفاده کرد.
برای مثال فرض کنید یک سیستم فروشگاهی دارید که Objectهای مختلفی دارد:
Product
Service
Subscription
Invoice
Order
و چند عملیات مختلف روی آنها انجام میشود:
Export
Report
Tax Calculation
Commission
Accounting
Statistics
در چنین شرایطی میتوان عملیاتهای جانبی را در Visitorهای مختلف قرار داد.
برای مثال:
Visitors/
ExportVisitor.php
ReportVisitor.php
TaxVisitor.php
AccountingVisitor.php
CommissionVisitor.php
و ساختار Objectها نیز مسئولیت اصلی خودشان را حفظ میکند.
# آیا باید برای هر پروژه Laravel از Visitor استفاده کنیم؟
خیر.
Visitor Pattern یک ابزار تخصصی است و استفاده بیش از حد از آن میتواند ساختار پروژه را پیچیده کند.
اگر فقط دو کلاس دارید و یک عملیات ساده روی آنها انجام میدهید، احتمالاً Visitor انتخاب مناسبی نیست.
Visitor زمانی ارزش بیشتری پیدا میکند که:
- تعداد Elementها زیاد باشد.
- عملیاتهای مختلفی روی آنها داشته باشید.
- ساختار Elementها نسبتاً ثابت باشد.
- مرتباً رفتارهای جدید به سیستم اضافه شود.
- بخواهید رفتارهای جانبی را از کلاسهای اصلی خارج کنید.
Visitor مخصوصاً در ساختارهای پیچیده مانند Object Tree یا ساختارهای مشابه Composite میتواند کاربرد بیشتری پیدا کند.
# تفاوت Visitor و Strategy چیست؟
این دو Pattern ممکن است در نگاه اول شبیه یکدیگر به نظر برسند، اما هدف متفاوتی دارند.
Strategy
Strategy معمولاً برای انتخاب یک الگوریتم یا روش انجام یک عملیات استفاده میشود.
مثلاً:
PaymentStrategy
├── StripePayment
├── ZarinpalPayment
└── PaypalPayment
در اینجا هدف، انتخاب روش پرداخت است.
Visitor
Visitor بیشتر زمانی استفاده میشود که یک عملیات مشخص باید روی انواع مختلف Object اجرا شود.
مثلاً:
Product
Invoice
Customer
و:
ReportVisitor
ExportVisitor
TaxVisitor
به صورت خلاصه:
Strategy
انتخاب الگوریتم
Visitor
جدا کردن عملیات از ساختار Objectها
# تفاوت Visitor و Template Method
در مقاله قبلی با Template Method Pattern آشنا شدیم.
Template Method معمولاً اسکلت یک الگوریتم را در کلاس پایه تعریف میکند و بعضی مراحل را به کلاسهای فرزند واگذار میکند.
اما Visitor ساختار متفاوتی دارد.
در Visitor:
Element
↓
accept()
↓
Visitor
↓
visitConcreteElement()
هدف اصلی، خارج کردن عملیات از Elementهاست.
بنابراین اگر مسئله شما مربوط به ساخت یک الگوریتم با مراحل ثابت و قابل تغییر در Subclassهاست، Template Method میتواند مناسبتر باشد.
اگر مسئله شما اجرای عملیاتهای مختلف روی یک ساختار Object ثابت است، Visitor میتواند انتخاب مناسبتری باشد.
# مزایای Visitor Pattern
Visitor چند مزیت مهم دارد.
1. جدا کردن رفتار از Object
منطق عملیات در Visitor قرار میگیرد و کلاس اصلی میتواند روی مسئولیت اصلی خودش تمرکز کند.
2. اضافه کردن عملیات جدید
میتوان Visitor جدید ایجاد کرد.
مثلاً:
ReportVisitor
JsonExportVisitor
TaxVisitor
بدون اینکه لازم باشد منطق این عملیاتها را در تمام Elementها قرار دهیم.
3. رعایت بهتر Single Responsibility
به جای اینکه یک کلاس مسئول چندین رفتار مختلف باشد، هر Visitor میتواند یک مسئولیت مشخص داشته باشد.
مثلاً:
TaxVisitor
→ مالیات
ReportVisitor
→ گزارش
ExportVisitor
→ خروجی
4. مناسب برای ساختارهای پیچیده
Visitor میتواند برای اجرای عملیات روی مجموعهای از Objectهای متفاوت یا ساختارهای درختی استفاده شود.
# معایب Visitor Pattern
Visitor با وجود مزایای خود، یک نقطه ضعف مهم دارد.
فرض کنید Interface ما این باشد:
interface VisitorInterface
{
public function visitProduct(Product $product): void;
public function visitInvoice(Invoice $invoice): void;
}
حالا اگر یک Element جدید اضافه کنیم:
Customer
باید Visitor Interface را نیز تغییر دهیم:
public function visitCustomer(Customer $customer): void;
بعد تمام Visitorهای موجود باید این متد را پیادهسازی کنند.
یعنی اگر تعداد Elementها مرتباً تغییر کند، نگهداری Visitor میتواند سخت و پرهزینه شود.
بنابراین یکی از قواعد مهم استفاده از Visitor این است:
> Visitor زمانی مناسبتر است که ساختار Elementها نسبتاً ثابت باشد اما عملیات روی آنها مرتباً تغییر کند.
# چه زمانی از Visitor Pattern استفاده کنیم؟
Visitor انتخاب مناسبی است اگر:
- چندین نوع Object مختلف دارید.
- Objectها ساختار نسبتاً ثابتی دارند.
- عملیاتهای مختلفی روی آنها انجام میدهید.
- میخواهید عملیاتهای جانبی را از کلاسهای اصلی جدا کنید.
- اضافه شدن رفتارهای جدید بیشتر از اضافه شدن نوع جدید Object اتفاق میافتد.
- با ساختارهای پیچیده یا درختی کار میکنید.
منابع آموزشی Pattern نیز Visitor را برای اجرای عملیات روی مجموعهای از Objectهای مختلف و همچنین خارج کردن رفتارهای جانبی از کلاسهای اصلی پیشنهاد میکنند.
# چه زمانی از Visitor استفاده نکنیم؟
اگر پروژه شما بسیار ساده است، Visitor احتمالاً فقط پیچیدگی اضافه ایجاد میکند.
برای مثال اگر فقط چنین چیزی دارید:
$product->calculatePrice();
نیازی نیست برای آن Visitor بسازید.
همچنین اگر تعداد Elementها مرتباً در حال افزایش است، باید با دقت بیشتری Visitor را انتخاب کنید؛ زیرا اضافه شدن هر نوع Element جدید معمولاً نیازمند تغییر Interface Visitor و پیادهسازیهای Concrete Visitor است.
# تست Visitor با PHPUnit
برای تست Visitor میتوانیم یک تست ساده ایجاد کنیم.
مثلاً:
<?php
use PHPUnit\Framework\TestCase;
class ReportVisitorTest extends TestCase
{
public function test_product_can_accept_visitor(): void
{
$product = new Product(
'Laptop',
50000000
);
$visitor = $this->createMock(
VisitorInterface::class
);
$visitor
->expects($this->once())
->method('visitProduct')
->with($product);
$product->accept($visitor);
}
}
در این تست بررسی میکنیم که وقتی:
$product->accept($visitor);
اجرا میشود، متد:
visitProduct()
روی Visitor فراخوانی شود.
# اشتباهات رایج هنگام استفاده از Visitor
اشتباه اول: استفاده برای مسائل ساده
Visitor Pattern پیچیدگی بیشتری نسبت به یک متد معمولی دارد.
پس برای هر عملیات سادهای نباید Visitor ایجاد کنیم.
اشتباه دوم: اضافه کردن Visitor برای هر متد
هدف Visitor این نیست که هر متد ساده را به یک کلاس Visitor منتقل کنیم.
باید یک نیاز واقعی برای جداسازی رفتار وجود داشته باشد.
اشتباه سوم: نادیده گرفتن تغییرات Elementها
اگر تعداد Elementها دائماً تغییر میکند، Visitor ممکن است انتخاب مناسبی نباشد.
زیرا اضافه شدن Element جدید معمولاً باعث تغییر Visitor Interface و تمام Concrete Visitorها میشود.
اشتباه چهارم: دسترسی نامناسب به اطلاعات داخلی
Visitor ممکن است برای اجرای عملیات خود به اطلاعات داخلی Element نیاز داشته باشد.
اگر این اطلاعات کاملاً private باشند، ممکن است مجبور شوید API مناسبی برای دسترسی به آنها ایجاد کنید.
این موضوع یکی از محدودیتهای شناختهشده Visitor است.
# جمعبندی Visitor Pattern در PHP
Visitor Pattern یکی از الگوهای طراحی رفتاری در برنامهنویسی شیگرا است که هدف اصلی آن جدا کردن عملیات از Objectهایی است که عملیات روی آنها اجرا میشود.
در این Pattern معمولاً با مفاهیم زیر سروکار داریم:
Visitor
Concrete Visitor
Element
Concrete Element
accept()
visit()
ساختار کلی آن را میتوان اینگونه خلاصه کرد:
VisitorInterface
│
┌─────────┴─────────┐
│ │
ReportVisitor ExportVisitor
│ │
└─────────┬─────────┘
│
accept()
↑
┌─────────┴─────────┐
│ │
Product Invoice
اگر ساختار Objectهای شما نسبتاً ثابت است اما مرتباً نیاز دارید عملیاتهای جدیدی مانند گزارشگیری، Export، محاسبه مالیات یا پردازشهای مختلف به آنها اضافه کنید، Visitor میتواند گزینه مناسبی باشد.
اما اگر تعداد کلاسهای Element مرتباً تغییر میکند، باید با دقت بیشتری از این Pattern استفاده کنید؛ چون تغییر ساختار Elementها روی تمام Visitorها اثر میگذارد.
# سوالات متداول
Visitor Pattern در PHP چیست؟
Visitor Pattern یک الگوی طراحی رفتاری است که عملیات و الگوریتمها را از Objectهایی که روی آنها اجرا میشوند جدا میکند.
متد accept در Visitor Pattern چه کاری انجام میدهد؟
متد accept() Visitor را دریافت میکند و آن را به متد مناسب visit...() هدایت میکند. این بخش نقش مهمی در پیادهسازی Double Dispatch دارد.
آیا Visitor Pattern در Laravel کاربرد دارد؟
بله. در پروژههای پیچیده Laravel میتوان از Visitor برای جدا کردن عملیاتهایی مانند گزارشگیری، Export، محاسبه مالیات، حسابداری یا پردازشهای مختلف از مدلها و Objectهای اصلی استفاده کرد.
تفاوت Visitor و Strategy چیست؟
Strategy بیشتر برای انتخاب و تعویض یک الگوریتم یا روش انجام کار استفاده میشود، در حالی که Visitor برای جدا کردن عملیاتهای مختلف از مجموعهای از Objectهای مشخص کاربرد دارد.
آیا همیشه باید از Visitor Pattern استفاده کنیم؟
خیر. Visitor Pattern نسبتاً پیچیده است و برای پروژههای ساده معمولاً ضرورتی ندارد. بهتر است زمانی از آن استفاده شود که جداسازی چند عملیات مختلف از یک ساختار Object مشخص واقعاً ارزش ایجاد کند.





