من Singleton الذي يعلق السيرفر إلى Observer الذي ينقذ الـ Event-Driven — أي الأنماط تستحق أن تتعلمها بعمق وأيها يكفي أن تمر عليها في كورس سريع؟ رأي صريح من مطور سنيور بعد عشر سنوات في كتابة أنظمة حقيقية.
في أحد أيام ٢٠١٩، كنت جالساً في غرفة الاجتماعات مع فريق من خمسة مطورين، والنظام الذي كنا نبنيه منذ ستة أشهر بدأ يتصرف بطريقة غريبة: الـ API بيعلق فجأة، الـ Memory بيقعد يرتفع من غير سبب، والـ Logs مليانة بـ NullPointerException في أماكن ما كان المفروض تحصل فيها. بعد يومين من الـ Debugging، اكتشفنا إن المشكلة كانت في الـ Singleton اللي كتبناه في بداية المشروع: كان الـ Instance بيُنشأ مرة واحدة بس، صح، لكن الـ Threads كلها كانت بتتنافس على نفس الـ Instance في نفس الوقت، والـ Lock اللي حطيناه كان بيحول الـ Concurrency لـ Sequential Execution. النتيجة؟ الـ Response Time زاد من ٢٠٠ مللي إلى ٤ ثواني، والـ CPU وصل لـ ٩٠٪. ساعتها قررت أكتب هذا المقال: مش كل Design Pattern هو حل سحري، وبعضها بيكون سم في الكود إذا استخدمته في المكان الخطأ.
الـ Singleton هو أشهر Design Pattern على الإطلاق، وكل كورس OOP بيبدأ بيه. الفكرة بسيطة: تأكد إن الكلاس عنده instance واحد بس، ووفر طريقة للوصول ليه من أي مكان في الكود. في النظرية، ده حل جميل: مفيش حاجة تتكرر، الـ Memory موفر، والـ Global Access سهل. في الواقع، الـ Singleton هو واحد من أكثر الأنماط اللي بتسبب مشاكل في الأنظمة الكبيرة، خصوصاً اللي فيها Concurrency.
المشكلة الأولى في الـ Singleton هي إنه بيخلي الـ Dependencies مخفية. لما تكتب ClassA.getInstance() في أي مكان في الكود، مفيش طريقة تعرف إن ClassA بيستخدم ClassB و ClassC إلا لما تقرأ الكود كله. ده بيخلي الـ Testing صعب، والـ Refactoring أصعب. في شركة كنت بشتغل فيها، كان عندنا نظام بـ ٣٠٠ ألف سطر كود، والـ Singleton كان موجود في ٤٠٪ من الملفات. لما جينا نغير في الـ Database Schema، اكتشفنا إن الـ Singleton كان بيستخدم الـ Schema القديم في ١٥ مكان مختلف، وكنا لازم نغيرهم واحد واحد.
المشكلة التانية هي الـ Thread Safety. الـ Singleton التقليدي اللي بيتكتب بالشكل ده:
public class Database {
private static Database instance;
private Database() {}
public static Database getInstance() {
if (instance == null) {
instance = new Database();
}
return instance;
}
}ده مش Thread-Safe. لو اتنين Threads دخلوا على getInstance() في نفس الوقت، ممكن يتعمل اتنين Instances، وده بيخرب الفكرة كلها. الحل التقليدي هو إضافة synchronized:
public static synchronized Database getInstance() {
if (instance == null) {
instance = new Database();
}
return instance;
}بس ده بيخلي الـ Performance ينهار، لأن كل مرة حد يطلب الـ Instance، الـ Thread بيضطر ينتظر حتى الـ Lock يتحرر. في نظام كان عندنا بـ ٥٠٠ طلب في الثانية، الـ Response Time زاد من ٥٠ مللي إلى ٣٠٠ مللي بعد ما أضفنا synchronized. الحل الأفضل هو استخدام Double-Checked Locking أو Initialization-on-demand Holder Idiom، بس حتى ده بيكون معقد ومش سهل الـ Debugging.
في رأيي، الـ Singleton مش لازم تستخدمه إلا في حالتين: الأولانية لما يكون عندك Resource حقيقي واحد في النظام، زي الـ Configuration أو الـ Logging Service. التانية لما تكون متأكد إن الـ Concurrency مش هتكون مشكلة، زي في الـ CLI Applications أو الـ Scripts الصغيرة. في أي حالة تانية، الأفضل تستخدم Dependency Injection أو الـ Service Locator Pattern، لأنهم بيخليوا الـ Dependencies واضحة وأكتر قابلية للـ Testing.
الـ Observer Pattern هو واحد من الأنماط القليلة اللي بجد تستحق الوقت اللي بتاخذه في تعلمها. الفكرة الأساسية هي إنك تفصل بين الـ Producer والـ Consumer: الـ Subject (اللي بيحصل فيه الحدث) بيحافظ على قائمة من الـ Observers، ولما يحصل حدث معين، الـ Subject بيبعت الـ Event لكل الـ Observers اللي مشتركين. ده بيخلي الكود مرن وسهل التوسع، خصوصاً في الأنظمة اللي بتعتمد على الأحداث زي الـ UI Frameworks أو الـ Microservices.
في ٢٠٢١، كنت بشتغل على نظام دفع إلكتروني لـ E-Commerce Platform، وكان عندنا مشكلة في الـ Notifications: لما المستخدم يعمل طلب، كان لازم نبعتله إيميل، ونحدث الـ Database، ونبعت Notification للـ Admin، ونحدث الـ Cache. الكود كان مكتوب بطريقة Procedural: بعد ما الـ Order يتعمل، كنا بنادي على أربع دوال واحدة ورا التانية. المشكلة إن لو واحدة من الدوال فشلت، كان لازم نرجع الـ Order كله، وكمان الـ Performance كان وحش لأن كل خطوة كانت بتنتظر اللي قبلها.
حلينا المشكلة باستخدام الـ Observer Pattern. بدل ما نكتب الكود بطريقة Sequential، حولنا كل خطوة لـ Observer مستقل. الـ Order Service كان الـ Subject، وكل Observer كان مسئول عن خطوة واحدة. لما الـ Order يتعمل، الـ Subject بيبعت الـ Event لكل الـ Observers في نفس الوقت، وكل واحد فيهم يشتغل على الـ Thread بتاعه. النتيجة؟ الـ Response Time نزل من ١.٥ ثانية لـ ٣٠٠ مللي، والـ System بقى أكتر استقرار لأن لو واحد من الـ Observers فشل، الباقيين ما تأثروش.
interface Observer {
update(event: OrderEvent): void;
}
class OrderService {
private observers: Observer[] = [];
addObserver(observer: Observer): void {
this.observers.push(observer);
}
createOrder(order: Order): void {
// Create order logic
const event = new OrderEvent(order);
this.observers.forEach(observer => observer.update(event));
}
}
class EmailService implements Observer {
update(event: OrderEvent): void {
sendEmail(event.order);
}
}
class DatabaseService implements Observer {
update(event: OrderEvent): void {
updateDatabase(event.order);
}
}الـ Observer Pattern مش مجرد فكرة أكاديمية، ده أساس في أي نظام Event-Driven. الـ DOM في الـ Browser بيعمل بيه، والـ Redux في الـ Frontend بيعتمد عليه، وحتى الـ Kafka في الـ Backend بيعمل بيه. الفرق بين الـ Observer والـ Singleton هو إن الـ Observer بيخلي الكود مرن وقابل للتوسع، بينما الـ Singleton بيخليه جامد وصعب التغيير.
الـ Factory Pattern هو واحد من الأنماط اللي الناس بتستخدمها كتير، بس مش دايماً في المكان الصح. الفكرة الأساسية هي إنك تفصل الـ Creation Logic عن الـ Business Logic: بدل ما تكتب new ClassA() في الكود، بتستخدم Factory اللي بتقرر أي Class تنشئ بناء على الـ Input. ده بيخلي الكود أكتر مرونة، خصوصاً لو عندك عدة Implementations لنفس الـ Interface.
في رأيي، الـ Factory بيكون مفيد في حالتين: الأولانية لما يكون عندك عدة Implementations لنفس الـ Interface، وانت عاوز تختار بينهم في الـ Runtime. مثلاً، لو عندك PaymentProcessor Interface، وعاوز تستخدم StripeProcessor في الـ Production و MockProcessor في الـ Testing، الـ Factory بيكون حل كويس. الحالة التانية لما يكون الـ Creation Logic معقد ومش مجرد new، مثلاً لو الـ Object بيحتاج إعدادات كتير قبل ما يتعمل.
بس الـ Factory بيكون مبالغة في حالات كتير. مثلاً، لو عندك Class واحد بس، ومفيش حاجة تتغير في المستقبل، الـ Factory بيكون Overhead مش لازم. في مشروع كنت بشتغل عليه، كان عندنا Factory بـ ٥٠ سطر كود عشان تنشئ Object واحد بس، وكان ممكن نكتب new ClassA() في سطر واحد. الـ Factory كان بيخلي الكود أطول وصعب الفهم من غير أي فايدة.
from abc import ABC, abstractmethod
class PaymentProcessor(ABC):
@abstractmethod
def process(self, amount: float) -> bool:
pass
class StripeProcessor(PaymentProcessor):
def process(self, amount: float) -> bool:
print(f"Processing ${amount} via Stripe")
return True
class PayPalProcessor(PaymentProcessor):
def process(self, amount: float) -> bool:
print(f"Processing ${amount} via PayPal")
return True
class PaymentFactory:
@staticmethod
def create_processor(processor_type: str) -> PaymentProcessor:
if processor_type == "stripe":
return StripeProcessor()
elif processor_type == "paypal":
return PayPalProcessor()
else:
raise ValueError("Invalid processor type")
# Usage
processor = PaymentFactory.create_processor("stripe")
processor.process(100.0)الـ Factory بيكون مفيد كمان لما يكون عندك Dependency على External System، وعاوز تخلي الكود سهل الـ Testing. مثلاً، لو عندك DatabaseConnection Factory، ممكن في الـ Testing تستخدم MockConnection بدل الـ Real Connection. بس لو مفيش حاجة تتغير، الأفضل تستخدم الـ Constructor العادي، لأنه أسهل في الفهم وأسرع في التنفيذ.
الـ Decorator Pattern هو واحد من الأنماط اللي بتبهر الناس لما يعرفوها، لأنه بيخلي الكود مرن جداً. الفكرة الأساسية هي إنك تلف الـ Object بـ Wrapper بيضيف سلوك جديد من غير ما يغير الـ Original Class. ده بيخلي الكود سهل التوسع، خصوصاً في الأنظمة اللي فيها سلوكيات كتير ممكن تتغير.
في ٢٠٢٠، كنت بشتغل على نظام لـ Analytics Platform، وكان عندنا مشكلة في الـ Logging: كنا عاوزين نضيف معلومات كتير لكل Request، زي الـ User ID والـ Timestamp والـ Request Path، بس ما كناش عاوزين نغير في كل Function في الكود. الحل كان استخدام الـ Decorator Pattern. كتبنا Decorator بسيط بيلف أي Function بياخد Request وبيرجع Response، وبيضيف الـ Logging قبل وبعد التنفيذ. النتيجة؟ قدرنا نضيف الـ Logging في مكان واحد، وكل الـ Functions اللي بتتعامل مع الـ Requests استفادت منه من غير ما نغير فيهم حاجة.
from functools import wraps
from typing import Callable, Any
def log_request(func: Callable) -> Callable:
@wraps(func)
def wrapper(*args, **kwargs) -> Any:
print(f"Request started: {func.__name__}")
result = func(*args, **kwargs)
print(f"Request finished: {func.__name__}")
return result
return wrapper
@log_request
def get_user_data(user_id: int) -> dict:
return {"id": user_id, "name": "John Doe"}
# Usage
print(get_user_data(1))الـ Decorator بيكون مفيد كمان في الـ Middleware زي في الـ Express.js أو الـ Django. بس زي أي حاجة، ممكن يبقى مبالغة. مثلاً، لو عندك Decorator واحد بس، ومفيش حاجة تتغير في المستقبل، ممكن تكتب الـ Logic جوه الـ Function مباشرة. كمان، لو الـ Decorators بقت كتير، الكود بيبقى صعب الفهم، خصوصاً لو الـ Decorators متداخلة في بعضها.
في الـ Academia، بيقولولك إن كل الـ Design Patterns مهمة، بس في الواقع، فيه أنماط كتير مفيش منها فايدة في الـ Production. مثلاً، الـ Memento Pattern: الفكرة إنك تحفظ الـ State بتاع الـ Object عشان تقدر ترجعله بعدين. ده بيكون مفيد في الـ Undo Functionality في الـ Editors، بس في معظم الأنظمة، مفيش حاجة اسمها Undo، والـ State بيكون كبير جداً ومش سهل حفظه. في مشروع كنت بشتغل عليه، حاولنا نستخدم الـ Memento عشان نعمل Undo للـ Orders، بس الـ State كان معقد جداً، وكنا بنضطر نحفظ الـ Database كله عشان نرجع خطوة واحدة.
كمان، الـ Visitor Pattern: الفكرة إنك تفصل الـ Algorithm عن الـ Data Structure. ده بيكون مفيد في الـ Compilers أو الـ Parsers، بس في معظم الأنظمة، الـ Data Structure بسيط ومش محتاج Visitor. في شركة كبيرة، كان عندنا فريق حاول يستخدم الـ Visitor عشان يعدل في الـ AST بتاع الـ Query Language، بس الكود بقى معقد جداً، وكان أسهل لو كتبوا الـ Logic جوه الـ Classes نفسها.
الـ Flyweight Pattern كمان: الفكرة إنك تشارك الـ Objects المشتركة عشان توفر الـ Memory. ده بيكون مفيد في الـ Games أو الـ Graphics، بس في معظم الأنظمة، الـ Memory مش مشكلة، والـ CPU بيكون أغلى. في نظام كنت بشتغل عليه، حاولنا نستخدم الـ Flyweight عشان نخزن الـ User Sessions، بس الـ Overhead بتاع إدارة الـ Shared Objects كان أكبر من الـ Memory اللي وفرناها.
بعد عشر سنين في كتابة كود حقيقي، اللي اتعلمته هو إن الـ Design Patterns مش حلول سحرية، إنما أدوات. زي أي أداة، ممكن تستخدمها صح وتجيب نتيجة كويسة، وممكن تستخدمها غلط وتخرب النظام كله. القاعدة الأولى: ما تستخدمش أي Pattern إلا لما تكون متأكد إن المشكلة اللي عندك تستاهل الحل ده. لو عندك مشكلة بسيطة، استخدم حل بسيط. القاعدة التانية: ما تفرطش في الـ Abstraction. كل طبقة Abstraction بتضيف Complexity، وكل Complexity بتخلي الكود أصعب في الفهم والصيانة.
الأنماط اللي تستاهل تتعلمها بعمق هي اللي بتحل مشاكل حقيقية في الأنظمة الكبيرة: الـ Observer للـ Event-Driven، الـ Strategy للـ Algorithms المتغيرة، الـ Decorator للـ Cross-Cutting Concerns، والـ Dependency Injection عشان الـ Testing. الأنماط اللي يكفي تمر عليها في كورس سريع هي اللي مفيش منها فايدة في الـ Production زي الـ Memento والـ Visitor والـ Flyweight. والأهم من كل حاجة: ما تستخدمش أي Pattern عشان الـ Hype أو عشان تبان ذكي. الكود الجيد هو اللي سهل الفهم وسهل الصيانة، مش اللي مليان Patterns معقدة.
آخر نصيحة: لما تكتب كود، فكر في الشخص اللي هيمشي وراك بعد سنة. هل الكود اللي انت كاتبه دلوقتي هيكون سهل الفهم، ولا هيحتاج يومين عشان يفهمه؟ لو الإجابة التانية، فأنت بتكتب كود سيئ، حتى لو مليان Patterns جميلة.