مشروع: نظام استدعاء صغير (Callback System)
على مدار هذا الدرس تعلّمت بناء جملة Lambda، والواجهات الوظيفية، وPredicate وFunction وConsumer وSupplier ومراجع الدوال وتركيب السلوك. في هذا الدرس الختامي ستجمع كل هذه الأفكار في مشروع صغير لكنه واقعي: نظام استدعاء / حافلة أحداث مصغّرة (Event Bus) مبنيّ بالكامل على الواجهات الوظيفية — دون مكتبات خارجية أو أطر عمل ثقيلة.
الهدف ليس بناء نظام أحداث للإنتاج، بل هو رؤية كيف تحلّ Lambda محلّ الكود المتكرّر، وكيف تخزّن السلوك وتركّبه كبيانات، وسبب مرونة هذا التصميم.
ما الذي سنبنيه
سنبني كلاس EventBus يتيح لك:
- تسجيل مستمعين متعددين من نوع
Consumer<T> لموضوع حدث محدّد بالاسم.
- إطلاق (نشر) حدث يستدعي كل مستمع مسجَّل بالترتيب.
- تسجيل مستمعين يعملون مرّة واحدة فقط ثم يلغي اشتراكه تلقائيًا.
- إضافة مرشّح
Predicate<T> بحيث لا يُطلق المستمع إلا عند استيفاء شرط معيّن.
يقل التطبيق الكامل عن 80 سطرًا من Java ولا يستخدم سوى java.util.function والمجموعات القياسية.
الخطوة الأولى — الـ EventBus الأساسي
ابدأ بأبسط نسخة: خريطة من سلسلة الموضوع إلى قائمة من ردود الاستدعاء Consumer.
import java.util.*;
import java.util.function.Consumer;
public class EventBus<T> {
// كل موضوع يحمل قائمة مرتّبة من المستهلكين
private final Map<String, List<Consumer<T>>> listeners = new HashMap<>();
/** تسجيل مستمع لموضوع معيّن. */
public void on(String topic, Consumer<T> listener) {
listeners
.computeIfAbsent(topic, k -> new ArrayList<>())
.add(listener);
}
/** إطلاق حدث ما يستدعي كل مستمع مسجَّل لذلك الموضوع. */
public void emit(String topic, T event) {
List<Consumer<T>> handlers = listeners.getOrDefault(topic, List.of());
handlers.forEach(h -> h.accept(event));
}
}
الاستخدام نظيف بالفعل:
EventBus<String> bus = new EventBus<>();
bus.on("login", user -> System.out.println("مرحبًا، " + user));
bus.on("login", user -> System.out.println("سجل التدقيق: " + user + " سجّل دخوله"));
bus.emit("login", "Alice");
// مرحبًا، Alice
// سجل التدقيق: Alice سجّل دخوله
لماذا Consumer<T>؟ رد الاستدعاء الذي يتفاعل مع حدث دون إرجاع قيمة هو بالضبط عقد Consumer. لا حاجة لاختراع واجهة — فالـ JDK يمتلك الشكل الصحيح بالفعل.
الخطوة الثانية — المستمعون لمرّة واحدة
المستمع once يُطلق عند أول حدث مطابق ثم يُزيل نفسه. الحيلة: لفّ المستهلك الذي يوفّره المستدعي داخل مستهلك آخر يلغي تسجيل نفسه قبل التفويض.
public void once(String topic, Consumer<T> listener) {
// نحتاج مرجعًا للغلاف لكي يزيل نفسه
Consumer<T>[] wrapperHolder = new Consumer[1];
wrapperHolder[0] = event -> {
listeners.getOrDefault(topic, List.of()).remove(wrapperHolder[0]);
listener.accept(event);
};
on(topic, wrapperHolder[0]);
}
حيلة المصفوفة أحادية العنصر هي حلّ شائع لقاعدة "المتغير المُقيَّد فعليًا" داخل Lambda. مرجع المصفوفة نفسه نهائي (final)، ومحتواها فقط هو ما يتغيّر. البديل هو حقل كلاس داخلي خاص — كلاهما صحيح.
الخطوة الثالثة — المستمعون المرشَّحون
أضف دالة onIf تقبل Predicate<T>. يعمل المستمع فقط حين يعيد المرشّح true. هذا يُبقي منطق الأعمال خارج المعالجات الفردية.
import java.util.function.Predicate;
public void onIf(String topic, Predicate<T> filter, Consumer<T> listener) {
on(topic, event -> {
if (filter.test(event)) {
listener.accept(event);
}
});
}
الآن يمكنك كتابة تسجيلات مشترك معبّرة:
EventBus<Order> orderBus = new EventBus<>();
// أخطر خدمة كشف الاحتيال فقط للطلبات الكبيرة
orderBus.onIf("order.placed",
order -> order.total() > 500.0,
order -> FraudService.check(order));
// سجّل كل طلب دائمًا
orderBus.on("order.placed", order -> Logger.log(order));
الخطوة الرابعة — تجميع كل شيء معًا
إليك عرضًا قابلًا للتشغيل يستخدم سجل بسيط كحمولة للحدث:
record UserEvent(String name, String action) {}
public class Main {
public static void main(String[] args) {
EventBus<UserEvent> bus = new EventBus<>();
// سجّل كل حدث دائمًا
bus.on("user", e -> System.out.println("[LOG] " + e.name() + " → " + e.action()));
// رحّب فقط بالمستخدمين الذين سجّلوا للتوّ (مرشَّح)
bus.onIf("user",
e -> e.action().equals("signup"),
e -> System.out.println("أهلًا بك، " + e.name() + "!"));
// تنبيه لمرّة واحدة لأول حدث في الجلسة
bus.once("user",
e -> System.out.println("أول حدث مستلَم: " + e));
bus.emit("user", new UserEvent("Alice", "signup"));
bus.emit("user", new UserEvent("Bob", "login"));
bus.emit("user", new UserEvent("Alice", "login"));
}
}
// [LOG] Alice → signup
// أهلًا بك، Alice!
// أول حدث مستلَم: UserEvent[name=Alice, action=signup]
// [LOG] Bob → login
// [LOG] Alice → login
أمان الخيوط المتعددة: هذا التطبيق أحادي الخيط. إن كنت تُطلق أحداثًا من خيوط متعددة، فاحتوِ تعديلات القائمة داخل كتل synchronized أو استبدل ArrayList بـ CopyOnWriteArrayList. حدّد دائمًا متطلبات التزامن قبل اختيار أبسط بنية بيانات.
دروس التصميم المستخلَصة
- السلوك كبيانات: Lambda المخزّنة في
List<Consumer<T>> هي قيم من الدرجة الأولى يمكن إضافتها وإزالتها والتكرار عليها — تمامًا كأي كائن آخر.
- التركيب دون الوراثة:
onIf تجمع Predicate وConsumer داخل Consumer جديد محليًا، دون الحاجة لأي كلاس فرعي.
- المفتوح/المغلق: لإضافة نوع مستمع جديد (مقيَّد، غير متزامن، مسجَّل)، تلفّ المستهلك الموجود — ولا تعدّل الحافلة نفسها قط.
- قابلية الاختبار: لأن كل معالج مجرّد دالة، يمكنك تمرير نسخ اختبارية (تأكيدات كـ Lambda) بدون أي إطار محاكاة.
الخلاصة
لقد بنيت نظام استدعاء وظيفيًا قابلًا للتركيب في أقل من 80 سطرًا بتطبيق كل مفهوم من هذا الدرس: Lambda كتطبيقات مستمعة، وConsumer لمعالجة الأحداث، وPredicate للتصفية، واستيعاب المتغيرات لغلاف once، وتركيب الدوال المضمَّن في onIf. هذا هو جوهر Java بالأسلوب الوظيفي — قطع سلوك صغيرة ذات أسماء واضحة متصلة ببعضها بدلًا من تسلسلات هرمية عميقة من الكلاسات.