سرفصل‌های آموزشی
آموزش الگوهای طراحی (Design Pattern)
الگوی طراحی Visitor

الگوی طراحی Visitor

در این مقاله به توضیح و بررسی الگوی طراحی (Design Pattern) Visitor می ‌پردازیم و این الگو را از زوایای مختلف مورد بررسی قرار می ‌دهیم، ویژگی‌های این الگو، نقاط مثبت و منفی آن و موارد کاربرد آن را به تفصیل بیان می ‌کنیم و در آخر با ارائه ی یک مثال عملی به شما کمک می ‌کنیم که بتوانید در صورت نیاز، این الگوی طراحی را در پروژه ی خود پیاده سازی نمایید. امیدوارم که این مقاله مورد توجه و استفاده ی شما قرار بگیرد.

الگوی طراحی Visitor زیرمجموعه ی الگوهای نوع Behavioral Pattern است و مانند بقیه ی الگوهای این دسته به بررسی و ساماندهی روابط و الگوریتم های موجود بین object های برنامه ی ما می ‌پردازد. الگوی طراحی Visitor به شما اجازه می ‌دهد که متدها یا الگوریتم هایی را به یک کلاس اضافه کنید، بدون این که تغییری در کد پایه ی آن کلاس اِعمال کنید.

طرح مشکل:

در اینجا یک مثال مفهومی برای درک بهتر الگوی Visitor مطرح می‌کنیم و در بخش نمونه کد یک مثال واقعی را پیاده سازی می ‌کنیم.
در ابتدا فرض کنید که تیم شما روی نمایش موقعیت‌ های جغرافیایی مربوط به اماکن مختلف، در نقشه فعالیت می ‌کند. هر شکل نماینده ‌ی یک مکان مخصوص است، مثلا نقطه، نشان دهنده ی ساختمان های مسکونی و دایره، نشان دهنده ی فروشگاه ها و مستطیل، نشان دهنده ی مراکز دولتی مانند اداره ها، پلیس و ... است و به همین صورت برنامه ی شما برای هر دسته از مکان ها یک نماینده انتخاب کرده است و نمایش می ‌دهد.
پس به طور خلاصه برنامه ی شما شامل یک interface به نام Shape (شکل) و تعدادی کلاس به نام‌های Dot و Circle و Rectangle و ... است که کلاس‌ها از Shape، Implement شده‌اند. هر کدام از این کلاس‌ها تعدادی متد دارند که وظیفه ی انجام امور مرتبط به هر Shape را بر عهده دارند. مثلا، متدی برای مشخص کردن اسم موقعیت جغرافیایی و متدی دیگر برای مشخص کردن موقعیت جغرافیایی شکل مربوطه تعیین شده است. اما همه ی این متدها در راستای امور مربوط به همان کلاس به خصوص هستند و به کلاس‌های دیگر مربوط نمی ‌شود اگر چه ممکن است برخی از این متدها مشترک باشند.
حال فرض کنید که از طرف مدیر پروژه به شما وظیفه‌ای داده می ‌شود که قابلیتی به برنامه اضافه کنید که بتوان نقشه ی مربوط به منطقه ی مورد نظر کاربر را به صورت داده ی xml، export کرد و در دسترس کاربر قرار داد. در ابتدا این task بسیار ساده و راحت به نظر می ‌رسد. شما در نظر دارید که یک متد به نام export به تمام کلاس‌هایی که از Shape، implement شده‌اند، اضافه کنید که وظیفه ی export کردن اطلاعات مربوط به object آن کلاس را بر عهده دارد. اما مدیر پروژه به شما می ‌گوید، شما امکان دسترسی به کلاس‌های مربوط به Shape ها را ندارید چون پروژه در وضعیت production است و مدیر پروژه ریسک دسترسی شما به کلاس‌های زیرمجموعه ی Shape را نمی ‌پذیرد چرا که در صورت وجود bug درکد شما کل پروژه از کار می ‌افتد. 
از طرفی باید به این نکته توجه کرد که کلاس‌های Dot و Circle و ... وظیفه ی انجام عملیات و امور مربوط به اطلاعات جغرافیایی کلاس خود را بر عهده دارند و تابعی مثل xmlExport نمی ‌تواند در قالب وظایف این کلاس قرار بگیرد و باعث چند وظیفه‌ای شدن کلاس‌های برنامه ما می ‌شوند که این، در تقابل با قوانین SOLID (Single Responsibility) است. 
پس ما نه توانایی اضافه کردن این متد به کلاس‌های زیر مجموعه Shape را داریم و نه این کار درستی است که این متد را در این کلاس‌ها بنویسیم. خب پس این متد کجا باید نوشته شود؟

