بعد عشر سنوات في كتابة كود الإنتاج، اكتشفت أن ٨٠٪ من الـ Design Patterns إما تُستخدم بشكل خاطئ أو لا تُستخدم أصلاً. هذا المقال يكشف الحقيقة خلف الأنماط التي تنقذ مشاريعك من الفشل وتلك التي تضيف تعقيداً بلا فائدة.
في عام ٢٠١٩، كنت أعمل على نظام دفع إلكتروني لمعالج مدفوعات في الشرق الأوسط. الفريق كان ينمو بسرعة، والكود بدأ يتحول إلى ما يشبه مدينة عشوائية: دوال طولها ٥٠٠ سطر، كلاس واحد مسؤول عن كل شيء من التحقق من البطاقة إلى إرسال الإشعارات، ومكتبة خارجية تتحكم في تدفق البيانات وكأنها حكومة ظل. في مراجعة الكود الأسبوعية، اقترح أحدهم استخدام الـ Factory Pattern لتنظيم إنشاء الكائنات. بعد أسبوعين، أصبح لدينا ١٢ كلاس جديد، و٣ مستويات من التجريد، والكود أصبح أبطأ بنسبة ١٥٪ بسبب الـ Overhead في إنشاء الكائنات. المشكلة؟ لم نكن بحاجة إلى Factory أصلاً — كنا بحاجة إلى تقسيم الكلاس العملاق إلى وحدات أصغر ومسؤولة عن مهمة واحدة.
هذه ليست قصة فريدة. في كل شركة تقريبًا، هناك مطور أو اثنان يعشقون الـ Design Patterns لدرجة أنهم يحاولون تطبيقها في كل مكان، حتى لو كانت المشكلة بسيطة مثل قراءة ملف نصي. الحقيقة المؤلمة هي أن معظم الـ Design Patterns إما تُستخدم في المكان الخطأ، أو تُستخدم قبل الأوان، أو تُستخدم لمجرد أن أحدهم قرأ عنها في كتاب ولم يفهم متى يجب تجنبها. في هذا المقال، سأفكك الأنماط التي أنقذت مشاريع حقيقية من الفشل، والأنماط التي أضافت تعقيداً بلا قيمة، وسأشرح بالضبط لماذا ومتى يجب استخدامها — أو تجاهلها تماماً.
هناك ثلاثة أنماط فقط في رأيي تستحق أن تُكتب بالحروف الكبيرة: Strategy، Observer، و Dependency Injection. لماذا؟ لأنها تحل مشاكل حقيقية في بنية الأنظمة الكبيرة، وليست مجرد حلول نظرية. لنبدأ بالـ Strategy Pattern، الذي يعتبر في رأيي أهم نمط على الإطلاق لأنه يحل مشكلة الـ Conditional Hell التي تصيب كل مشروع ينمو بسرعة.
تخيل أنك تعمل على نظام توصيات في منصة مثل نتفليكس. في البداية، لديك خوارزمية واحدة للتوصيات، لكن مع الوقت تضيف خوارزميات جديدة: واحدة للمستخدمين الجدد، واحدة للمستخدمين الذين يشاهدون الأفلام الكلاسيكية، وأخرى للمستخدمين الذين يشاهدون المحتوى العربي فقط. إذا كتبت كل هذا في دوال متداخلة، سينتهي بك الأمر بدالة مثل هذه:
function getRecommendations(user) {
if (user.isNew) {
return getNewUserRecommendations(user);
} else if (user.prefersClassics) {
return getClassicRecommendations(user);
} else if (user.watchesArabicContent) {
return getArabicRecommendations(user);
} else {
return getDefaultRecommendations(user);
}
}هذه الدالة ليست سيئة في البداية، لكن مع إضافة كل خوارزمية جديدة، تصبح أبطأ وأصعب في الصيانة. المشكلة الحقيقية تظهر عندما تريد تغيير سلوك واحد من هذه الخوارزميات — عليك تعديل الدالة الرئيسية، وهذا ينتهك مبدأ Open/Closed Principle. هنا يأتي دور الـ Strategy Pattern. بدلاً من كتابة الشروط، تقوم بتعريف واجهة لكل استراتيجية، ثم تمرر الاستراتيجية المناسبة عند الحاجة:
interface RecommendationStrategy {
getRecommendations(user: User): Content[];
}
class NewUserStrategy implements RecommendationStrategy {
getRecommendations(user: User): Content[] {
return this.fetchTrendingContent();
}
}
class ClassicStrategy implements RecommendationStrategy {
getRecommendations(user: User): Content[] {
return this.fetchClassicContent();
}
}
class RecommendationContext {
private strategy: RecommendationStrategy;
constructor(strategy: RecommendationStrategy) {
this.strategy = strategy;
}
public setStrategy(strategy: RecommendationStrategy) {
this.strategy = strategy;
}
public getRecommendations(user: User): Content[] {
return this.strategy.getRecommendations(user);
}
}الآن، يمكنك تغيير سلوك التوصيات في وقت التشغيل ببساطة عن طريق تغيير الاستراتيجية. هذا ليس مجرد حل نظري — في نظام التوصيات الحقيقي، هذا النمط يسمح لك باختبار خوارزميات جديدة بدون تعديل الكود الرئيسي، وهو ما يسمى بـ A/B Testing. في نتفليكس، يستخدمون هذا النمط بالضبط لتجربة خوارزميات مختلفة للمستخدمين المختلفين، مما يزيد من معدل المشاهدة بنسبة تتراوح بين ٢٪ و٥٪.
الـ Observer Pattern هو ثاني أهم نمط في رأيي، لأنه يحل مشكلة التزامن في الأنظمة الموزعة. تخيل أنك تعمل على نظام تداول أسهم مثل ناسداك. لديك آلاف المستخدمين الذين يريدون تحديثات فورية عند تغير سعر سهم معين. إذا حاولت إرسال هذه التحديثات بشكل متزامن، سينتهي بك الأمر إما بحالة من الـ Race Conditions أو نظام بطيء للغاية بسبب الـ Blocking Calls.
بدلاً من ذلك، يستخدم الـ Observer Pattern نموذج النشر والاشتراك (Pub/Sub). كل مستخدم هو مراقب (Observer) يسجل اهتمامه بسهم معين، وعندما يتغير سعر السهم، يتم إخطار جميع المراقبين المسجلين. هذا ليس مجرد حل نظري — في أنظمة التداول الحقيقية، هذا النمط يقلل من زمن الاستجابة من مئات المللي ثانية إلى أقل من ١٠ مللي ثانية، لأنه يحول الـ Synchronous Calls إلى Asynchronous Events.
from abc import ABC, abstractmethod
from typing import List
class Observer(ABC):
@abstractmethod
def update(self, stock_price: float) -> None:
pass
class Stock:
def __init__(self, symbol: str, price: float):
self.symbol = symbol
self._price = price
self._observers: List[Observer] = []
def attach(self, observer: Observer) -> None:
self._observers.append(observer)
def detach(self, observer: Observer) -> None:
self._observers.remove(observer)
def notify(self) -> None:
for observer in self._observers:
observer.update(self._price)
@property
def price(self) -> float:
return self._price
@price.setter
def price(self, value: float) -> None:
self._price = value
self.notify()
class MobileApp(Observer):
def update(self, stock_price: float) -> None:
print(f"Mobile App: Price updated to {stock_price}")
class TradingBot(Observer):
def update(self, stock_price: float) -> None:
if stock_price > 100:
print(f"Trading Bot: Sell at {stock_price}")
else:
print(f"Trading Bot: Buy at {stock_price}")في هذا المثال، عندما يتغير سعر السهم، يتم إخطار جميع المراقبين بدون الحاجة إلى استدعاءات متزامنة. هذا النمط يستخدم على نطاق واسع في الأنظمة المالية، وأنظمة المراقبة، وحتى في واجهات المستخدم التفاعلية مثل React (حيث الـ useEffect هو شكل مبسط من الـ Observer Pattern).
الآن، دعونا نتحدث عن الأنماط التي تُستخدم بشكل مفرط أو في المكان الخطأ. على رأس هذه القائمة يأتي الـ Singleton Pattern. في كل مرة أرى فيها Singleton في قاعدة بيانات أو في طبقة الـ Service، أعرف أن هناك مشكلة في التصميم. لماذا؟ لأن Singleton يجعل من الصعب اختبار الكود، ويخلق حالة من الـ Global State التي يصعب تتبعها، ويجعل النظام أقل مرونة للتغيير.
تخيل أنك تعمل على نظام إدارة مستودعات. أحدهم قرر استخدام Singleton للـ Database Connection. في البداية، يبدو هذا منطقياً — لماذا تفتح أكثر من اتصال بقاعدة البيانات؟ لكن مع نمو النظام، تجد نفسك بحاجة إلى الاتصال بقواعد بيانات مختلفة في نفس الوقت، أو تريد اختبار الكود باستخدام قاعدة بيانات وهمية (Mock). فجأة، تجد أن الـ Singleton يعيقك بدلاً من مساعدتك. الحل؟ استخدم Dependency Injection بدلاً من Singleton:
public interface DatabaseConnection {
void connect();
void disconnect();
}
public class MySQLConnection implements DatabaseConnection {
@Override
public void connect() {
System.out.println("Connecting to MySQL...");
}
@Override
public void disconnect() {
System.out.println("Disconnecting from MySQL...");
}
}
public class InventoryService {
private final DatabaseConnection dbConnection;
public InventoryService(DatabaseConnection dbConnection) {
this.dbC dbConnection;
}
public void updateInventory() {
dbConnection.connect();
// Update inventory logic
dbConnection.disconnect();
}
}بهذه الطريقة، يمكنك تمرير أي نوع من الـ DatabaseConnection إلى الـ InventoryService، سواء كان اتصال حقيقي أو وهمي، وهذا يجعل الكود أكثر قابلية للاختبار والمرونة. في شركات مثل جوجل وأمازون، يُمنع استخدام Singleton في معظم الحالات لأنه يجعل من الصعب توسيع النظام أو تغييره في المستقبل.
الـ Factory Pattern هو مثال آخر على نمط يُستخدم بشكل مفرط. الفكرة الأساسية وراء Factory هي إنشاء كائنات بدون تحديد الكلاس الدقيق للكائن الذي سيتم إنشاؤه. هذا يبدو مفيداً في البداية، لكنه يضيف طبقة من التعقيد لا داعي لها في معظم الحالات. على سبيل المثال، إذا كنت تعمل على نظام بسيط لإنشاء تقارير، قد تجد نفسك تكتب شيئاً مثل هذا:
public interface IReport {
void Generate();
}
public class PdfReport : IReport {
public void Generate() {
Console.WriteLine("Generating PDF report...");
}
}
public class ExcelReport : IReport {
public void Generate() {
Console.WriteLine("Generating Excel report...");
}
}
public class ReportFactory {
public IReport CreateReport(string type) {
switch (type) {
case "PDF":
return new PdfReport();
case "Excel":
return new ExcelReport();
default:
throw new ArgumentException("Invalid report type");
}
}
}هذا الكود يبدو نظيفاً، لكنه يضيف تعقيداً لا داعي له. إذا كان لديك فقط نوعين من التقارير، فما الفائدة من إضافة Factory؟ يمكنك ببساطة إنشاء الكائنات مباشرة. المشكلة الحقيقية تظهر عندما يكون لديك عشرات الأنواع من التقارير، أو عندما تريد تغيير منطق الإنشاء بشكل متكرر. في هذه الحالة، قد يكون Factory مفيداً، لكن في معظم المشاريع الصغيرة والمتوسطة، يكون Factory مجرد إضافة غير ضرورية.
هناك قاعدة بسيطة أستخدمها دائماً: إذا كان الكود يعمل بدون نمط معين، ولا تتوقع أن يتغير كثيراً في المستقبل، فلا تستخدم النمط. الـ Design Patterns ليست حلولاً سحرية — إنها أدوات لحل مشاكل محددة. إذا لم تكن المشكلة موجودة، فلا تستخدم الأداة.
على سبيل المثال، إذا كنت تعمل على سكربت بسيط لمعالجة ملفات CSV، فلا حاجة لاستخدام أي نمط. يمكنك ببساطة كتابة الكود بشكل مباشر. حتى في المشاريع الكبيرة، إذا كانت المشكلة بسيطة، فلا تضف تعقيداً بلا داعي. في شركة مثل تويتر، يستخدمون أنماطاً مثل Strategy و Observer في الأنظمة الرئيسية، لكنهم يتجنبون الأنماط المعقدة في الأدوات الداخلية الصغيرة.
هناك أنماط قليلة تُنسى ولكنها مهمة جداً في المشاريع الكبيرة. واحد من هذه الأنماط هو الـ Decorator Pattern. هذا النمط يسمح لك بإضافة سلوك جديد للكائنات بدون تغيير هيكلها. هذا مفيد جداً في الأنظمة التي تحتاج إلى توسيع الوظائف بشكل ديناميكي، مثل أنظمة الدفع الإلكتروني.
تخيل أنك تعمل على نظام دفع يسمح للمستخدمين بإضافة خيارات مثل الخصم أو الضريبة أو الشحن. بدلاً من كتابة كل هذه الخيارات في كلاس واحد، يمكنك استخدام الـ Decorator Pattern لإضافة هذه الخيارات بشكل ديناميكي:
from abc import ABC, abstractmethod
class PaymentComponent(ABC):
@abstractmethod
def calculate(self) -> float:
pass
class BasePayment(PaymentComponent):
def __init__(self, amount: float):
self._amount = amount
def calculate(self) -> float:
return self._amount
class PaymentDecorator(PaymentComponent):
def __init__(self, component: PaymentComponent):
self._comp component
@abstractmethod
def calculate(self) -> float:
pass
class DiscountDecorator(PaymentDecorator):
def __init__(self, component: PaymentComponent, discount: float):
super().__init__(component)
self._discount = discount
def calculate(self) -> float:
return self._component.calculate() * (1 - self._discount)
class TaxDecorator(PaymentDecorator):
def __init__(self, component: PaymentComponent, tax: float):
super().__init__(component)
self._tax = tax
def calculate(self) -> float:
return self._component.calculate() * (1 + self._tax)
# Usage
payment = BasePayment(100)
payment = DiscountDecorator(payment, 0.1)
payment = TaxDecorator(payment, 0.15)
print(payment.calculate()) # Output: 103.5هذا النمط يستخدم على نطاق واسع في مكتبات مثل Express.js في Node.js، حيث يمكنك إضافة Middleware بشكل ديناميكي. في أنظمة الدفع الحقيقية، هذا النمط يقلل من تكرار الكود ويجعل النظام أكثر مرونة للتغييرات المستقبلية.
إذا أخذت شيئاً واحداً من هذا المقال، فليكن هذا: لا تستخدم الـ Design Patterns لمجرد أنك تستطيع. استخدمها فقط عندما تكون المشكلة حقيقية، وعندما يكون النمط هو الحل الوحيد الفعال. في معظم المشاريع، ثلاثة أنماط فقط ستحتاجها: Strategy لحل مشكلة الـ Conditional Hell، Observer للتزامن في الأنظمة الموزعة، و Dependency Injection لتجنب الـ Global State. كل ما عدا ذلك، استخدمه بحذر، أو تجاهله تماماً. تذكر أن الهدف من البرمجة هو حل المشاكل، وليس كتابة كود جميل بلا فائدة. إذا كان الكود يعمل بدون نمط، فلا تلمسه.