أنماط التصميم في بايثون
درس 5 من 5
5أنماط التصميم (Design Patterns) في بايثون
الصورة الكبيرةFactory، Observer، و Decorator، ثلاثة حلول كلاسيكية لثلاث مشاكل تصميم متكررة، مع أمثلة بايثون كاملة ورسومات UML توضّح بنية كل نمط.
ماذا تشاهد في الرسم: المتجر يعلن «طلب جديد» مرة واحدة، فيصل الخبر للبريد والرسائل والسجل معًا. تضيف مشتركًا رابعًا دون أن تلمس كود المتجر.
خارطة هذا الجزء
المفاهيم بالترتيب، اضغط أيًّا منها للقفز إليه:
طريقان إلى نفس الفكرة
أنماط التصميم حلول مجرّبة لمشاكل تتكرر، مثل وصفات الطبخ المعروفة. المصنع يشبه مطعمًا تطلب منه «وجبة نباتية» فيختار هو الطبّاخ المناسب. والمراقب يشبه اشتراكًا في نشرة: كل مشترك يصله الخبر حين يحدث. والمزخرف يشبه إضافة غلاف هدية دون أن تغيّر الهدية.
Factory نمط إنشاء يخفي اختيار الصنف خلف دالة، وObserver نمط سلوك يُعلم قائمة مشتركين بالتغيير، وDecorator نمط بنية يغلّف كائنًا أو دالة ليضيف سلوكًا (ومنه @decorator في بايثون). يحكمها مبدآن: برمج على الواجهة لا التنفيذ، وفضّل التركيب على الوراثة.
الأساسيات المطلوبة
هذا الجزء يبني مباشرة على ما تعلمته في الجزء التاسع، كل نمط تصميم هنا سترسمه أصلاً كـ Class Diagram. تحتاج أيضًا إلى:
| المفهوم | لماذا يهمّك هنا |
|---|---|
Abstract Base Classes (abc.ABC) | تُستخدم لتعريف "العقد" المشترك الذي تلتزم به الكلاسات المختلفة في كل نمط. |
| Duck Typing / Protocol | بايثون لا يفرض واجهات صارمة كجافا؛ typing.Protocol يمنحك مرونة إضافية عند تطبيق الأنماط. |
| التركيب (Composition) من الجزء التاسع | Decorator بالتحديد هو تطبيق مباشر لعلاقة composition/association التي درستها في UML. |
المفاهيم الأساسية
نمط التصميم (Design Pattern) ليس قالب كود جاهز للنسخ، بل وصف لبنية مسؤوليات أثبتت نجاحها في حل مشكلة تصميم متكررة. هو إجابة عن ثلاثة أسئلة: ما المشكلة؟ من هي الأطراف المشاركة وأدوارها؟ ولماذا هذا التركيب تحديدًا يجعل التغيير المستقبلي أسهل وأكثر أمانًا؟
1Creational: أنماط الإنشاء
تُعنى بـكيف تُنشأ الكائنات. مفيدة عندما يكون إنشاء الكائنات مباشرًا ومبعثرًا في أماكن كثيرة من الكود. مثالنا: Factory.
2Structural: أنماط البنية
تُعنى بـكيف تُركَّب الكائنات معًا. مفيدة عندما نريد توسيع السلوك بالتركيب بدل توليد عدد هائل من الكلاسات الفرعية. مثالنا: Decorator.
3Behavioral: أنماط السلوك
تُعنى بـكيف تتواصل الكائنات وتتقاسم المسؤوليات. مفيدة لفصل مُصدر الحدث عن ردود الفعل عليه. مثالنا: Observer.
4المبدآن الحاكمان في المشروع
الأنماط الثلاثة بالتفصيل
1️⃣ Factory: مركزية الإنشاء
المشكلة: إنشاء الكائنات مبعثر عبر الكود بجُمل if/elif متكررة. كل نوع جديد يعني فتح نفس الدالة القديمة وتعديلها، انتهاك مباشر لمبدأ الفتح والإغلاق.
## ❌ الطريقة الهشّة: كل نوع جديد يعني تعديل هذه الدالة
def create_shape(kind):
if kind == "circle":
return Circle()
elif kind == "square":
return Square()
# إضافة "triangle" تعني العودة لتعديل هذا الكود...
الرسم 13: نمط Factory القائم على سجل (Registry): الدالة create() لا تتغيّر أبدًا؛ الأنواع الجديدة تُسجَّل من الخارج.
## ✅ الحل: سجل خارجي (Registry) لا يمسّ منطق الإنشاء الأساسي أبدًا
class ShapeFactory:
def __init__(self):
self._registry = {}
def register_kind(self, name, cls):
self._registry[name] = cls
def create(self, kind):
return self._registry[kind]() # no if/elif branch, ever
factory = ShapeFactory()
factory.register_kind("circle", Circle)
factory.register_kind("square", Square)
# adding a new shape later never touches create():
factory.register_kind("triangle", Triangle)
2️⃣ Observer: الفصل بين المُصدر وردود الفعل
المشكلة: كائن واحد (ناشر/Publisher) يجب أن يُخبر عدة كائنات أخرى (مراقبين/Observers) عند تغيّر حالته، دون أن "يعرف" تفاصيلهم أو يتكوّد اسمهم مباشرة داخله.
الرسم 14: محطة الطقس (Subject) لا "تعرف" شاشة الهاتف أو الموقع تحديدًا؛ فقط تعرف أنها "مراقبون" يملكون update().
class WeatherStation:
def __init__(self):
self._observers = []
def attach(self, observer):
self._observers.append(observer)
def set_temperature(self, value):
self._temperature = value
for obs in self._observers: # لا تعرف أنواعهم الحقيقية
obs.update(self._temperature)
class PhoneDisplay:
def update(self, temperature):
print(f"📱 الجو الآن: {temperature}°")
station = WeatherStation()
station.attach(PhoneDisplay())
station.set_temperature(31)
الرسم 15: نفس النمط بمنظور Sequence Diagram: لاحظ حلقة notify التي تستدعي كل مراقب بدون أن تفرّق بينهم.
3️⃣ Decorator: تركيب السلوك بدل تكديس الوراثة
المشكلة: كائن أساسي له ميزات اختيارية قابلة للدمج (مثل إضافات طلب بيتزا). الوراثة تحتاج كلاسًا فرعيًا لكل توليفة ممكنة، عدد الكلاسات ينفجر مع كل ميزة جديدة.
الرسم 16: كل إضافة (Topping) "تغلّف" بيتزا أخرى وتضيف تكلفتها فقط، دون المساس بالكلاسات الموجودة.
class Pizza:
def cost(self): return 8.0
def description(self): return "بيتزا عادية"
class ToppingDecorator(Pizza):
def __init__(self, wrapped: Pizza):
self._wrapped = wrapped
class CheeseTopping(ToppingDecorator):
def cost(self): return self._wrapped.cost() + 1.5
def description(self): return self._wrapped.description() + " + جبن"
class OliveTopping(ToppingDecorator):
def cost(self): return self._wrapped.cost() + 1.0
def description(self): return self._wrapped.description() + " + زيتون"
order = OliveTopping(CheeseTopping(Pizza()))
print(order.description(), "->", order.cost())
# output: بيتزا عادية + جبن + زيتون -> 10.5
Pizza أو CheeseTopping إطلاقًا.مثال محلول
خرافة مقابل حقيقة
| الخرافة | الحقيقة |
|---|---|
| يجب استخدام أنماط التصميم في كل مشروع | الأنماط أدوات وليست التزامات، استخدامها بدون ضغط تصميمي حقيقي يزيد التعقيد بلا فائدة. |
| حفظ اسم النمط يعني فهمه | معرفة أن هذا "Observer" لا يكفي؛ المطلوب أن تشرح لماذا هذا التركيب يحل مشكلتك تحديدًا. |
| أنماط GoF يجب تطبيقها في بايثون كما في جافا حرفيًا | بايثون الديناميكية (كائنات من الدرجة الأولى، Duck Typing) غالبًا تُفقطّط تطبيق نفس الفكرة بكود أقل رسمية. |
| المزيد من التجريد (Abstraction) دائمًا أفضل | التجريد يجب أن "يكسب" تعقيده بحل مشكلة صيانة حقيقية، وإلا فهو تعقيد بلا داعٍ. |
أين تُستخدم هذه الأنماط فعليًا؟
بوابات الدفع
Factory لإنشاء معالج دفع مناسب (بطاقة، محفظة رقمية، تحويل بنكي) دون تكديس شروط في كل مكان يُنشأ فيه الدفع.
أنظمة الإشعارات
Observer يفصل "حدوث الشيء" (طلب جديد، رسالة) عن "طرق الإخطار" (بريد، رسالة نصية، إشعار تطبيق) التي قد تتغيّر أو تزيد بمرور الوقت.
معالجة طلبات الويب (Middleware)
سلاسل الـ Middleware في أطر عمل الويب (مثل Flask/Django) هي عمليًا Decorators متتالية تُغلّف معالجة الطلب بطبقات صلاحيات، تسجيل، وضغط.
واجهات المستخدم (UI)
مكوّنات واجهة قابلة للتزيين (حدود، ظلال، تمرير) غالبًا مبنية بنمط Decorator بدل عشرات الكلاسات الفرعية.
جرّب بنفسك
تريد إضافة طريقة دفع جديدة دون تعديل الكود الذي ينشئ الطرق القديمة. أي نمط؟
Factory يجمع منطق الإنشاء في مكان واحد، فتسجّل الطريقة الجديدة فيه بدل أن تبحث عن كل مكان يُنشأ فيه دفع.
بطاقات مراجعة سريعة
اختبر فهمك
1. لماذا نقول إن Factory القائم على Registry "مغلق للتعديل"؟
لأن دالة create() نفسها لا تحتاج لأي تغيير عند إضافة نوع جديد؛ فقط نستدعي register_kind() من الخارج. الكود المستقر (create) يبقى كما هو، والتوسعة تأتي بالإضافة لا بالتعديل.
2. ما الفرق الجوهري بين Observer و Dependency (من الجزء التاسع)؟
Dependency هي علاقة UML عامة تصف أي اعتماد مؤقت بين كلاسين. Observer نمط تصميم محدد يستخدم هذه الفكرة لكن يضيف بنية كاملة: قائمة مشتركين، وميثود update() موحّد، وآلية اشتراك/إلغاء اشتراك ديناميكية.
3. متى يصبح Decorator خيارًا أفضل من الوراثة المباشرة؟
عندما تكون الميزات الإضافية اختيارية ومستقلة وقابلة للدمج بعدة توليفات، الوراثة تحتاج كلاسًا لكل توليفة، بينما Decorator يركّب أي توليفة ديناميكيًا في وقت التشغيل بدون كلاسات إضافية.
4. لو أردت رسم نمط Observer بـ Class Diagram من الجزء التاسع، أي علاقة UML تصف الرابط بين Subject و Observer؟
غالبًا Aggregation (o--): الـ Subject "يحتفظ بقائمة" من الـ Observers لكنه لا يملك دورة حياتهم، يمكن للمراقب أن يوجد وينتهي باستقلالية عن الناشر (بالاشتراك وإلغاء الاشتراك).
مصادر هذا الدرس
مرتّبة بالنوع. ابدأ بالتوثيق الرسمي إذا تبي المرجع الدقيق، وبالمقال أو الفيديو إذا تبي الشرح أولًا.
مقالات3
خلّصت الدرس؟
سجّل الدخول ليُحفظ تقدّمك.