اگر بخواهیم جایگاه الگوی طراحی Observer را در طبقه بندی الگو های طراحی بررسی کنیم، مشخص میشود که این الگو بر اساس هدف، جزو الگو های رفتاری یا Behavioral است یعنی الگوی طراحی Observer به رفتار هایی که در برنامه ی ما اتفاق می افتد مربوط است.
الگوی طراحی Observer
الگوی ناظر یا همان Observer یک الگوی طراحی نرمافزار است که در آن یک شیء به نام موضوع (subject)، فهرست وابستگیهایش را با نام ناظران (observers) نگه میدارد و هرگونه تغییر در وضعیت خود را به طور خودکار و معمولاً با صدا کردن یک تابع مشترک در ناظران به آن ها اطلاعرسانی میکند.
مشکل
فرض کنید فردی علاقه مند به خرید محصولی است که در یک فروشگاه قرار است به فروش برسد. (برای مثال فردی به دنبال یک سنسور است که قرار است به زودی در فروشگاه موجود شود و به فروش برسد). این فرد هر روز به فروشگاه سر میزند که ببیند این محصول برای فروش قرار گرفته است یا خیر.
از آن طرف اگر فروشگاه بخواهد برای هر محصول خود اطلاع رسانی کند باید میلیون ها ایمیل بزند که بگوید ما کالایی را در فروشگاه اضافه کردیم. این کار باعث می شود که یک سری از کاربران، از این که هر روز به فروشگاه سر بزنند و وضعیت محصول مورد نظر را بررسی کنند راحت بشوند اما باعث ناراحتی عدهی دیگر از مشتریانی میشود که علاقه ای به محصولات جدید یا موجود شده ندارند.
راه حل
اگر به تمام مشتریان ایمیل بزنیم، منابع سیستم را بیهوده هدر داده ایم و از آن طرف اگر ایمیل نزنیم یک بخشی از منابع سیستم ما برای چک کردن های هر روز یک سری مشتریان بیهوده مصرف می شود. اگر بخواهیم این کار را بهینه بکنیم باید یک فیلد جدید توی دیتابیس ایجاد کنیم که هر شخصی که در انتظار موجود کردن محصولی است را در آن ذخیره کنیم و بعد از این که آن محصول اضافه شد به آن ایمیل ارسال شود.
برای بالا بردن خوانایی برنامه و توسعه بهتر پروژه، ما از الگوی طراحی Observer استفاده می کنیم. این الگو به ما میگوید شیء اصلی ما که قرار است کار اصلی را انجام بدهد به عنوان subject
و بقیه ی اشیا را به عنوان Observer درنظر بگیریم و وقتی شیء اصلی ما کار خود را انجام داد به بقیه اشیاء اعلام کند که آن ها نیز کار خود را انجام دهند.
در این مسئله شیء اصلی ما، به روز رسانی محصولات در فروشگاه است و مشاهده کنندگان آن (Observers) ارسال ایمیل و ارسال پیام کوتاه می شوند که هر کدام کار خاصی را انجام می دهند. در این الگو ما کد ها را در تابع اصلی نمینویسیم که باعث میشود کد ها ساماندهی و مرتب شوند. اگر بعد از مدتی خواستیم برنامه ما قابلیت ارسال محصولات بعد از به روز رسانی در شبکه های اجتماعی را داشته باشد، احتیاجی نیست کد های قبلی را تغییر دهیم، فقط لازم است آن بخش از برنامه که برای ارسال محصولات به شبکه های اجتماعی است را به subject
مربوطه به عنوان یک Observer اضافه کنیم.
مزایا و معایب استفاده از Observer ها
مثل هر روش دیگری که ما در دنیای برنامه نویسی از آن استفاده می کنیم این الگو هم مزایا و معایب خاص خود را دارد که در ادامه به آن می پردازیم:
مزایا
- بالا رفتن خوانایی کد
- اتصال چند کلاس به یک دیگر با سادهترین حالت
- یک اطلاع رسانی به تمام observer ها صرفه نظر از تعداد و نوع آنها ارسال می شود
معایب
- قانون اول SOLID را نقض میکند
- اگر به درستی این الگو پیاده سازی نشود ممکن است پیچیدگی اضافی به پروژه و کد های ما اضافه کند.
- تا زمانی که
subject
بهobserver
ها اطلاع رسانی نکند نمی تواند نمونهی observer های ذخیره شده در خود را آزاد کند و احتمال نشت حافظه (Memory leak) وجود دارد که به این مشکل Lapsed listener problem می گویند.
پیاده سازی در PHP
برای پیاده سازی این دیزاین پترن در PHP، ما به دو رابط (interface) برای subject و observerها نیاز داریم. برای این کار خود PHP برای ما یک نمونه در قالب دو رابط به نام های SplObserver
و SplSubejct
در اختیار ما قرار داده است.
رابط SplSubject
دارای سه متد اصلی به نام attach
, detach
و notify
و به شرح زیر است:
- متد
attach
برای اضافه کردن یک observer - متد
detach
برای حذف یک observer - متد
notify
هم برای ارسال اعلان به observer
/**
* The <b>SplSubject</b> interface is used alongside
* <b>SplObserver</b> to implement the Observer Design Pattern.
* @link https://php.net/manual/en/class.splsubject.php
*/
interface SplSubject {
/**
* Attach an SplObserver
* @link https://php.net/manual/en/splsubject.attach.php
* @param SplObserver $observer <p>
* The <b>SplObserver</b> to attach.
* </p>
* @return void
* @since 5.1.0
*/
public function attach (SplObserver $observer);
/**
* Detach an observer
* @link https://php.net/manual/en/splsubject.detach.php
* @param SplObserver $observer <p>
* The <b>SplObserver</b> to detach.
* </p>
* @return void
* @since 5.1.0
*/
public function detach (SplObserver $observer);
/**
* Notify an observer
* @link https://php.net/manual/en/splsubject.notify.php
* @return void
* @since 5.1.0
*/
public function notify ();
}
در تصویر پایین رابط SplObserver
را مشاهده می کنیم که دارای یک تابع update
است. تابع notify در رابط SplSubject
، یک تابع به نام update
را در هر observer صدا می کنند و در تابع update
هر observer کار مربوط به خود را انجام میدهد.
/**
* The <b>SplObserver</b> interface is used alongside
* <b>SplSubject</b> to implement the Observer Design Pattern.
* @link https://php.net/manual/en/class.splobserver.php
*/
interface SplObserver {
/**
* Receive update from subject
* @link https://php.net/manual/en/splobserver.update.php
* @param SplSubject $subject <p>
* The <b>SplSubject</b> notifying the observer of an update.
* </p>
* @return void
* @since 5.1.0
*/
public function update (SplSubject $subject);
}
فرض کنید ما یک سیستم وبلاگ داریم. هر وقت کسی در یک مطلب نظری قرار می دهد، یک سری کار ها باید انجام بشود. (برای مثال به نویسنده آن مطلب یک ایمیل زده شود مبنی بر این که یک نظر برای مطلب شما اضافه شده است، تعداد نظر ها یک عدد اضافه شود و همچنین یک ایمیل به تمام افرادی که نظر داده اند زده شود مبنی بر این که یک نظر جدید برای این پست قرار گرفته است).
خب برای این کار یک کلاس AddedComment
داریم که از SplSubject
مشتق شده که به صورت زیر است.
<?php
class AddedComment implements SplSubject
{
protected $observers = [];
public $comment_text;
public $post_id;
public function __construct($comment_text, $post_id)
{
$this->comment_text = $comment_text;
$this->post_id = $post_id;
}
public function attach(SplObserver $observer)
{
$key = spl_object_hash($observer);
$this->observers[$key] = $observer;
return $this;
}
public function detach(SplObserver $observer)
{
$key = spl_object_hash($observer);
unset($this->observers[$key]);
}
public function notify()
{
foreach ($this->observers as $observer) {
$observer->update($this);
}
}
}
متد attach یک شیء از نوع SplObserver
دریافت می کند و آن را به وسیله تابع spl_object_hash
تبدیل به یک رشتهی hash شده کرده و آن را به عنوان کلید و خود شیء را به عنوان مقدار اصلی در آرایه ی observers ذخیره می کند که در آینده بتواند به وسیله ی notify
به این observer ها اعلان بفرستند.
و متد notify هم برای این است که متد update در هریک از observer ها را اجرا کند.
برای این مثال ساده ما سه observer داریم که همه برای مثال فقط یک عبارتی را در صفحه برای ما چاپ می کنند.
class EmailAuthor implements SplObserver
{
public function update(SplSubject $subject)
{
echo __METHOD__ . " Emailing the author of post id: " . $subject->post_id . " that someone commented with : " . $subject->comment_text . "<br>";
}
}
class EmailOtherCommentators implements SplObserver
{
public function update(SplSubject $subject)
{
echo __METHOD__ . " Emailing all other comment authors who commented on " . $subject->post_id . " that someone commented with : " . $subject->comment_text . "<br>";
}
}
class IncrementCommentCount implements SplObserver
{
public function update(SplSubject $subject)
{
echo __METHOD__ . " Updating comment count to + 1 for blog post id: " . $subject->post_id . "<br>";
}
}
همه ی observer ها از SplObserver
مشتق شده اند، پس باید همه ی آن ها متد update
را داشته باشند.
در این مثال این observer ها کاری به جز چاپ کردن روی صفحه نمایش کاری نمی کنند ولی بدیهی است که در پروژه های واقعی این کلاس ها کارهایی بیشتر از یک چاپ انجام می دهند.
خب الان همه چیز اماده شده است و فقط باید از آن استفاده کنیم. در فایل اصلی که میخواهیم از آن استفاده کنیم باید کد زیر را قرار دهیم.
$new_comment = 'This is a new comment';
$blog_post_id = 564;
echo "Adding observers to subject<br>";
$addedComment = new AddedComment($new_comment, $blog_post_id);
$addedComment->attach(new IncrementCommentCount())->attach(new EmailOtherCommentators())->attach(new EmailAuthor());
echo "Now going to notify() them...<br>";
$addedComment->notify();
echo "Done<br>";
ما فرض را بر این میگیریم که تمام کدهای بالا را پشت سر هم و در یک فایل نوشته شده اند. برای مثال ما یک متغیر به نام new_comment
ساخته ایم که در آن محتوای نظر قرار داده شده است و همچنین id مطلبی که نظر روی آن گذاشته شده است، در متغیر blog_post_id
ذخیره میشود. در خط بعدی از کلاس AddedComment
یک شیء ساخته و مقادیر new_comment
و blog_post_id
را به آن می دهیم. در مرحله بعد با استفاده از متد attach
، تمام observer های خود را به این شیء میدهیم.
در مرحله بعد متد notify
صدا زده می شود و این متد تمام متد های update
در کلاس های observer را صدا کرده و خروجی نهایی این کد به صورت زیر است:
Adding observers to subject
Now going to notify() them...
IncrementCommentCount::update Updating comment count to + 1 for blog post id: 564
EmailOtherCommentators::update Emailing all other comment authors who commented on 564 that someone commented with : This is a new comment
EmailAuthor::update Emailing the author of post id: 564 that someone commented with : This is a new comment
Done
الگوی observer در لاراول برای خیلی چیز ها استفاده می شود برای مثال می توان به event ها و listener ها اشاره کرد که با استفاده از آن ها می توانیم رفتار هایی شبیه به این الگو ایجاد کنیم.
ما باید زمانی از این الگو استفاده کنیم که یک کار باعث شود که چند کار دیگر تغییر کرده و یا اجرا شوند.
جمع بندی
معماری رویداد محور (Event-driven architecture) یک الگوی معماری نرمافزار است، که به وسیله آن میتوان تمام اتفاقات را ضبط و پردازش کرده و ارتباط وقایع با یکدیگر را کنترل و پیاده سازی کنیم. در این الگوی طراحی یک سری اتفاقاتی انجام میشوند که به وسیله observe ها با یکدیگر ارتباط داده شده و یک سیستم یکپارچه را به وجود میآورند.