أنماط التصميم في جافا

نمط المراقب (Observer)

15 دقيقة الدرس 6 من 13

نمط المراقب (Observer)

يُعرِّف نمط المراقب علاقة تبعية من نوع "واحد-إلى-متعدد" بين الكائنات: حين يغيّر كائن ما (يُسمّى الموضوع أو الناشر) حالتَه، يُخطَر جميع التابعون له (المراقبون أو المستمعون) تلقائيًا. هذا هو النمط الأساسي الذي يقوم عليه كل نظام قائم على الأحداث استخدمتَه من قبل — من مستمعي Swing ورد فعل Android، إلى أحداث تطبيق Spring والتدفقات التفاعلية.

المشكلة التي يحلّها

بدون نمط المراقب، يضطر الموضوع الذي يريد إخطار الآخرين إلى الاحتفاظ بمراجع مُرمَّجة لكل طرف مهتم. أضِف مستهلكًا جديدًا وستضطر إلى تعديل الموضوع — وهذا انتهاك صريح لمبدأ الفتح/الإغلاق. يُفصِل نمط المراقب منتج الأحداث عن مستهلكيها، فيستطيع كل طرف التطور باستقلالية.

مصطلحات النشر/الاشتراك: "Subject/Observer" هو المصطلح الكلاسيكي من كتاب GoF. تستخدم الأنظمة الحديثة غالبًا "Publisher/Subscriber" أو "EventEmitter/Listener". الآلية متطابقة؛ والتسمية تعكس السياق فحسب. ستجد كلا الشكلين في قاعدة كود Java الواحدة.

البنية الكلاسيكية لـ GoF في Java

يتطلب الحد الأدنى واجهتين وموضوعًا واحدًا ملموسًا:

// عقد المراقب public interface Observer { void update(String eventType, Object payload); } // عقد الموضوع public interface Subject { void addObserver(Observer o); void removeObserver(Observer o); void notifyObservers(String eventType, Object payload); }

يخزّن الموضوع الملموس مراقبيه في قائمة ويُوزّع كل إشعار عليهم:

