من Singleton الذي يعلق السيرفر إلى Observer الذي ينقذ الـ Event-Driven، إليك الحقيقة الصادمة عن الـ Design Patterns التي تستخدمها فعلاً في الإنتاج والتي تكتفي بالحديث عنها في المقابلات.
كنت أتصفح كود مشروع قديم لشركة ناشئة في دبي، ووجدت 17 ملفاً باسم Singleton.java. كل ملف كان يحمل نفس الكلاس، وكل واحد منهم كان يفتح اتصالاً جديداً بقاعدة البيانات. النتيجة؟ السيرفر بيعلق عند 200 مستخدم متزامن، والـ Memory Leak يتصاعد مثل بالون على وشك الانفجار. سألني المطور الرئيسي: «ما المشكلة؟ أنا استخدمت Design Pattern صحيح!». الحقيقة هي أن الـ Design Patterns ليست حلولاً سحرية، بل أدوات لها سياقات محددة، وبعضها تحول مشروعك إلى متاهة إذا استخدمت خارج السياق.
في هذا المقال، لن نتحدث عن الـ 23 Design Pattern الكلاسيكية كما في كتاب GoF. بدلاً من ذلك، سنفكك الأنماط التي أراها تُستخدم فعلاً في مشاريع الإنتاج، والأنماط التي تُذكر فقط في المقابلات أو تُضاف كزينة أكاديمية. سأريك الأكواد الحقيقية، المشاكل الحقيقية، وكيف تتجنب تحويل مشروعك إلى متحف للأنماط البرمجية بدلاً من نظام يعمل بكفاءة.
الـ Singleton هو أشهر Design Pattern على الإطلاق، وربما أكثرها سوء فهم. الفكرة تبدو بسيطة: تأكد من وجود نسخة واحدة فقط من الكلاس في التطبيق. لكن المشكلة تبدأ عندما تستخدمه في أماكن لا تحتاج فيها لهذا التقييد. مثلاً، في مشروع لشركة تأمين، استخدموا Singleton لإدارة اتصالات قاعدة البيانات. النتيجة؟ عند 300 مستخدم متزامن، بدأ الـ Thread Blocking، والـ Response Time ارتفع من 200ms إلى 4500ms. لماذا؟ لأن الـ Singleton هنا تحول إلى عنق زجاجة (Bottleneck) بدلاً من حل.
الحقيقة هي أن الـ Singleton مفيد فقط في حالات محددة جداً: مثلاً، إدارة الـ Configuration أو الـ Logging حيث تحتاج فعلاً لنسخة واحدة مشتركة. لكن في معظم الحالات، وخاصة مع الـ Dependency Injection الحديثة، الـ Singleton يصبح عائقاً. انظر إلى هذا الكود الذي رأيته في مشروع حقيقي:
public class DatabaseConnection {
private static DatabaseConnection instance;
private Connection connection;
private DatabaseConnection() {
this.c DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "user", "pass");
}
public static synchronized DatabaseConnection getInstance() {
if (instance == null) {
instance = new DatabaseConnection();
}
return instance;
}
public Connection getConnection() {
return connection;
}
}
// الاستخدام السيئ:
public class UserService {
public void createUser(User user) {
DatabaseConnection db = DatabaseConnection.getInstance();
// ... استخدام الاتصال
}
}المشكلة هنا ليست فقط في الـ Thread Safety (لاحظ الـ synchronized)، بل في أن هذا الكود يجعل من المستحيل اختبار الـ UserService بشكل منعزل. كل مرة تريد فيها كتابة اختبار لوحدة، ستضطر لفتح اتصال حقيقي بقاعدة البيانات. الحل؟ استخدم الـ Dependency Injection بدلاً من الـ Singleton:
public class UserService {
private final Connection connection;
public UserService(Connection connection) {
this.c connection;
}
public void createUser(User user) {
// ... استخدام الاتصال
}
}
// في بيئة الإنتاج:
Connection realConnection = DriverManager.getConnection(...);
UserService service = new UserService(realConnection);
// في اختبارات الوحدة:
Connection mockConnection = mock(Connection.class);
UserService testService = new UserService(mockConnection);هناك حالات قليلة يكون فيها الـ Singleton هو الحل الأمثل. مثلاً، في نظام الـ Logging حيث تريد كتابة السجلات من أي مكان في التطبيق دون الحاجة لتمرير كائن الـ Logger في كل مكان. أو في إدارة الـ Configuration حيث تريد تحميل الإعدادات مرة واحدة فقط. لكن حتى في هذه الحالات، هناك بدائل أفضل في كثير من الأحيان. مثلاً، في Spring Framework، يمكنك استخدام الـ @ConfigurationProperties لتحميل الإعدادات مرة واحدة دون الحاجة للـ Singleton.
بينما الـ Singleton يُستخدم في الغالب بشكل خاطئ، الـ Observer هو Design Pattern الذي ينقذ المشاريع الكبيرة دون أن يلاحظه أحد. فكر في أي نظام يعتمد على الأحداث: الـ Real-Time Notifications، الـ Stock Market Updates، أو حتى الـ Frontend State Management. كل هذه الأنظمة تعتمد على فكرة الـ Observer في جوهرها.
في مشروع لشركة تداول عملات رقمية، استخدمنا الـ Observer لتنفيذ نظام الـ Real-Time Price Updates. بدلاً من أن يقوم الـ Frontend بالـ Polling كل ثانية، قمنا بإنشاء Subject (الـ Observable) يرسل التحديثات لكل الـ Observers المسجلين. النتيجة؟ انخفض الـ Network Traffic بنسبة 85%، وانخفض الـ CPU Usage على السيرفر من 70% إلى 15%. هذا هو الفرق بين النظام الذي يعمل بكفاءة والنظام الذي يعلق عند أول زيادة في الحمل.
// Subject (Observable)
class PriceFeed {
private observers: Observer[] = [];
private price: number = 0;
addObserver(observer: Observer) {
this.observers.push(observer);
}
removeObserver(observer: Observer) {
const index = this.observers.indexOf(observer);
if (index > -1) {
this.observers.splice(index, 1);
}
}
setPrice(newPrice: number) {
this.price = newPrice;
this.notifyObservers();
}
private notifyObservers() {
for (const observer of this.observers) {
observer.update(this.price);
}
}
}
// Observer
interface Observer {
update(price: number): void;
}
// مثال على Observer
class TradingDashboard implements Observer {
update(price: number) {
console.log(`Price updated: $${price}`);
// تحديث واجهة المستخدم
}
}
// الاستخدام
const feed = new PriceFeed();
const dashboard = new TradingDashboard();
feed.addObserver(dashboard);
// عند تحديث السعر
feed.setPrice(50000); // كل الـ Observers سيتلقون التحديثلاحظ كيف أن هذا الكود لا يحتوي على أي تبعيات خارجية، ويمكن اختباره بسهولة. يمكنك كتابة اختبار وحدة للـ PriceFeed دون الحاجة لوجود واجهة مستخدم حقيقية. هذا هو جمال الـ Observer: يفصل بين الـ Logic و الـ Presentation بشكل كامل.
إذا كنت تعمل في الـ Frontend، فأنت تستخدم الـ Observer يومياً دون أن تدرك ذلك. مكتبات مثل Redux و RxJS تعتمد على نفس المبدأ. مثلاً، في Redux، الـ Store هو الـ Subject، والـ Components هي الـ Observers. عندما يتغير الـ State، يقوم الـ Store بإخطار كل الـ Components المسجلة. هذا هو بالضبط الـ Observer Pattern، لكن مع بعض الإضافات مثل الـ Middleware و الـ Reducers.
// مثال مبسط على Redux باستخدام Observer
class Store {
constructor(reducer) {
this.reducer = reducer;
this.state = reducer(undefined, {});
this.listeners = [];
}
subscribe(listener) {
this.listeners.push(listener);
return () => {
this.listeners = this.listeners.filter(l => l !== listener);
};
}
dispatch(action) {
this.state = this.reducer(this.state, action);
this.listeners.forEach(listener => listener(this.state));
}
}
// الاستخدام
const store = new Store((state = { count: 0 }, action) => {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
default:
return state;
}
});
const unsubscribe = store.subscribe(state => {
console.log('State updated:', state);
});
store.dispatch({ type: 'INCREMENT' }); // سيطبع: State updated: { count: 1 }الـ Factory Pattern هو أحد الأنماط التي تُستخدم بشكل مفرط في المشاريع. الفكرة تبدو جذابة: بدلاً من إنشاء الكائنات مباشرة باستخدام الـ new، استخدم Factory لإخفاء تفاصيل الإنشاء. لكن في الواقع، الـ Factory Pattern مفيد فقط في حالات محددة، وليس لكل كلاس في المشروع.
في مشروع لشركة توصيل طلبات، استخدموا Factory لإنشاء كل الكائنات: الـ Order، الـ User، الـ Payment، حتى الـ Notification. النتيجة؟ كود معقد بلا داعٍ، وصعوبة في تتبع تدفق الإنشاء. مثلاً، لإنشاء طلب جديد، كان عليك المرور بـ 4 Factory مختلفة، وكل واحدة منها تعتمد على الأخرى. هذا النوع من التعقيد غير ضروري في معظم الحالات.
// مثال على Factory مبالغ فيه
public class OrderFactory {
private readonly UserFactory _userFactory;
private readonly PaymentFactory _paymentFactory;
public OrderFactory(UserFactory userFactory, PaymentFactory paymentFactory) {
_userFactory = userFactory;
_paymentFactory = paymentFactory;
}
public Order CreateOrder(int userId, decimal amount) {
var user = _userFactory.CreateUser(userId);
var payment = _paymentFactory.CreatePayment(amount);
return new Order(user, payment);
}
}
// الاستخدام
var orderFactory = new OrderFactory(new UserFactory(), new PaymentFactory());
var order = orderFactory.CreateOrder(123, 99.99m);هذا الكود يبدو نظيفاً في البداية، لكنه يضيف طبقة من التعقيد دون فائدة حقيقية. بدلاً من ذلك، يمكنك ببساطة إنشاء الكائنات مباشرة باستخدام الـ new، أو استخدام الـ Dependency Injection إذا كنت بحاجة للتحكم في الإنشاء. الـ Factory Pattern مفيد فقط عندما يكون لديك منطق معقد لإنشاء الكائنات، مثل:
في مشروع لشركة SaaS، استخدمنا Factory لإنشاء الـ Database Connections بناءً على نوع العميل. العملاء العاديون يستخدمون MySQL، بينما العملاء الكبار يستخدمون PostgreSQL مع Replication. هنا، الـ Factory كان حلاً مثالياً لأنه وفر لنا مرونة في تغيير نوع قاعدة البيانات دون تعديل الكود في كل مكان.
from abc import ABC, abstractmethod
class DatabaseConnection(ABC):
@abstractmethod
def connect(self):
pass
class MySQLConnection(DatabaseConnection):
def connect(self):
print("Connecting to MySQL")
class PostgreSQLConnection(DatabaseConnection):
def connect(self):
print("Connecting to PostgreSQL")
class ConnectionFactory:
def create_connection(self, customer_type):
if customer_type == "premium":
return PostgreSQLConnection()
else:
return MySQLConnection()
# الاستخدام
factory = ConnectionFactory()
c factory.create_connection("premium")
connection.connect() # سيطبع: Connecting to PostgreSQLالـ Strategy Pattern هو أحد الأنماط التي تغير طريقة تفكيرك في حل المشاكل. بدلاً من كتابة سلسلة طويلة من الـ If-Else أو الـ Switch Cases، تقوم بفصل الخوارزميات المختلفة إلى كلاسات مستقلة يمكن تبديلها في Runtime. هذا النمط مفيد بشكل خاص عندما يكون لديك منطق متغير بشكل متكرر، مثل الـ Payment Processing أو الـ Discount Calculations.
في مشروع لشركة تجارة إلكترونية، كان لدينا نظام لحساب الخصومات. في البداية، كان الكود مليئاً بالـ If-Else:
function calculateDiscount(user, order) {
if (user.type === 'premium') {
return order.total * 0.2;
} else if (user.type === 'gold') {
return order.total * 0.15;
} else if (order.items.length > 10) {
return order.total * 0.1;
} else if (order.total > 1000) {
return order.total * 0.05;
} else {
return 0;
}
}هذا الكود يعمل، لكنه صعب التعديل والتوسع. كل مرة نريد إضافة نوع جديد من الخصومات، نضطر لتعديل هذه الدالة، مما يزيد من احتمالية إدخال أخطاء. الحل؟ استخدام الـ Strategy Pattern:
// Strategy Interface
class DiscountStrategy {
calculate(order) {
throw new Error("Method not implemented");
}
}
// استراتيجيات مختلفة
class PremiumDiscount extends DiscountStrategy {
calculate(order) {
return order.total * 0.2;
}
}
class GoldDiscount extends DiscountStrategy {
calculate(order) {
return order.total * 0.15;
}
}
class BulkDiscount extends DiscountStrategy {
calculate(order) {
return order.items.length > 10 ? order.total * 0.1 : 0;
}
}
class HighValueDiscount extends DiscountStrategy {
calculate(order) {
return order.total > 1000 ? order.total * 0.05 : 0;
}
}
// Context
class DiscountCalculator {
constructor(strategy) {
this.strategy = strategy;
}
setStrategy(strategy) {
this.strategy = strategy;
}
calculate(order) {
return this.strategy.calculate(order);
}
}
// الاستخدام
const calculator = new DiscountCalculator(new PremiumDiscount());
const discount = calculator.calculate({ total: 500, items: [] });
console.log(discount); // 100
// تغيير الاستراتيجية في Runtime
calculator.setStrategy(new BulkDiscount());
const newDiscount = calculator.calculate({ total: 500, items: Array(15).fill({}) });
console.log(newDiscount); // 50هذا الكود أسهل في الصيانة والتوسع. إذا أردت إضافة نوع جديد من الخصومات، كل ما عليك فعله هو إنشاء كلاس جديد وتنفيذه للـ DiscountStrategy. لا حاجة لتعديل الكود الموجود. هذا هو جمال الـ Strategy Pattern: يفصل بين الـ What و الـ How.
رغم فوائد الـ Strategy Pattern، إلا أنه ليس الحل الأمثل في كل الحالات. مثلاً، إذا كان لديك خوارزمية بسيطة جداً، مثل حساب الضريبة بنسبة ثابتة، فلا داعي لاستخدام Strategy Pattern. في هذه الحالة، الكود البسيط أفضل من إضافة تعقيد غير ضروري. القاعدة العامة هي: استخدم Strategy Pattern عندما يكون لديك منطق متغير بشكل متكرر أو عندما يكون لديك أكثر من 3-4 حالات في الـ If-Else.
هناك بعض الـ Design Patterns التي تُدرس في كل دورة برمجة، لكنها نادراً ما تُستخدم في مشاريع الإنتاج الحقيقية. من بين هذه الأنماط: الـ Decorator، الـ Adapter، و الـ Facade. هذه الأنماط ليست سيئة، لكنها غالباً ما تُستخدم في سياقات محددة جداً، وليس بشكل عام كما يُشاع.
لنبدأ بالـ Decorator Pattern. الفكرة هي إضافة سلوك جديد للكائنات دون تعديل الكلاس الأصلي. مثلاً، يمكنك استخدام Decorator لإضافة الـ Logging أو الـ Caching للكائنات. لكن في الواقع، معظم المكتبات الحديثة توفر هذه الوظائف بشكل مدمج. مثلاً، في Java، يمكنك استخدام الـ Dynamic Proxies أو الـ Spring AOP بدلاً من كتابة Decorators يدوياً. في JavaScript، يمكنك استخدام الـ Proxies أو الـ Higher-Order Functions لتحقيق نفس الهدف دون الحاجة للـ Decorator Pattern التقليدي.
// مثال على Decorator Pattern
class Coffee {
cost() {
return 5;
}
}
class MilkDecorator {
constructor(coffee) {
this.coffee = coffee;
}
cost() {
return this.coffee.cost() + 2;
}
}
class SugarDecorator {
constructor(coffee) {
this.coffee = coffee;
}
cost() {
return this.coffee.cost() + 1;
}
}
// الاستخدام
let coffee = new Coffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
console.log(coffee.cost()); // 8هذا الكود يبدو جميلاً في النظرية، لكن في الواقع، نادراً ما تحتاج لكتابته يدوياً. معظم الأطر توفر طرقاً أسهل لتحقيق نفس الهدف. مثلاً، في الـ Frontend، يمكنك استخدام الـ Higher-Order Components في React بدلاً من كتابة Decorators يدوياً.
بالنسبة للـ Adapter Pattern، فهو مفيد عندما تريد جعل واجهتين غير متوافقتين تعملان معاً. مثلاً، إذا كان لديك مكتبة قديمة وتريد استخدامها مع مكتبة جديدة. لكن في معظم الحالات، يمكنك تجنب الحاجة للـ Adapter عن طريق إعادة تصميم الكود بشكل أفضل. مثلاً، بدلاً من استخدام Adapter لتحويل واجهة المكتبة القديمة إلى واجهة المكتبة الجديدة، يمكنك ببساطة استخدام المكتبة الجديدة مباشرة إذا كانت متوافقة مع متطلبات مشروعك.
أما الـ Facade Pattern، فهو ببساطة توفير واجهة مبسطة لنظام معقد. مثلاً، يمكنك إنشاء Facade لتجميع عدة مكالمات لـ API في مكالمة واحدة. لكن في الواقع، هذا النمط غالباً ما يُستخدم بشكل ضمني دون الحاجة لتسميته. مثلاً، في الـ Frontend، الـ Redux Thunk أو الـ Redux Saga يمكن اعتبارهما Facades لتبسيط التعامل مع الـ Async Operations.
بعد أكثر من عشر سنوات في تطوير البرمجيات، هذه هي نصيحتي الصريحة لك: لا تتعلم الـ Design Patterns كقواعد جامدة، بل تعلمها كأدوات لحل مشاكل محددة. الـ Observer و الـ Strategy هما الأنماط التي أستخدمها بشكل متكرر في مشاريع الإنتاج، بينما الـ Singleton و الـ Factory غالباً ما تُستخدم بشكل مفرط. بالنسبة للأنماط الأخرى مثل الـ Decorator و الـ Adapter، فهي مفيدة في سياقات محددة، لكن لا تجعلها أولوية في تعلمك.
قبل أن تستخدم أي Design Pattern، اسأل نفسك هذه الأسئلة:
في النهاية، الهدف من الـ Design Patterns ليس كتابة كود معقد، بل كتابة كود نظيف، قابل للصيانة، وقابل للتوسع. إذا كان استخدام Design Pattern سيجعل الكود أكثر تعقيداً دون فائدة حقيقية، فلا تستخدمه. تذكر دائماً: أفضل كود هو الكود الذي لا تحتاج لكتابته.
البرمجة ليست عن كتابة الكود، بل عن حل المشاكل. الـ Design Patterns هي أدوات، وليس قواعد مقدسة.
— مطور مجهول في وادي السيليكون