راه حل مشکل:

اگر تعریف الگوی طراحی Visitor را به خاطر بیاورید، گفتیم که این الگوی طراحی در مواقعی کاربرد دارد که می ‌خواهیم به کلاس یا کامپوننتی از برنامه ی خود ویژگی ای را اضافه کنیم ولی به هر دلیلی نمی ‌توانیم این قابلیت را زیرمجموعه ی کلاس مربوطه قرار دهیم. برای حل مشکل از راه حلی که الگوی Visitor ارائه کرده استفاده می ‌کنیم.
به این صورت که یک Interface به نام Visitor می ‌سازیم و در هر کلاسی که نیاز داشته باشیم، این Interface را implement می ‌کنیم. برای هر ویژگی که می ‌خواستیم در کلاس‌هایمان داشته باشیم ولی امکان پیاده سازی مستقیم در کلاس را نداشت، می ‌توانیم یک Visitor بسازیم که از Interface Visitor، implement می ‌شود. مثلا در اینجا ما یک Visitor  برای xmlExport می ‌سازیم.
از طرفی باید اشاره کنیم که طریقه ی ارسال داده برای هر کدام از کلاس‌های زیر مجموعه Shape متفاوت است، مثلا اطلاعات جغرافیایی مربوط به نقطه به وسیله دو عدد برای موقعیت افقی(X) و موقعیت عمودی(Y) آن مشخص می ‌شود اما برای مستطیل علاوه بر موقعیت افقی و عمودی آن به طول و عرض مستطیل هم نیاز داریم. پس متد ارسال داده ی هر کدام از شکل ها متفاوت است و باید برای شکل مربوطه شخصی سازی شوند. 
برای حل این مشکل در Shape Interface یک متد به نام Accept که ورودی از جنس Visitor، قبول می ‌کند اضافه می ‌کنیم و این متد را در همه ی کلاس‌ها بازتعریف می ‌کنیم. این متد به این صورت کار می ‌کند که یک شیئ از خودش را به عنوان ورودی به متد مربوط به آن شکل در کلاس xmlExportVisitor می ‌دهد تا در آن عملیات مربوط به تبدیل اطلاعات جغرافیایی آن شکل به فرمت xml انجام شود.
زمانی که ما می‌خواهیم عملیات جدید را صدا بزنیم، از شیئی ساخته شده از Element ها استفاده می ‌کنیم و تابع Accept را روی آن صدا می ‌زنیم. تابع Accept که شیئی از Visitor را با خود دارد، در واقع به سراغ Element مربوطه می ‌رود و اطلاعاتی که از آن نیاز دارد را دریافت می ‌کند و در متدهای خود استفاده می ‌کند.
در مجموع با توجه به شکل1 برنامه ی ما از سه بخش کلی تشکیل می ‌شود که شامل کدهای Client یا کنترلر اصلی برنامه است، بخش دوم Visitor است که شامل عملیات و متدهایی است که نمی ‌توانستیم در کلاس‌های Element قرار دهیم و بخش سوم کلاس‌ها و Interface مربوط به Element هاست..


 
شکل 1

در برنامه ی ما Element ها همان Shape و زیر مجموعه‌های آن هستند. اگر به شکل توجه کنید، متوجه می ‌شوید که در کلاس‌های زیر مجموعه Element یک متد به نام Accept به باقی متدهای مربوط به هر Element اضافه شده است که این متد دقیقا جایی است که Element های ما به Visitor ها متصل می ‌شوند و یک object از همان Element به عنوان ورودی به Visitor مربوط به آن می ‌فرستند تا عملیات مورد نیاز روی آن object انجام شود.

رابطه با سایر الگوها:

1) الگوی طراحی Visitor را می ‌توان نسخه ی قوِی تر و توانمندتر از الگوی طراحی Command دانست چرا که الگوی Command درخواست ها را به object ها تبدیل می ‌کند و امکان اجرای عملیات مختلف را بر روی این object ها به ما می ‌دهد. ولی الگوی Visitor امکان اجرای عملیات روی object های کلاس‌های مختلف را به ما می ‌دهد.
2) شما می ‌توانید از الگو Visitor برای انجام عملیات مختلف روی یک Composite tree استفاده کنید که چون رابطه درختی با هم دارند بسیار کاربردی است.
3) الگوی Visitorرا می ‌توان به همراه الگوی Iterator استفاده کرد، بدین گونه که می ‌توان روی تمام اِلِمان‌های Iterator عملیات یا پردازش خاصی را انجام داد حتی اگر این اِلِمان ها از جنس کلاس‌های مختلف باشند.

