85- آموزش جاوااسکریپت | Observer Pattern
در این بخش با Observer Pattern در JavaScript آشنا میشویم؛ الگویی برای subscribe و unsubscribe کردن به رویدادها. همچنین پیادهسازی آن را با prototype و ES6 class بررسی میکنیم.
در این بخش، الگوی Observer Pattern را در JavaScript بررسی میکنیم. این الگو به ما اجازه میدهد به رویدادها یا عملکردهای مشخصی اشتراک بدهیم و در صورت نیاز، اشتراک را لغو کنیم.
Observer Pattern یک مدل بسیار مناسب برای مدیریت رویدادها فراهم میکند و در بسیاری از برنامههای رابط کاربری، سیستمهای event-driven و معماریهای واکنشی کاربرد دارد.
Observer Pattern چیست؟
در Observer Pattern، یک آبجکت اصلی داریم که مجموعهای از توابع یا observerها را نگهداری میکند. این observerها میتوانند:
- به یک رویداد subscribe شوند
- از آن رویداد unsubscribe شوند
- هنگام اجرای رویداد، فراخوانی شوند
در واقع، این الگو کمک میکند بین بخشی که رویداد را منتشر میکند و بخشهایی که به آن واکنش نشان میدهند، ارتباطی منعطف ایجاد شود.
کاربرد Observer Pattern
این الگو در سناریوهای مختلف بسیار مفید است، از جمله:
- مدیریت رویدادهای رابط کاربری
- بهروزرسانی بخشهای مختلف DOM
- سیستمهای اعلان و notification
- معماری مبتنی بر event
- ارتباط بین اجزای مستقل در برنامه
بسیاری از کتابخانهها و فریمورکها نیز از مفاهیم مشابه Observer برای مدیریت eventها استفاده میکنند.
پیادهسازی با Prototype
در این مثال، یک سازنده به نام EventObserver میسازیم که لیستی از observerها را نگهداری میکند:
function EventObserver() {
this.observers = [];
}
در ادامه، متدهای subscribe، unsubscribe و fire را روی prototype تعریف میکنیم.
تعریف متدهای اصلی
متد subscribe
این متد یک تابع را به لیست observerها اضافه میکند:
EventObserver.prototype = {
subscribe: function (fn) {
this.observers.push(fn);
console.log(`You are now subscribed to ${fn.name}`);
},
unsubscribe: function (fn) {
this.observers = this.observers.filter(function (item) {
if (item !== fn) {
return item;
}
});
console.log(`You are now unsubscribed from ${fn.name}`);
},
fire: function () {
this.observers.forEach(function (item) {
item.call();
});
}
};
بررسی متدها
1. متد subscribe
در این متد، تابع ورودی به آرایه observers اضافه میشود:
this.observers.push(fn);
سپس با استفاده از fn.name نام تابع نمایش داده میشود تا مشخص شود اشتراک با موفقیت انجام شده است.
2. متد unsubscribe
این متد با استفاده از filter، تابع موردنظر را از لیست observerها حذف میکند:
this.observers = this.observers.filter(function (item) {
if (item !== fn) {
return item;
}
});
در نتیجه، یک آرایه جدید ساخته میشود که همه توابع بهجز تابع موردنظر را نگه میدارد.
3. متد fire
این متد همه observerهای ذخیرهشده را اجرا میکند:
this.observers.forEach(function (item) {
item.call();
});
با این کار، هر تابعی که قبلاً subscribe شده باشد، هنگام fire اجرا خواهد شد.
ساخت رابط کاربری ساده
برای نمایش عملکرد Observer Pattern، یک رابط کاربری ساده با چند دکمه میسازیم:
<button class="sub-ms">Subscribe to Milliseconds</button>
<button class="unsub-ms">Unsubscribe from Milliseconds</button>
<br /><br />
<button class="sub-s">Subscribe to Seconds</button>
<button class="unsub-s">Unsubscribe from Seconds</button>
<br /><br />
<button class="fire">Fire</button>
این دکمهها برای subscribe و unsubscribe کردن دو تابع مختلف و همچنین اجرای رویداد استفاده میشوند.
ساخت نمونهای از EventObserver
اکنون یک نمونه از observer میسازیم:
const click = new EventObserver();
تعریف توابعی که قرار است subscribe شوند
در این مثال، دو تابع داریم:
- یک تابع برای نمایش میلیثانیه فعلی
- یک تابع برای نمایش ثانیه فعلی
const getCurMilliseconds = function () {
console.log(`Current Milliseconds: ${new Date().getMilliseconds()}`);
};
const getCurSeconds = function () {
console.log(`Current Seconds: ${new Date().getSeconds()}`);
};
اتصال دکمهها به رویدادها
حالا دکمهها را به عملیات subscribe، unsubscribe و fire متصل میکنیم:
document.querySelector('.sub-ms').addEventListener('click', function () {
click.subscribe(getCurMilliseconds);
});
document.querySelector('.unsub-ms').addEventListener('click', function () {
click.unsubscribe(getCurMilliseconds);
});
document.querySelector('.sub-s').addEventListener('click', function () {
click.subscribe(getCurSeconds);
});
document.querySelector('.unsub-s').addEventListener('click', function () {
click.unsubscribe(getCurSeconds);
});
document.querySelector('.fire').addEventListener('click', function () {
click.fire();
});
روند اجرای مثال
رفتار این برنامه به شکل زیر است:
- اگر روی
Fireکلیک کنیم و هنوز تابعی subscribe نشده باشد، هیچ اتفاقی نمیافتد. - اگر روی
Subscribe to Millisecondsکلیک کنیم، تابع نمایش میلیثانیه ثبت میشود. - اگر سپس روی
Fireکلیک کنیم، مقدار میلیثانیه فعلی نمایش داده میشود. - اگر تابع ثانیه را هم subscribe کنیم، با هر بار
Fireهر دو تابع اجرا میشوند. - اگر یکی از آنها unsubscribe شود، فقط تابع باقیمانده اجرا خواهد شد.
نسخه کامل مثال با Prototype
در ادامه، کل مثال را یکجا میبینید:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Observer Pattern</title>
</head>
<body>
<button class="sub-ms">Subscribe to Milliseconds</button>
<button class="unsub-ms">Unsubscribe from Milliseconds</button>
<br /><br />
<button class="sub-s">Subscribe to Seconds</button>
<button class="unsub-s">Unsubscribe from Seconds</button>
<br /><br />
<button class="fire">Fire</button>
<script>
function EventObserver() {
this.observers = [];
}
EventObserver.prototype = {
subscribe: function (fn) {
this.observers.push(fn);
console.log(`You are now subscribed to ${fn.name}`);
},
unsubscribe: function (fn) {
this.observers = this.observers.filter(function (item) {
if (item !== fn) {
return item;
}
});
console.log(`You are now unsubscribed from ${fn.name}`);
},
fire: function () {
this.observers.forEach(function (item) {
item.call();
});
}
};
const click = new EventObserver();
const getCurMilliseconds = function () {
console.log(`Current Milliseconds: ${new Date().getMilliseconds()}`);
};
const getCurSeconds = function () {
console.log(`Current Seconds: ${new Date().getSeconds()}`);
};
document.querySelector('.sub-ms').addEventListener('click', function () {
click.subscribe(getCurMilliseconds);
});
document.querySelector('.unsub-ms').addEventListener('click', function () {
click.unsubscribe(getCurMilliseconds);
});
document.querySelector('.sub-s').addEventListener('click', function () {
click.subscribe(getCurSeconds);
});
document.querySelector('.unsub-s').addEventListener('click', function () {
click.unsubscribe(getCurSeconds);
});
document.querySelector('.fire').addEventListener('click', function () {
click.fire();
});
</script>
</body>
</html>
پیادهسازی با ES6 Class
همین ساختار را میتوان با استفاده از classهای ES6 نیز نوشت. در این حالت، بهجای prototype از syntax جدید class استفاده میکنیم.
class EventObserver {
constructor() {
this.observers = [];
}
subscribe(fn) {
this.observers.push(fn);
console.log(`You are now subscribed to ${fn.name}`);
}
unsubscribe(fn) {
this.observers = this.observers.filter((item) => {
if (item !== fn) {
return item;
}
});
console.log(`You are now unsubscribed from ${fn.name}`);
}
fire() {
this.observers.forEach((item) => item.call());
}
}
تفاوت Prototype و Class
از نظر عملکرد، این دو پیادهسازی در این مثال نتیجه یکسانی دارند. تفاوت اصلی در نحوه نگارش است.
- در نسخه
prototype، متدها بهصورت دستی روی prototype قرار میگیرند. - در نسخه
class، کد خواناتر و مدرنتر نوشته میشود. - در عمل،
classدر JavaScript نوعی syntax سادهتر برای همان مفاهیم prototype-based است.
چرا Observer Pattern مهم است؟
این الگو به ما کمک میکند وابستگی مستقیم بین اجزای برنامه را کاهش دهیم. بهجای اینکه هر بخش مستقیماً دیگری را صدا بزند، فقط به رویدادها گوش میدهد و در زمان مناسب واکنش نشان میدهد.
این ویژگی باعث میشود:
- کد ماژولارتر شود
- توسعهپذیری بیشتر شود
- نگهداری برنامه آسانتر شود
- افزودن قابلیتهای جدید سادهتر انجام شود
جمعبندی
در این بخش با Observer Pattern در JavaScript آشنا شدیم. این الگو برای مدیریت اشتراک روی رویدادها بسیار مناسب است و امکان subscribe، unsubscribe و اجرای مجموعهای از توابع را فراهم میکند.
همچنین دیدیم که این الگو را میتوان هم با prototype و هم با ES6 class پیادهسازی کرد. اگر در برنامهای نیاز دارید چند بخش مختلف به یک رویداد واکنش نشان دهند، Observer Pattern یکی از بهترین گزینههاست.