import java.util.ArrayList; import java.util.List; import java.util.Objects; public class EventBus implements Subject { private final List<Observer> observers = new ArrayList<>(); @Override public void addObserver(Observer o) { Objects.requireNonNull(o, "Observer must not be null"); observers.add(o); } @Override public void removeObserver(Observer o) { observers.remove(o); } @Override public void notifyObservers(String eventType, Object payload) { // التكرار على نسخة لتفادي ConcurrentModificationException // إذا أزال مراقب نفسه أثناء الإشعار List.copyOf(observers).forEach(o -> o.update(eventType, payload)); } }
كرِّر دائمًا على نسخة من القائمة. استدعاء List.copyOf(observers) قبل الحلقة يمنع ConcurrentModificationException حين يُلغي مراقب تسجيلَ نفسه استجابةً لاستدعائه — وهو سيناريو شائع جدًا في الإنتاج.

مثال عملي: معالجة الطلبيات

تخيّل خدمة تجارة إلكترونية حيث يجب أن يُطلق تسجيل طلب جديد عدة تأثيرات جانبية — إرسال بريد تأكيد، وتحديث المخزون، وتسجيل حدث تحليلي. مع نمط المراقب يُسجّل كل اهتمام باستقلالية:

// سجل حدث بأنواع محكمة (Java 16+) public record OrderEvent(long orderId, String status) {} // ثلاثة مراقبون مستقلون public class EmailNotifier implements Observer { @Override public void update(String eventType, Object payload) { if ("ORDER_PLACED".equals(eventType) && payload instanceof OrderEvent oe) { System.out.println("Sending confirmation email for order " + oe.orderId()); } } } public class InventoryService implements Observer { @Override public void update(String eventType, Object payload) { if ("ORDER_PLACED".equals(eventType) && payload instanceof OrderEvent oe) { System.out.println("Reserving stock for order " + oe.orderId()); } } } public class AnalyticsTracker implements Observer { @Override public void update(String eventType, Object payload) { System.out.println("Analytics: event=" + eventType + " payload=" + payload); } }
// ربط الأجزاء معًا EventBus bus = new EventBus(); bus.addObserver(new EmailNotifier()); bus.addObserver(new InventoryService()); bus.addObserver(new AnalyticsTracker()); // نشر حدث bus.notifyObservers("ORDER_PLACED", new OrderEvent(42L, "PLACED")); // مخرجات الكونسول: // Sending confirmation email for order 42 // Reserving stock for order 42 // Analytics: event=ORDER_PLACED payload=OrderEvent[orderId=42, status=PLACED]

خدمة OrderService التي تستدعي notifyObservers لا تعرف شيئًا عن البريد الإلكتروني أو المخزون أو التحليلات. أضِف تأثيرًا جانبيًا جديدًا بكتابة صف جديد فحسب — دون أي تغيير في الكود الموجود.

حافلة الأحداث ذات النوع المحكم والموضوعات المفلترة

نادرًا ما تستخدم أنظمة الإنتاج قائمة مراقبين واحدة. تحسين شائع هو تعيين أنواع الأحداث لقوائم مستمعين منفصلة، متجنبًا بذلك رقصة instanceof داخل كل مراقب:

import java.util.*; import java.util.function.Consumer; public class TypedEventBus { private final Map<Class<?>, List<Consumer<?>>> listeners = new HashMap<>(); public <T> void subscribe(Class<T> eventType, Consumer<T> listener) { listeners.computeIfAbsent(eventType, k -> new ArrayList<>()).add(listener); } @SuppressWarnings("unchecked") public <T> void publish(T event) { List<Consumer<?>> targets = listeners.getOrDefault(event.getClass(), List.of()); List.copyOf(targets).forEach(l -> ((Consumer<T>) l).accept(event)); } }
TypedEventBus bus = new TypedEventBus(); // مراقبون بصيغة lambda — بدون حاجة لتطبيق واجهات منفصلة bus.subscribe(OrderEvent.class, e -> System.out.println("Email for order " + e.orderId())); bus.subscribe(OrderEvent.class, e -> System.out.println("Inventory for order " + e.orderId())); bus.publish(new OrderEvent(99L, "PLACED"));

استخدام Consumer<T> مع lambda يُزيل الحاجة إلى كتابة صفوف مراقب منفصلة للتفاعلات البسيطة — وهذا أقرب بكثير لكيفية تعامل أطر العمل الحديثة مع الأحداث داخليًا.

دعم Java المدمج للمراقب

شحنت Java دعمًا للمراقب بأشكال متعددة على مرّ السنين:

  • java.util.Observer / java.util.Observable — مُهمَلان منذ Java 9. تجنّبهما في الكود الجديد: سلامة الخيوط معطوبة والتصميم غير مرن.
  • java.beans PropertyChangeSupport — لا يزال يُستخدم في كود سطح المكتب/Swing لإشعارات تغيير الخصائص.
  • Flow API (java.util.concurrent.Flow) — تدفقات تفاعلية من Java 9+: Publisher وSubscriber وSubscription وProcessor. واعية بالضغط الخلفي؛ الأساس الحديث للبرمجة التفاعلية.
  • Spring ApplicationEvent — يلفّ Spring نمط المراقب خلف ApplicationEventPublisher و@EventListener، مقدمًا معالجة أحداث تصريحية قادرة على العمل غير المتزامن مع حقن الاعتمادية.
لا تستخدم java.util.Observable أبدًا. أُهمِل في Java 9 لأسباب وجيهة: الإشعار غير آمن للخيوط، وتصميمه يربط المراقبين بصف أساسي ملموس بدلًا من واجهة. استخدم Flow API أو حافلة أحداث مخصصة أو إطار عمل مثل Spring Events.

اعتبارات الخيوط والذاكرة

تتفاجأ معظم الفرق بمشكلتين في بيئة الإنتاج:

  1. سلامة الخيوط. إذا سجّل مراقبون أو أُزيلوا من خيوط متعددة، فلا بد من حماية القائمة. استبدل ArrayList بـ CopyOnWriteArrayList للحصول على بديل آمن للخيوط خالٍ من الأقفال يحلّ مشكلة النسخة تلقائيًا أيضًا.
  2. تسرّب الذاكرة. موضوع يحتفظ بمراجع قوية للمراقبين يُبقيهم أحياء إلى أجل غير مسمى. أُطر واجهة المستخدم (Swing, JavaFX, Android) مشهورة بهذا: تسجّل مستمعًا في activity/fragment دون إلغاء تسجيله قط، فتظل التسلسل الهرمي للعرض بأكمله في الذاكرة. اقرن دائمًا addObserver بـ removeObserver مقابلة في أسلوب إيقاف دورة الحياة.
import java.util.concurrent.CopyOnWriteArrayList; public class ThreadSafeEventBus implements Subject { // CopyOnWriteArrayList: قراءات آمنة للخيوط، نسخ عند الكتابة private final CopyOnWriteArrayList<Observer> observers = new CopyOnWriteArrayList<>(); @Override public void addObserver(Observer o) { observers.addIfAbsent(o); } @Override public void removeObserver(Observer o) { observers.remove(o); } @Override public void notifyObservers(String type, Object payload) { // مكرِّر CopyOnWriteArrayList هو نسخة أصلًا — لا حاجة لنسخ يدوي observers.forEach(o -> o.update(type, payload)); } }

المقايضات ومتى تستخدم نمط المراقب

  • استخدمه عندما يجب أن تتفاعل مكوّنات متعددة غير مترابطة مع تغيّرات الحالة وتريد إبقاء الناشر جاهلًا بمستهلكيه.
  • احذر من عواصف الإشعارات المتتالية — مراقب يُطلق حدثًا آخر قد يخلق سلاسل يصعب تتبّعها. سجّل إرسال الأحداث في بيئة التطوير.
  • فضّل الحافلات ذات الأنواع المحكمة أو أُطر العمل (Spring Events, Guava EventBus, RxJava) على الحافلات المبنية يدويًا بمفاتيح نصية في الإنتاج — فهي تضيف أمانًا وقت الترجمة ودعمًا للعمل غير المتزامن.
  • فكّر في التدفقات التفاعلية (Project Reactor, RxJava) حين تحتاج إلى ضغط خلفي أو تركيب أو خطوط معالجة غير متزامنة معقدة؛ نمط المراقب بذاته هو إطلاق ونسيان.

الخلاصة

نمط المراقب هو محرّك البنية القائمة على الأحداث. جوهره عقد بين ناشر لا يعرف من يستمع وأي عدد من المستمعين لا يعرف أحدهم الآخر. في Java الحديثة يتجلّى ذلك في حافلات الأحداث القائمة على lambda، وواجهة Flow API، والتجريدات على مستوى أُطر العمل مثل Spring Events. المهارات الاحترافية الأساسية هي: الحفاظ على سلامة قائمة المراقبين للخيوط، ومنع تسرّب الذاكرة بإلغاء التسجيل بانضباط، ومعرفة متى تلجأ إلى تجريد تفاعلي أعلى مستوى بدلًا من بناء حافلتك الخاصة.