مزایا و معایب استفاده از Visitor: 

مزایا: 
1) اولین مزیتی که می ‌توان برای این الگوی طراحی نام برد، کمک به رعایت قانون Open/Closed Principle  از مجموعه قوانین SOLID است. مورد استفاده ی این الگوی طراحی، مواقعی است که نمی ‌توانیم به علت قانون Open/Closed یا نبود دسترسی به کلاس مربوطه، قابلیت مورد نظر را به برنامه یمان اضافه کنیم و با استفاده از الگوی طراحی Visitor این مانع رفع می‌شود.
2) دومین مزیت، عدم نقض قانون Single Responsibility Principle از مجموعه قوانین SOLID است که به لطف الگو Visitor هر کدام از کلاس‌های ما دقیقا یک وظیفه بر عهده دارند. مثلا کلاس‌های Element تنها وظیفه ی انجام امور مربوط به همان Element را بر عهده دارد مانند: ذخیره کردن نام در متغیر مربوطه یا انجام پردازش روی موقعیت جغرافیایی آن Element، از طرفی کلاس Visitor مربوط به آن Element وظیفه ی انجام اموری را دارد که می ‌تواند روی object های آن اِلِمان اجرا شود اما جزو وظایف Element مورد نظر نیست. پس قانون Single Responsibility را رعایت کرده‌ایم.
3) مزیت دیگر این است که یک Visitor در تعامل با کلاس‌های مختلف می ‌تواند مجموعه ای از اطلاعات مختلف از این کلاس‌ها را در کنار هم جمع کند و برای مثال پردازشی برروی آن اطلاعات انجام دهد؛ در حالی که این اطلاعات به هم ارتباطی ندارند و از کلاس‌های مختلفی دریافت شده‌اند.
4) مزیت چهارم این است که شما، عملیات و الگوریتم هایی که در برنامه خود استفاده می ‌کنید را تحت نام متدهای مختلف در یک کلاس به نام Visitor جمع می ‌کنید که این کار از پخش شدن این متدهای مرتبط به هم، در سطح برنامه شما جلوگیری می ‌کند.
معایب:
1) اولین و مهم‌ترین عیبی که می ‌توان به این الگو وارد نمود این است که اگر شما یک Element به برنامه خود اضافه کنید یا از آن حذف کنید، در این صورت شما باید تمام Visitor هایی که به آن Element مربوط بودند را به‌روزرسانی کنید و متدهایی را به آن اضافه یا کم کنید.
2) عیب دیگری که می ‌تواند شما را از استفاده از این الگو منصرف کند این است که، وقتی شما object ای را به Visitor ورودی می ‌دهید، این Visitor ممکن است درخواست دسترسی به اطلاعات یا متدهایی از object شما را داشته باشد که به عنوان اطلاعات یا متدهایprivate تعریف شده‌اند. از طرفی چون مورد استفاده ی Visitor عموما در مواردی است که پروژه در وضعیت production قرار دارد و امکان به‌روزرسانی متدها و متغیرهای کلاس object مورد نظر وجود ندارد، پس باید به وضعیت دسترسی اطلاعات و متدهای مورد استفاده از object در Visitor توجه داشته باشیم.

یک مثال واقعی

تعریف مثال: در این جا فرض کنید چند شرکت داریم (Company) و این شرکت‌ها شامل تعدادی دپارتمان (Department) هستند و هر دپارتمان تعدادی کارمند (Employee) دارد. برنامه ی ما برای ساماندهی و مدیریت شرکت‌ها، توسعه داده شده است. در این حالت هر کدام از موجودیت های Company و Department و Employee یک کلاس هستند که از Interface به نام Entity، Implement شده‌اند (شکل 2).
 شکل 2
شما در کلاس Company عملیات مربوط به شرکت را انجام می ‌دهید و این اتفاق برای کلاس‌های دپارتمان و کارمند هم، مشابه است. حال فرض کنید مدیر پروژه از شما می ‌خواهد کدی به برنامه اضافه کنید که هزینه ی دستمزد شرکت‌ها به همراه هزینه ی تفکیک شده ی هر دپارتمان زیر مجموعه ی این شرکت‌ها را به عنوان یک گزارش خروجی بدهد.
در اینجا هم با مشکل نبود دسترسی به کلاس‌های Company و Department و Employee مواجه هستیم و از طرفی قرار دادن کدِ تهیه ی گزارش در این کلاس‌ها از نظر قوانین SOLID صحیح نیست.
پس به سراغ الگوی طراحی Visitor می ‌رویم و طبق آنچه که در بخش‌های  قبلی توضیح داده شد آن را پیاده سازی می ‌کنیم.
شکل کلی کلاس‌های زیر مجموعه ی Entity به شکل زیر در می ‌آید:
 شکل 3

