حول المحتوى:
شرح أنماط التصميم الأكثر استخداماً في المشاريع الكبيرة، مع أمثلة عملية وشرح سبب الحاجة إليها وكيفية اختبارها.
في المشاريع الصغيرة في بايثون غالباً نكتب الكود بشكل مباشر وبسيط، لكن مع توسّع المشروع وازدياد عدد المطورين والموديولات، يبدأ الكود في التعقيد ويصبح من الصعب صيانته أو توسيعه بدون “تكسير” أجزاء أخرى من النظام. هنا تظهر أهمية Design Patterns.
في هذا المقال سنشرح مفهوم أنماط التصميم (Design Patterns) في بايثون مع التركيز على نمطين من الأكثر استخداماً في التطبيقات الكبيرة: Factory و Singleton، وسنرى:
إذا كنت مهتماً بالتصميم الجيد للكود، فهذا المقال موجه لك، خاصة إن كنت تعمل على مشاريع Web مثل Django أو FastAPI، أو مشاريع تستخدم البرمجة المتزامنة وغير المتزامنة كما شرحنا في مقال البرمجة غير المتزامنة في بايثون.
Design Patterns ليست كود جاهز للنسخ، بل هي حلول عامة لمشاكل متكررة في التصميم. يمكنك اعتباره “قالب تفكير” يساعدك على تنظيم الكود، تقليل التكرار، وجعل المشروع قابلاً للتوسع.
من أشهر تصنيفات أنماط التصميم:
رغم أن بايثون لغة ديناميكية وبسيطة، إلا أن المشاريع الكبيرة (خاصة في مجال الـ Web أو الـ Microservices أو أدوات تحليل البيانات كبيرة الحجم) تستفيد كثيراً من الأنماط، لأنها:
Factory هو نمط من نوع Creational، فكرته باختصار: بدلاً من أن تنشئ الكائنات (Objects) بشكل مباشر في كل مكان، تنشئها في مكان مركزي مسؤول عن هذا الإنشاء.
لماذا؟ لأنك أحياناً:
النتيجة: كود أسهل في التعديل والاختبار، وأكثر تنظيماً.
Singleton أيضاً نمط من نوع Creational، فكرته: التأكد أنه لا يوجد إلا نسخة واحدة فقط من كائن معيّن في التطبيق، مع توفير نقطة وصول عالمية له.
هذا مفيد في حالات مثل:
لكن Singleton يحتاج حذراً في الاستخدام، خاصة في التطبيقات متعددة الخيوط (Threading في بايثون) أو في الأنظمة الكبيرة التي تحتاج إلى عزل واضح بين المكوّنات.
تخيل أنك تبني نظام دفع (Payment System) في تطبيق Python/Backend يدعم أكثر من مزوّد دفع:
مقاربة ساذجة قد تكون هكذا:
payment_method = input("Enter method: ")
if payment_method == "credit_card":
# إنشاء كائن وتعامل مع الدفع بالبطاقة
elif payment_method == "paypal":
# إنشاء كائن وتعامل مع PayPal
elif payment_method == "apple_pay":
# ...
كلما أضفت وسيلة دفع جديدة تضطر لتعديل هذا الكود وغيره. هذا يكسر مبدأ Open/Closed Principle (الكود يجب أن يكون مفتوحاً للإضافة، مغلقاً للتعديل).
لنعرّف أولاً واجهة (Interface) موحّدة لوسائل الدفع باستخدام Class أساسي:
from abc import ABC, abstractmethod
class PaymentGateway(ABC):
@abstractmethod
def pay(self, amount: float) -> None:
pass
class CreditCardPayment(PaymentGateway):
def pay(self, amount: float) -> None:
print(f"Processing credit card payment of {amount} USD")
class PayPalPayment(PaymentGateway):
def pay(self, amount: float) -> None:
print(f"Processing PayPal payment of {amount} USD")
class ApplePayPayment(PaymentGateway):
def pay(self, amount: float) -> None:
print(f"Processing Apple Pay payment of {amount} USD")
الآن نعرّف Factory مسؤولة عن إنشاء الكائن المناسب:
class PaymentFactory:
@staticmethod
def create_payment(method: str) -> PaymentGateway:
method = method.lower()
if method == "credit_card":
return CreditCardPayment()
elif method == "paypal":
return PayPalPayment()
elif method == "apple_pay":
return ApplePayPayment()
else:
raise ValueError(f"Unsupported payment method: {method}")
استخدام الـ Factory في كود الأعمال (Business Logic):
def process_order(amount: float, method: str) -> None:
payment_gateway = PaymentFactory.create_payment(method)
payment_gateway.pay(amount)
if __name__ == "__main__":
process_order(100.0, "credit_card")
process_order(50.0, "paypal")
بهذا الشكل:
process_order لا تعرف تفاصيل إنشاء الكائنات ولا تحتاج لتعديلها مع كل وسيلة دفع جديدة.يمكنك جعل الـ Factory أكثر مرونة باستخدام قاموس (Dictionary) لتسجيل الكلاسات:
class PaymentFactory:
_registry = {
"credit_card": CreditCardPayment,
"paypal": PayPalPayment,
"apple_pay": ApplePayPayment,
}
@classmethod
def register(cls, method: str, gateway_cls: type[PaymentGateway]) -> None:
cls._registry[method.lower()] = gateway_cls
@classmethod
def create_payment(cls, method: str) -> PaymentGateway:
method = method.lower()
gateway_cls = cls._registry.get(method)
if not gateway_cls:
raise ValueError(f"Unsupported payment method: {method}")
return gateway_cls()
بهذه الطريقة يمكنك تسجيل وسيلة دفع جديدة في ملف مستقل بدون تعديل الكود الأصلي:
class StripePayment(PaymentGateway):
def pay(self, amount: float) -> None:
print(f"Processing Stripe payment of {amount} USD")
PaymentFactory.register("stripe", StripePayment)
الاختبار يصبح أسهل لأنك تختبر كلاس واحد مسؤول عن الإنشاء:
import unittest
class TestPaymentFactory(unittest.TestCase):
def test_create_credit_card_payment(self):
gateway = PaymentFactory.create_payment("credit_card")
self.assertIsInstance(gateway, CreditCardPayment)
def test_unsupported_method_raises_error(self):
with self.assertRaises(ValueError):
PaymentFactory.create_payment("unknown")
if __name__ == "__main__":
unittest.main()
يمكنك أيضاً استخدام mocks لاختبار مناداة pay() بدون تنفيذ فعلي (مثلاً عدم الاتصال بـ API خارجي حقيقي).
تخيل أن لديك تطبيق Web كبير يحتاج إلى:
لا تريد أن تنشئ أكثر من نسخة من هذا الكائن في كل مرة، بل تريد نسخة واحدة مشتركة.
واحدة من أبسط الطرق هي باستخدام متغيّر Module-Level، لكن غالباً نستخدم نمطاً عاماً قابلاً لإعادة الاستخدام، مثل استخدام MetaClass:
class SingletonMeta(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
instance = super().__call__(*args, **kwargs)
cls._instances[cls] = instance
return cls._instances[cls]
نستخدم هذه الميتاكلاس لإنشاء أي Singleton:
class AppConfig(metaclass=SingletonMeta):
def __init__(self):
print("Initializing config...")
self._settings = {
"db_url": "postgres://user:pass@localhost:5432/mydb",
"debug": True,
}
def get(self, key: str, default=None):
return self._settings.get(key, default)
if __name__ == "__main__":
c1 = AppConfig()
c2 = AppConfig()
print(c1 is c2) # True
هنا:
AppConfig() ينشئ كائناً جديداً._instances.في تطبيقات تستخدم threading، يمكن أن يحدث “Race Condition” عند إنشاء أول نسخة. لتفادي ذلك نستخدم Lock:
import threading
class ThreadSafeSingletonMeta(type):
_instances = {}
_lock: threading.Lock = threading.Lock()
def __call__(cls, *args, **kwargs):
# Double-checked locking
if cls not in cls._instances:
with cls._lock:
if cls not in cls._instances:
instance = super().__call__(*args, **kwargs)
cls._instances[cls] = instance
return cls._instances[cls]
نستخدمها كالتالي:
class Logger(metaclass=ThreadSafeSingletonMeta):
def __init__(self):
self.logs = []
def log(self, message: str):
self.logs.append(message)
print(f"[LOG] {message}")
هذا التصميم يضمن أن Logger يملك نسخة واحدة حتى عند وجود أكثر من Thread في التطبيق كما هو الحال في بعض خوادم الويب.
الاختبار الأساسي هو التأكد من أن كل الاستدعاءات تعيد نفس الكائن:
import unittest
class TestSingleton(unittest.TestCase):
def test_singleton_instance(self):
c1 = AppConfig()
c2 = AppConfig()
self.assertIs(c1, c2)
def test_singleton_state_shared(self):
c1 = AppConfig()
c2 = AppConfig()
c1._settings["debug"] = False
self.assertFalse(c2.get("debug"))
if __name__ == "__main__":
unittest.main()
بهذا الشكل نضمن أن:
استخدم Factory عندما:
أمثلة شائعة:
استخدم Singleton عندما:
لكن تجنّبه عندما:
بديل أفضل في كثير من الحالات هو حقن التبعية (Dependency Injection) حيث تمرّر الكائن المطلوب صراحةً للدوال أو الكلاسات بدلاً من الوصول له كـ Singleton عالمي.
في مشاريع Web الكبيرة، غالباً تستخدم هذه الأنماط مع:
للاختبار، غالباً:
setUp() لكل اختبار حتى لا تؤثر الاختبارات على بعضها.عند تصميم مشروع Python متوسط أو كبير، لا تتعامل مع Factory و Singleton كقواعد إلزامية، بل كأدوات:
إذا فهمت لماذا تم تصميم هذه الأنماط وما المشاكل التي تحلّها، سيكون تطبيقك لها في Python أكثر وعياً وفعالية، وستلاحظ فرقاً كبيراً في قابلية الكود للصيانة والتوسّع والاختبار.
شرح أنماط التصميم الأكثر استخداماً في المشاريع الكبيرة، مع أمثلة عملية وشرح سبب الحاجة إليها وكيفية اختبارها.
مساحة اعلانية