مشروع: تطبيق الأنماط معًا
الأنماط الفردية أدوات. الهندسة الحقيقية تكمن في معرفة أي الأدوات تختار وكيف توصّلها دون إفراط في التعقيد. في هذا الدرس الختامي ستبني نظام إرسال إشعارات صغيرًا لكنه واقعي — مجال يظهر في كل قاعدة كود إنتاجية تقريبًا — مع تأليف خمسة أنماط عمدًا: Singleton وFactory Method وBuilder وStrategy وObserver. بعد إدخال كل نمط إلى الكود سترى لماذا اختير على البدائل.
قواعد هذا المشروع: أبقِ كل صنف صغيرًا ومحدّد الغرض. فضّل التركيب على الوراثة. دَع الأنماط تنبثق من متطلبات حقيقية — لا تلجأ لنمط إلا حين تدفعك مشكلة ملموسة إليه.
المجال: إرسال الإشعارات
يجب أن يستطيع النظام:
- دعم قنوات توصيل متعددة (بريد إلكتروني، رسائل نصية، إشعارات فورية) — مع إمكانية إضافة قنوات جديدة دون لمس الكود الحالي.
- بناء حمولات إشعارات معقّدة عبر واجهة برمجية طليقة بدون مُنشئ متداخل الأعداد.
- توجيه الإشعارات إلى استراتيجية إرسال قابلة للتهيئة (فورية أو دفعية أو مقيّدة) يمكن تبديلها أثناء التشغيل.
- السماح للمكوّنات المهتمة (مسجّل التدقيق، عدّاد المقاييس، شارة الواجهة) بالتفاعل عند إرسال إشعار دون اقترانها بالمُرسِل.
- الاحتفاظ بنسخة واحدة مشتركة من المُرسِل عبر التطبيق.
الخطوة 1 — Singleton: المُرسِل NotificationDispatcher
يمتلك المُرسِل سجل القنوات وقائمة التسليم. يجب أن يستخدم كل جزء من التطبيق النسخة ذاتها — حالة استخدام كلاسيكية لـ Singleton. استخدم نمط الـ enum؛ فهو آمن للخيوط بموجب مواصفات JVM وحصين ضد هجمات الانعكاس.
// Singleton آمن للخيوط عبر enum
public enum NotificationDispatcher {
INSTANCE;
private DeliveryStrategy strategy = new ImmediateDeliveryStrategy();
private final List<DispatchObserver> observers = new ArrayList<>();
private final Map<String, NotificationChannel> channels = new HashMap<>();
public void registerChannel(NotificationChannel channel) {
channels.put(channel.channelName(), channel);
}
public void setStrategy(DeliveryStrategy strategy) {
this.strategy = strategy;
}
public void addObserver(DispatchObserver observer) {
observers.add(observer);
}
public void dispatch(Notification notification) {
NotificationChannel channel = channels.get(notification.channel());
if (channel == null) throw new IllegalArgumentException(
"No channel registered: " + notification.channel());
strategy.deliver(notification, channel);
observers.forEach(o -> o.onDispatched(notification));
}
}
لماذا Singleton بالـ enum بدلًا من القفل المزدوج؟ يضمن JVM أن ثوابت الـ enum تُهيَّأ مرة واحدة بالضبط لكل محمّل أصناف، مما يجعل النمط آمنًا للخيوط بالتصميم. يتطلّب القفل المزدوج حقلًا volatile واستدلالًا دقيقًا حول رؤية الذاكرة — نمط الـ enum أبسط وصحيح بالبناء.
الخطوة 2 — Factory Method: قنوات الإشعارات
لكل قناة (بريد إلكتروني، رسائل نصية، إشعارات فورية) آلية إرسال مختلفة تمامًا. تعزل طريقة المصنع قرار الإنشاء خلف واجهة مستقرة، حتى لا يعتمد المُرسِل أبدًا على أصناف القنوات المحدّدة.
// واجهة المنتج
public interface NotificationChannel {
String channelName();
void send(Notification notification);
}
// المنتجات المحدّدة
public class EmailChannel implements NotificationChannel {
@Override public String channelName() { return "email"; }
@Override public void send(Notification n) {
System.out.printf("[EMAIL] To: %s | Subject: %s%n",
n.recipient(), n.subject());
}
}
public class SmsChannel implements NotificationChannel {
@Override public String channelName() { return "sms"; }
@Override public void send(Notification n) {
System.out.printf("[SMS] To: %s | Body: %s%n",
n.recipient(), n.body());
}
}
public class PushChannel implements NotificationChannel {
@Override public String channelName() { return "push"; }
@Override public void send(Notification n) {
System.out.printf("[PUSH] Device: %s | Title: %s%n",
n.recipient(), n.subject());
}
}
// المصنع — يفصل إنشاء القناة عن المُرسِل
public class ChannelFactory {
public static NotificationChannel create(String type) {
return switch (type.toLowerCase()) {
case "email" -> new EmailChannel();
case "sms" -> new SmsChannel();
case "push" -> new PushChannel();
default -> throw new IllegalArgumentException("Unknown channel: " + type);
};
}
}
الخطوة 3 — Builder: بناء كائنات Notification
يحمل الـ Notification متلقيًا وقناةً وموضوعًا ونصًا وأولويةً وبيانات وصفية اختيارية. مُنشئ متداخل الأعداد سيحتاج ستة تعريفات زائدة. يحلّ نمط Builder هذا بواجهة برمجية طليقة مقروءة ويتحقّق من الحقول المطلوبة عند استدعاء build().
// كائن قيمة غير قابل للتعديل
public record Notification(
String recipient,
String channel,
String subject,
String body,
int priority,
Map<String, String> metadata
) {
public static Builder builder() { return new Builder(); }
public static class Builder {
private String recipient;
private String channel;
private String subject = "(no subject)";
private String body = "";
private int priority = 5;
private final Map<String, String> metadata = new LinkedHashMap<>();
public Builder to(String recipient) { this.recipient = recipient; return this; }
public Builder via(String channel) { this.channel = channel; return this; }
public Builder subject(String subject) { this.subject = subject; return this; }
public Builder body(String body) { this.body = body; return this; }
public Builder priority(int priority) { this.priority = priority; return this; }
public Builder meta(String key, String val) { metadata.put(key, val); return this; }
public Notification build() {
Objects.requireNonNull(recipient, "recipient is required");
Objects.requireNonNull(channel, "channel is required");
return new Notification(recipient, channel, subject, body,
priority, Map.copyOf(metadata));
}
}
}
السجلات Records كأهداف للـ Builder: تُولّد سجلات Java (Java 16+) أساليب equals وhashCode ومُنشئًا قانونيًا تلقائيًا. إقران سجل بـ Builder متداخل يمنحك عدم قابلية التعديل مع واجهة بناء ممتعة دون كتابة أي كود وصولي.
الخطوة 4 — Strategy: سلوك التسليم
يجب أن يدعم المُرسِل ثلاثة أوضاع توصيل دون تضمينها بشكل ثابت. يستخرج نمط Strategy كل وضع إلى صنفه الخاص ويتيح للمُستدعي تبديل الاستراتيجيات أثناء التشغيل — دون أي if/else في المُرسِل ذاته.
// واجهة الاستراتيجية
public interface DeliveryStrategy {
void deliver(Notification notification, NotificationChannel channel);
}
// الاستراتيجية أ: إرسال فوري
public class ImmediateDeliveryStrategy implements DeliveryStrategy {
@Override
public void deliver(Notification n, NotificationChannel ch) {
ch.send(n);
}
}
// الاستراتيجية ب: دفعي مع تفريغ دوري
public class BatchDeliveryStrategy implements DeliveryStrategy {
private final List<Map.Entry<Notification, NotificationChannel>> batch = new ArrayList<>();
private final int batchSize;
public BatchDeliveryStrategy(int batchSize) { this.batchSize = batchSize; }
@Override
public void deliver(Notification n, NotificationChannel ch) {
batch.add(Map.entry(n, ch));
if (batch.size() >= batchSize) flush();
}
public void flush() {
batch.forEach(e -> e.getValue().send(e.getKey()));
batch.clear();
System.out.println("[BATCH] Flushed " + batchSize + " notifications");
}
}
// الاستراتيجية ج: مقيّدة — بحدٍّ أقصى N في الدقيقة (مبسّطة)
public class ThrottledDeliveryStrategy implements DeliveryStrategy {
private final int maxPerMinute;
private int sentThisMinute = 0;
public ThrottledDeliveryStrategy(int maxPerMinute) {
this.maxPerMinute = maxPerMinute;
}
@Override
public void deliver(Notification n, NotificationChannel ch) {
if (sentThisMinute >= maxPerMinute) {
System.out.println("[THROTTLE] Rate limit reached — dropping: " + n.subject());
return;
}
ch.send(n);
sentThisMinute++;
}
}
الخطوة 5 — Observer: التفاعل مع الإشعارات المُرسَلة
يحتاج مسجّل التدقيق وعدّاد المقاييس وشارة الواجهة إلى معرفة وقت إرسال إشعار — لكن لا ينبغي لأي منها الاقتران بحلقة إرسال المُرسِل. يتيح نمط Observer اشتراكها باستقلالية.
// واجهة المراقِب
public interface DispatchObserver {
void onDispatched(Notification notification);
}
// المراقِبون المحدّدون
public class AuditLogger implements DispatchObserver {
@Override
public void onDispatched(Notification n) {
System.out.printf("[AUDIT] channel=%s recipient=%s subject=%s priority=%d%n",
n.channel(), n.recipient(), n.subject(), n.priority());
}
}
public class MetricsCounter implements DispatchObserver {
private final Map<String, Long> counts = new HashMap<>();
@Override
public void onDispatched(Notification n) {
counts.merge(n.channel(), 1L, Long::sum);
}
public void printReport() {
counts.forEach((ch, cnt) ->
System.out.printf("[METRICS] %s: %d sent%n", ch, cnt));
}
}
توصيل كل شيء معًا
أسلوب main أدناه هو المكان الوحيد الذي يعرف الأنواع المحدّدة. كل ما يلمسه المُرسِل يُتعامل معه عبر واجهات.
import java.util.*;
public class Main {
public static void main(String[] args) {
// --- المصنع: بناء القنوات وتسجيلها مع مُرسِل Singleton ---
NotificationDispatcher dispatcher = NotificationDispatcher.INSTANCE;
dispatcher.registerChannel(ChannelFactory.create("email"));
dispatcher.registerChannel(ChannelFactory.create("sms"));
dispatcher.registerChannel(ChannelFactory.create("push"));
// --- المراقِب: إرفاق المستمعين ---
AuditLogger audit = new AuditLogger();
MetricsCounter metrics = new MetricsCounter();
dispatcher.addObserver(audit);
dispatcher.addObserver(metrics);
// --- الاستراتيجية: البدء بالإرسال الفوري ---
dispatcher.setStrategy(new ImmediateDeliveryStrategy());
// --- البنّاء: بناء الإشعارات بشكل طليق ---
Notification welcome = Notification.builder()
.to("alice@example.com")
.via("email")
.subject("Welcome to the platform")
.body("Hi Alice, your account is ready.")
.priority(3)
.meta("template", "welcome-v2")
.build();
Notification otp = Notification.builder()
.to("+447700900123")
.via("sms")
.subject("OTP")
.body("Your code is 482910")
.priority(1)
.build();
Notification alert = Notification.builder()
.to("device-token-xyz")
.via("push")
.subject("New message")
.body("You have an unread message.")
.priority(5)
.build();
// الإرسال — كل استدعاء يُشغّل channel.send() ثم جميع المراقِبين
dispatcher.dispatch(welcome);
dispatcher.dispatch(otp);
dispatcher.dispatch(alert);
// --- تبديل الاستراتيجية: التحوّل إلى الدفعي في منتصف التشغيل ---
BatchDeliveryStrategy batch = new BatchDeliveryStrategy(2);
dispatcher.setStrategy(batch);
dispatcher.dispatch(Notification.builder().to("bob@example.com")
.via("email").subject("Newsletter").body("Issue #42").build());
dispatcher.dispatch(Notification.builder().to("bob@example.com")
.via("email").subject("Invoice ready").body("See attachment.").build());
// تم بلوغ batchSize=2 — يُنفَّذ التفريغ التلقائي هنا
metrics.printReport();
}
}
قراءة مسار الاستدعاء: حين ينفَّذ dispatcher.dispatch(welcome)، يتدفّق التحكّم عبر (1) enum Singleton للحصول على EmailChannel المسجّلة؛ (2) ImmediateDeliveryStrategy الحالية التي تفوّض إلى channel.send()؛ (3) كل DispatchObserver بالترتيب. لا يعلم أيٌّ من الأصناف المنفّذة لهذه الواجهات بوجود الآخرين — كل التنسيق يقع في كود التوصيل داخل main.
المقايضات ومتى تتوقّف
قد يتحوّل الجمع بين الأنماط إلى إفراط في الهندسة. اطرح هذه الأسئلة قبل إضافة كل نمط:
- Singleton: هل ثمة قيد فريدية حقيقي على مستوى التطبيق، أم يكفي مُنشئ عادي مع توصيل دقيق؟ إذا صعب عزل الاختبارات، فكّر في حقن المُرسِل بدلًا من ذلك.
- Factory: هل سيكون switch بسيط في المُستدعي أوضح؟ استخدم مصنعًا حين سينمو عدد المنتجات أو حين يكون الإنشاء معقّدًا.
- Builder: مبرَّر حين يملك الصنف أربعة معاملات أو أكثر أو أي معاملات اختيارية. لمعاملين إلزاميين، المُنشئ أبسط.
- Strategy: يستحق الواجهة الإضافية حين توجد خوارزميتان أو أكثر والتبديل أثناء التشغيل مطلوب فعلًا. إن كانت خوارزمية واحدة فقط ستوجد، فالنمط سابق لأوانه.
- Observer: ضروري حين يكون عدد الأطراف المهتمة مفتوحًا أو مجهولًا وقت الترجمة. تجنّبه إذا كان لديك مستمع واحد لن يتغيّر أبدًا.
تفشّي الأنماط: قاعدة كود تطبّق خمسة أنماط على مشكلة من خمسين سطرًا ليست جيدة التصميم — بل هي مُفرطة في الهندسة. الهدف تقليص التعقيد العرضي. تحقّق دائمًا أن النمط يزيل اقترانًا أكثر مما تُدخله طبقات التجريد.
الخلاصة
في هذا المشروع رأيت خمسة أنماط تعمل بتناسق: حافظ Singleton enum على المُرسِل المشترك؛ وغلّف Factory Method إنشاء القنوات؛ وأنتج Builder كائنات إشعارات نظيفة محقَّقة؛ وجعل Strategy وضع التسليم قرارًا وقت التشغيل؛ وأتاح Observer لمستمعين لا صلة بينهم التفاعل مع عمليات الإرسال دون اقتران. الرؤية المهنية الجوهرية هي أن الأنماط أدوات تواصل بقدر ما هي أدوات تصميم — حين يقرأ زميل ChannelFactory.create() أو يرى صنفًا اسمه DeliveryStrategy، يفهم قصد التصميم فورًا. استخدم تلك المفردات المشتركة بتعمّد، وقاوِم إضافة الأنماط حتى تكون المشكلة التي يحلّها حاضرةً بشكل ملموس.