همان‌طور که در شکل 3 مشاهده می ‌کنید، به کلاس‌های زیر مجموعه Entity، یک متد به نام Accept اضافه کردیم که وظیفه ارسال یک نمونه از object کلاس خود را به یکی از متدهای کلاس Visitor بر عهده دارد. به غیر از این مورد، نیاز به اِعمال تغییر دیگری نیست.
از طرفی کلاس SalaryReportVisitor که وظیفه ی انجام عملیات محاسبه ی دستمزد شرکت‌ ها و دپارتمان های زیر مجموعه ی شرکت‌ها به صورت تفکیک شده را بر عهده دارد از Visitor Interface، Implement می ‌شود. به شکل 4 توجه کنید.
 شکل 4

در اینجا کلاس Entity معادل Element و Visitor معادل Visitor در دیاگرام، مفهومی است که در بخش قبل درباره آن‌ها صحبت کردیم.
وضعیت پوشه بندی در پیاده سازی Visitor:

/App
/Http/Controllers/
YourController.php
/Elements
Company.php
Department.php
Employee.php
/Interfaces
Entity.php
Visitor.php
/Visitor
SalaryReportVisitor.php

توجه کنید که شما می‌توانید ساختار پوشه بندی و نام‌گذاری رعایت شده در نمونه کد را متناسب با ساختار پروژه ی خود تغییر دهید ولی توجه داشته باشید که نام‌گذاری و پوشه بندی باید به گونه‌ای باشد که با خواندن اسم متدها یا پوشه ها، وظیفه و ماموریت آن متد یا پوشه قابل‌فهم باشد.
پیاده سازی مثال:

ابتدا کد Interface های Visitor و Entity را تکمیل می ‌کنیم و سپس در کلاس‌ها، این interface ها را implement می ‌کنیم.
کد Entity:

namespace App\Interfaces;


interface Entity
{
/**  some of Entity’s own methods  **/
public function accept(Visitor $visitor);
}

همان ‌طور که مشاهده می‌کنید این interface شامل تعدادی متد است که مخصوص به Entity است، ولی حتما متد Accept را دارد چون راه ارتباطی Visitor و کلاس‌های زیر مجموعه ی Entity همین متد Accept است.
کد Visitor.php: 

namespace App\Interfaces;


use App\Elements\Company;
use App\Elements\Department;
use App\Elements\Employee;


interface Visitor
{
public function visitCompany(Company $company);


public function visitDepartment(Department $department);
 
public function visitEmployee(Employee $employee);
}

Visitor برای هر کلاس که نیاز به انجام عملیات اضافه دارد یک متد دارد.
کد Employee.php: 

namespace App\Elements;


use App\Interfaces\Entity;
use App\Interfaces\Visitor;


class Employee implements Entity
{
    private $name;
    private $position;
    private $salary;
private $age;


     /**  Employee constructor. */
    public function __construct(string $name, string $position, int $salary, int $age)
    {
  $this->name = $name;
        $this->position = $position;
        $this->salary = $salary;
        $this->age = $age;
    }


    /** Some Functions to get access to private variables in class */
    public function getName(): string { return $this->name; }


    public function getPosition(): string { return $this->position;  }


    public function getSalary(): int  { return $this->salary; }


    public function getAge(): int  { return $this->age; }


    /** accept method that returns an object from present class to visitor */
    public function accept(Visitor $visitor)
    {
return $visitor->visitEmployee($this);
    }
}

همان‌طور که مشاهده می ‌کنید این کلاس شامل یک متد به نام Accept است که یک نمونه از همین کلاس را به ()visitEmployee باز می ‌گرداند که متد مربوط به انجام عملیاتی مربوط به Employee در کلاس SalaryReportVisitor است.

کد های مربوط به Department و Company هم مانند کد کلاس Employee است ولی با این تفاوت که متد Accept در این کلاس‌ها متد مربوط به کلاس خود را از Visitor صدا می ‌زنند که برای Company و Department به ترتیب ()visitComapny و ()visitDepartment است.

کد Department.php: 

namespace App\Elements;


use App\Interfaces\Entity;
use App\Interfaces\Visitor;


class Department implements Entity
{
private $name;
private $employees;

/** Department constructor */
public function __construct(string $name, array $employees)
{
$this->name = $name;
        $this->employees = $employees;
    }


public function getName(): string { return $this->name; }


public function getEmployees(): array { return $this->employees; }


// calculate total salary for employees of departmment
public function getCost() :int
{
$cost = 0;
foreach ($this->employees as $employee) {
$cost += $employee->getSalary();
}
        return $cost;
    }


// accept method access visitDepartment from visitor
public function accept(Visitor $visitor)
  {
        return $visitor->visitDepartment($this);
    }
}

