الگوی Visitor

visitor pattern
  • پترن ویزیتور جزء پترن های رفتاری است.
  • هدف پترن ویزیتور، جداسازی الگوریتم از اشیائی است که قرار است روی آنها عملیاتی اجرا شود.
  • با پترن ویزیتور می توان یک عملیات جدید را بدون تغییر کلاس های عناصر اصلی، تعریف کرد.
  • به عبارت دیگر وقتی که نیاز به اجرای یک عملیات بر روی مجموعه ای از اشیاء ناهمگون باشد که ماهیت آن عملیات ثابت ولی شکل پیاده سازی برای هر شئ فرق کند، از پترن ویزیتور استفاده می کنیم.

مثال گراف

مسئله:

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

visitor graph example

در این حین اگر Task ای به شما واگذار شود که از شما بخواهد اطلاعات گراف را در یک فایل XML ذخیره کنید چه می کنید؟ در نگاه اول کار ساده به نظر می رسد. اضافه کردن یک متد Export XML به هر Node Class. البته به صورت بازگشتی تا تمام گره های گراف را پوشش دهد. حتی با استفاده از چند ریختی (Polymorphism) می توان آن را به صورت Loosely Coupling پیاده کرد به طوری که هر گره مجبور نباشد خودش Export مخصوص خودش را پیاده کند.

متاسفانه معمار سیستم به شما اجازه نخواهد داد تا Node Class ها را تغییر دهید. حق با اوست چرا که پروژه در مرحله ی Production است و هرگونه تغییری، خطای بالقوه ایجاد می کند. این ریسکی نیست که معمار سیستم بخواهد آن را بپذیرد.

visitor pattern xml implementation

معمار سیستم از کار شما متعجب خواهد شد و از شما سوال خواهد کرد که آیا اصلا این کار اصولی است که کد های XML Export را درون Node Class ها قرار دهید در حالی که وظیفه ی اصلی این کلاس ها کار با داده های جغرافیایی است؟ اضافه کردن کد های XML Export در Node Class مانند یک وصله ی ناجور است.

معمار سیستم همچنین از شما می پرسد که آیا این راه روش مناسبی است؟ اگر بعد از اینکه XML Export را به سیستم اضافه کردید کار جدیدی تحت عنوان JSON Export به شما محول شد چه؟ آیا باید JSON Export را هم به تمام Node Class ها اضافه کنید و این کلاس ها را دوباره دستخوش تغییر کنید؟ بدیهی است که این روش، روش استانداردی نیست.

