در قسمت اول دیدیم که Event Emitter چطور یک اتفاق را به چند شنونده اعلام می‌کند. آن نمونه برای شروع مناسب بود، اما راهی برای حذف شنونده نداشت. در یک برنامه واقعی، هر ارتباطی عمر مشخصی دارد و باید بتوانیم آن را تمام کنیم.

در قسمت دوم مجموعه «از رویداد تا معماری قابل کنترل» همین حلقه گمشده را اضافه می‌کنیم. با Observer آشنا می‌شویم و می‌بینیم چرا unsubscribe فقط یک امکان جانبی نیست، بلکه بخش ضروری طراحی اشتراک است.

در الگوی Observer یک منبع داده داریم که چند بخش تغییرات آن را دنبال می‌کنند. هر وقت وضعیت منبع عوض شود، همه دنبال‌کننده‌ها باخبر می‌شوند. بخش مهمی که گاهی فراموش می‌شود، قطع کردن همین ارتباط است.

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

در این الگو معمولا دو نقش داریم. Subject داده و فهرست دنبال‌کننده‌ها را نگه می‌دارد. Observer تابع یا شیئی است که با تغییر داده خبر می‌گیرد. یک subject می‌تواند چند observer داشته باشد و هر observer هم می‌تواند در زمان دلخواه ارتباطش را قطع کند.

یک Observer ساده

به جای آرایه از Set استفاده می‌کنیم تا یک تابع دوبار ثبت نشود و حذف آن هم مستقیم باشد.

class PriceStore {
  constructor(price) {
    this.price = price;
    this.observers = new Set();
  }

  subscribe(observer) {
    this.observers.add(observer);

    return () => {
      this.observers.delete(observer);
    };
  }

  setPrice(price) {
    this.price = price;
    for (const observer of this.observers) observer(price);
  }
}

const store = new PriceStore(60000);

const unsubscribe = store.subscribe(price => {
  console.log(`قیمت جدید: ${price}`);
});

store.setPrice(61000); // قیمت جدید: 61000
unsubscribe();
store.setPrice(62000); // چیزی چاپ نمی‌شود

خود متد subscribe یک تابع برمی‌گرداند. این تابع همان observer را از مجموعه حذف می‌کند. این شکل از API کاربردی است چون مصرف‌کننده لازم نیست برای لغو اشتراک، نام رویداد یا شناسه جداگانه‌ای نگه دارد.

استفاده از closure باعث می‌شود تابع برگشتی هنوز به همان observer دسترسی داشته باشد. حتی بعد از تمام شدن اجرای subscribe، مرجع لازم برای حذف در دسترس می‌ماند. متد delete روی Set هم اگر عضو قبلا حذف شده باشد خطا نمی‌دهد، پس صدا زدن دوباره unsubscribe بی‌خطر است.

جریان اجرا را دنبال کنیم

در ابتدا observers خالی است. با اجرای subscribe، تابع نمایش قیمت وارد Set می‌شود. فراخوانی اول setPrice قیمت را عوض می‌کند و observer اجرا می‌شود. سپس unsubscribe همان تابع را حذف می‌کند. فراخوانی دوم setPrice هنوز مقدار داخلی store را تغییر می‌دهد، اما دیگر کسی برای دریافت اعلان وجود ندارد.

کاربرد در رابط کاربری

اگر این کد را در یک کامپوننت استفاده کنیم، هنگام ساخته شدن صفحه subscribe را صدا می‌زنیم و هنگام حذف صفحه، تابع unsubscribe را اجرا می‌کنیم. در React این کار معمولا در تابع cleanup مربوط به useEffect انجام می‌شود. در برنامه‌های بدون فریم‌ورک هم قبل از حذف عنصر یا تغییر صفحه باید همین پاک‌سازی انجام شود.

یک کاربرد دیگر اتصال WebSocket است. ممکن است چند بخش صفحه پیام‌های یک اتصال را دنبال کنند. با بسته شدن هر بخش، فقط اشتراک همان بخش لغو می‌شود و اتصال یا بقیه شنونده‌ها به کار خود ادامه می‌دهند.

تفاوت Observer با Pub-Sub

در Observer معمولا observerها مستقیما به یک subject یا منبع مشخص وصل می‌شوند. در Pub-Sub یک کانال یا واسطه بین آن‌ها قرار می‌گیرد و دو طرف ممکن است اصلا همدیگر را نشناسند. برای یک store محلی، Observer انتخاب ساده‌ای است. برای ارتباط چند سرویس، Pub-Sub معمولا مناسب‌تر است.

هر اشتراکی باید راه خروج داشته باشد. اگر API اشتراک می‌سازیم، بهتر است از همان ابتدا unsubscribe را هم بخشی از قرارداد آن بدانیم، نه قابلیتی که بعدا اضافه شود.

چند نکته مهم

اگر observer هنگام اعلان خودش را حذف کند، Set این وضعیت را به شکل قابل پیش‌بینی مدیریت می‌کند. با این حال، اضافه یا حذف کردن observerها وسط پیمایش ممکن است رفتار مورد انتظار تیم را مبهم کند. برای سیستم‌های حساس می‌توان قبل از اعلان با [...this.observers] یک snapshot ساخت.

همچنین بهتر است مشخص کنیم observer تازه ثبت‌شده باید همان لحظه مقدار فعلی را دریافت کند یا فقط تغییرهای بعدی را ببیند. نمونه ما فقط تغییرهای بعدی را اعلام می‌کند. بسیاری از storeهای رابط کاربری مقدار فعلی را بلافاصله پس از subscribe هم ارسال می‌کنند.

ادامه دارد

در این قسمت یک بخش مهم از ارتباط‌ها را کامل کردیم. قسمت سوم چند روز دیگر منتشر می‌شود و مسیر را از همین نقطه ادامه می‌دهد.