کد Company.php:

namespace App\Elements;


use App\Interfaces\Entity;
use App\Interfaces\Visitor;


class Company implements Entity
{
private $name;
private $departments;


/** Company constructor */
public function __construct(string $name, array $departments)
{
        $this->name = $name;
        $this->departments = $departments;
    }
public function getName() { return $this->name; }
public function getDepartment() { return $this->departments; }


// accept function access visitCompany from visitor
public function accept(Visitor $visitor)
{    return $visitor->visitCompany($this);   }
}

توجه کنید که متد ()Getcost از کلاس Department.php مربوط به جمع کل دستمزدهای کارمندان آن دپارتمان است.

کد SalaryReportVisitor:

namespace App\Visitors;


use App\Elements\Company;
use App\Elements\Department;
use App\Elements\Employee;
use App\Interfaces\Visitor;


class SalaryReportVisitor implements Visitor
{


    public function visitCompany(Company $company)
    {
// this method will calculate the sum of all salaries for employees 
// whom works for the company and return data in proper form.
    }


    public function visitDepartment(Department $department)
    {
// this method will calculate the sum of all salaries for employees 
// whom work in a specific department of a company 
// and return the data in proper form
    }


    public function visitEmployee(Employee $employee)
    {
// this method will return data of a employee include name,
// position, age and salary in proper form
    }


}

حال که کلاس‌ها و متدهای مربوط به Visitor و Entity ها را کامل کردیم. می ‌توانیم کد مربوط به کنترلر را تکمیل کنیم.
کد مربوط به کنترلر که درخواست اصلی در این کلاس صدا زده می ‌شود (در اینجا HomeController):

namespace App\Http\Controllers;


use App\Elements\Company;
use App\Elements\Department;
use App\Elements\Employee;
use App\Visitors\SalaryReportVisitor;
use Illuminate\Http\Request;


class HomeController extends Controller
{
    public function index()
    {
// new employees
$MD_employee_1 = new Employee(
"M_D_Employee 1", 
"designer",
100000,
23
);
$TS_employee_1 = new Employee(
"T_S_Employee 1",    /**  name */
"supervisor",   /**  position  */ 
70000,    /**  salary  */
24  /**  age  */
);
 
// new departments
$mobileDev = new Department("Mobile Development", [
$MD_employee_1,
// and some other employees …
]);


$techSupport = new Department("Tech Support", [
$TS_employee_1,
            // and some other employees …
        ]);
 


// new Comapny
$company = new Company("SuperStarDevelopment", [
$mobileDev,
$techSupport
]);


// make new visitor 
$salary_report = new SalaryReportVisitor();


//call accept method for company
// return $company->accept($salary_report);


//call accept method for department
// return $techSupport->accept($salary_report);


//call accept method for employee
// return $MD_employee_1->accept($salary_report);


    }
}

همان‌طور که مشاهده می ‌کنید تنها کاری که لازم است انجام دهیم، صدا زدن متد Accept از کلاس‌های زیر مجموعه ی Entity و پاس دادن یک Visitor به عنوان ورودی به این متد است.

سخن پایانی

در این مقاله به بررسی الگوی طراحی  Visitor پرداختیم و مسائلی که به وسیله این الگو رفع می ‌شود را بررسی کردیم. همچنین نقاط قوت و ضعف آن را بررسی کردیم. تصمیم به استفاده از این الگوی طراحی بستگی به میزان متغیر بودن کلاس‌ها و اجزای آن دارد و بیشتر در مواردی که نیاز به اضافه کردن یک سری ویژگی خاص به برنامه ی خود داریم، به کار می ‌رود.
موفق باشید

منابع

1.https://sourcemaking.com/design_patterns/command

2.https://refactoring.guru/design-patterns/visitor

3.https://refactoring.guru/design-patterns

4.https://refactoring.guru/design-patterns/behavioral-patterns

5.https://sourcemaking.com/design_patterns/visitor

6.https://sourcemaking.com/design_patterns/behavioral_patterns

7.https://www.geeksforgeeks.org/visitor-design-pattern

8.https://www.tutorialspoint.com/design_pattern/visitor_pattern.htm

9.https://www.baeldung.com/java-visitor-pattern

10.https://dzone.com/articles/design-patterns-visitor

11.https://refactoring.guru/design-patterns/command

12.https://refactoring.guru/design-patterns/iterator

online-support-icon