راه حل:

  • الگوی ویزیتور پیشنهاد می کند که رفتار جدید را در داخل کلاس جداگانه ای به نام ویزیتور قرار دهید و آن را با کلاس های قبلی ترکیب نکنید.
  • شئ اصلی که قرار است رفتار جدید روی آن اعمال شود، به عنوان یک آرگومان به داخل یکی از متدهای کلاس ویزیتور پاس داده می شود.
  • حالا اگر قرار باشد که این رفتار جدید بر روی اشیاء کلاس های متفاوت اجرا شود چه؟ برای نمونه؛ در مثالی که زده شد امکان دارد که عملیات XML Export در هر Node Class ای متفاوت باشد. به همین دلیل کلاس ویزیتور باید به جای داشتن تنها یک متد، مجموعه ای از متدها را داشته باشد (به تعداد Node Class ها) که هر متد به عنوان آرگومان، شئ ای از جنس یکی از Node Class ها را قبول کند.

  • اما دقیقا چطور می توان این سه متد را فراخوانی کرد آن هم وقتی که با یک گراف کامل سر و کار دارید؟ این متد ها Signature های مختلف دارند (پارامترهای مختلفی را قبول می کنند) پس نمی توان از Polymorphism استفاده کرد.
  • برای انتخاب متد ویزیتور مناسب، باید ابتدا کلاس آن را بررسی کرد. آیا واقعا این شبیه یک کابوس نیست؟

  • امکان دارد که این سوال پیش بیاید که چرا از Method Overloading استفاده نمی کنیم؟ آن هم وقتی که تمام متد ها نام یکسان دارند؟ متاسفانه حتی با وجود اینکه برخی زبان های برنامه نویسی از Overloading پشتیبانی می کنند (نظیر C# و جاوا) اما این قضیه کمکی نخواهد کرد. زیرا کلاسی که Node Object متعلق به آن است ناشناخته است و Overloading مکانیزمی برای فهمیدن اینکه کدام متد مناسب اجرای آن است ندارد.
  • به جای اینکه به کلاینت اجازه داده شود تا متد مناسب را برای اجرا انتخاب کند، می توان این انتخاب را به شئ ای که به عنوان آرگومان ورودی به کلاس ویزیتور فرستاده می شود محول کرد. از آن جایی که اشیاء، جنس کلاس خود را می دانند، می توانند متد مناسب داخل کلاس ویزیتور را برگزینند.

مثال دنیای واقعی

visitor pattern real life example

یک مأمور خوب بیمه همیشه آمادگی آن را دارد تا با توجه به نوع مشتری، بیمه نامه مخصوص آن را ارائه دهد.

  • اگر ساختمان مسکونی باشد، بیمه ی سلامت می فروشد.
  • اگر ساختمان متعلق به بانک باشد، بیمه ی سرقت می فروشد.
  • اگر ساختمان متعلق به کافی شاپ باشد، بیمه ی آتش سوزی و سیل می فروشد و ….

ساختار UML

visitor pattern uml

توضیح UML

  • اینترفیس Visitor، مجموعه ای از Visiting Method ها را در خود دارد. این متدها اغلب دارای نام یکسانی هستند و به ازای هر Concrete Element (مثلا گره های گراف) یکی از این متد ها وجود دارد.
  • به ازای هر رفتار جدید، یک Concrete Visitor جدید وجود خواهد داشت.
  • اینترفیس Element دارای متدی برای پذیرش ویزیتورهاست که نام آن متد accept است. این متد فقط دارای یک پارامتر ورودی از جنس اینترفیس ویزیتور است.
  • هر Concrete Element باید متد پذیرش (acceptance method) را پیاده سازی کند. هدف این متد این است که با فراخوانی هر متد از Concrete Element در واقع متد متناظر آن در Concrete Visitor فراخوانی شود. (Redirect)
  • دقت کنید، اگر متد پذیرش در کلاس پایه (Element Class Base) تعریف شود، باید در کلاس های Concrete Element دوباره نویسی شود. (Override)
  • Client ها معمولا مجموعه ای از اشیاء پیچیده را در خود دارند. (به عنوان مثال درخت Composite) آنها از وجود کلاس های Concrete Element آگاه نیستند. زیرا آنها فقط با اشیاء و اینترفیس ها کار می کنند.

کاربرد الگوی Visitor

  • زمانیکه قرار باشد یک عملیات بر روی تمام عناصر یک شئ پیچیده اعمال شود. (مانند شئ درختی) با این پترن می توان عملیات خاصی را بر روی مجموعه ای از اشیاء از جنس کلاس های مختلف اجرا کرد.
  • برای جداسازی کلاس های اصلی برنامه از رفتار های کمکی، از این پترن استفاده می شود.
  • مناسب برای وقتیست که یک رفتار فقط در بعضی از کلاس های یک مجموعه کلاس، قابل پیاده سازی باشد.

پیاده سازی با یک مثال

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

بدین منظور باید ابتدا یک اینترفیس تعریف کرد که به ازای هر کلاس اصلی (استاد و کارمند) یک متد داشته باشد.

حال باید یک اینترفیس برای عناصر اصلی تعریف کرد که دارای یک متد به نام Accept باشد و پارامتر ورودی آن شئ Visitor باشد.

 باید متد های accept موجود در تمام کلاس های عناصر اصلی را پیاده سازی کنیم. وظیفه ی متد accept این است که هنگام فراخوانی خودش، در واقع متد Visitor متناظر با خودش را فراخوانی کند. ( از طریق جنس شئ ای که به عنوان پارامتر وارد متد می شود.) کلاس استاد دانشگاه به شکل زیر است.

امکان دارد که کلاس های عناصر اصلی دارای متد های خاصی باشند که در کلاس پایه یا اینترفیس وجود ندارند. ویزیتور باید به آنها دسترسی داشته باشد. کلاس کارمند دانشگاه هم به شکل زیر است.

کلاس های عناصر اصلی باید تنها با Visitor ها در ارتباط باشند آن هم از طریق اینترفیسِ Visitor. اما کلاس های Visitor باید از همه ی کلاس های عناصر اصلی و نوع پارامتر ورودی آنها مطلع بوده و به آن دسترسی داشته باشند.

برای پیاده سازی هر رفتار جدیدی که در کلاس های عناصر اصلی نیست باید یک کلاس Visitor جدید ساخت و همه ی متد های Visiting را پیاده سازی کرد. در اینجا ما دو کلاس Visitor خواهیم داشت، یکی مربوط به عیدی نوروز و دیگری مربوط به عید فطر می باشد. کلاس عیدی نوروز به شرح زیر است.

کلاس مربوط به عیدی عید فطر به شرح زیر است.

Client باید شئ Visitor را بسازد و از طریق متد accept به کلاس های عناصر اصلی پاس دهد. کلاس Client هیچ درکی از کلاس های عناصر اصلی ندارد.

نحوه ی استفاده از پترن Visitor در برنامه به شکل زیر است.

و خروجی آن به شکل زیر است.

…Calculating salary and Noruz reward

Salary and Noruz reward for professor is 3700

Salary and Noruz reward for professor is 2500

 

…Calculating salary and Ramezan reward

Salary and Ramezan reward for professor is 3400

Salary and Ramezan reward for professor is 2200

نوشته شده توسط mrbitmap علیرضا علی رمضانی

مقالات مرتبط

جدیدترین مقالات

فهرست