عندما تبدأ العمل على مشاريع بايثون كبيرة، فإن أحد أول الأشياء التي ستلاحظها هو أن يصبح الكود صعب الفهم والاختبار والتطوير. إذا لم تتبع بعض قواعد التصميم الأساسية، فهنا تبرز أهمية مبادئ SOLID الشهيرة: وهي مجموعة من أفضل الممارسات المصممة لتسهيل عمل الفريق بشكل كبير.
نشأت هذه المبادئ في مجال البرمجة الكلاسيكية الموجهة للكائنات (جافا، سي++، سي#، إلخ).لكنها تتناسب تمامًا مع لغة بايثون طالما أنك تستخدم الفئات والكائنات بطريقة جادة إلى حد ما. دعونا نلقي نظرة مفصلة على ماهيتها، ومن أين أتت، ولماذا هي مهمة، وقبل كل شيء، كيف تطبيق مبادئ SOLID مع أمثلة واضحة في لغة بايثون لجعل الكود الخاص بك أكثر قابلية للصيانة والتوسع، وأكثر متعة في التعامل معه.
ما هو مفهوم SOLID، ومن أين يأتي كل هذا؟
على المدى SOLID هو اختصار شاع استخدامه بفضل مايكل فيذرز لتجميع خمسة مبادئ تصميم اقترحها في الأصل روبرت سي. مارتن، المعروف باسم العم بوب. نشر مهندس البرمجيات الأمريكي هذا، وهو أحد الموقعين على بيان أجايل، مقالة بعنوان "مبادئ التصميم الموجه للكائنات" في منتصف التسعينيات، ولاحقًا مقالة بعنوان "مبادئ التصميم وأنماط التصميم"، حيث وضع العديد من أسس التصميم الحديث الموجه للكائنات.
بمرور الوقت، قام مؤلفون آخرون مثل باربرا ليسكوف وبرتراند ماير كما ساهموا بأفكارٍ دُمجت في هذه المجموعة من المبادئ. ببساطة، خطرت لمايكل فيذرز فكرة (ذكية للغاية) لإعادة ترتيبها بحيث تُشكّل الأحرف الأولى كلمة SOLID، مما ساعدها على الانتشار بسرعة كبيرة في مجتمع التطوير.
تتوافق الأحرف الخمسة لكلمة SOLID مع مبادئ التصميم الموجهة للكائنات هذه، والتي تنطبق أيضًا على لغة بايثون:
- مبدأ المسؤولية الفردية (S) (مبدأ المسؤولية الفردية)
- O – مبدأ الانفتاح/الإغلاق (مبدأ الانفتاح/الإغلاق)
- L – مبدأ استبدال ليسكوف (مبدأ استبدال ليسكوف)
- أولاً - مبدأ فصل الواجهات (مبدأ فصل الواجهة)
- د - مبدأ عكس التبعية (مبدأ عكس التبعية)
الفكرة العامة هي أن هذه المبادئ الخمسة، عند استخدامها معًا، تساعدك هذه الأدوات في كتابة برامج مرنة وسهلة الاختبار والصيانةوهذا يترجم إلى عمليات نشر أسرع، وأخطاء غامضة أقل، وإعادة استخدام أفضل للتعليمات البرمجية، ومشاكل أقل عندما يكون المشروع قيد الإنتاج لعدة سنوات.
ما هي مبادئ SOLID المستخدمة في لغة بايثون؟
إن تطبيق مبادئ SOLID في بايثون ليس مجرد تمرين أكاديمي، بل له تأثير مباشر على العمل اليومي للفريق. عندما تلتزم بهذه المبادئ، فهي تقلل من تعقيد الكود، وتقلل من رائحة الكود، وتمنع قاعدة الكود الخاصة بك من أن "تفوح منها رائحة كريهة".باستخدام التشبيه الشهير، "إذا كانت الرائحة كريهة، فهذا يعني أن التصميم رديء". في نظام ويندوز، يختار العديد من المطورين تثبيت وتكوين WSL2 للحصول على بيئة لينكس أقرب إلى بيئة الإنتاج.
في بيئات العمل التعاونية (فرق تطوير الواجهة الخلفية، وهندسة البيانات، والمنتجات ذات دورات التطوير الطويلة، وما إلى ذلك)، تُعد هذه المبادئ أساسية لـ يمكن لعدة أشخاص العمل على نفس قاعدة البيانات دون تجاوز حدودهم أو إتلاف كل شيء بأدنى لمسة.علاوة على ذلك، تسمح لغة بايثون، على الرغم من مرونتها وديناميكيتها، بالتطبيق السلس لمفاهيم البرمجة الكائنية النموذجية: الفئات المجردة، والتسلسلات الهرمية للوراثة، والتركيب، والواجهات عبر abc، الخ.
باختصار، يساعدك SOLID على تحقيق ما يلي:
- كود أنظف وأكثر قابلية للقراءةحتى بعد مرور سنوات على كتابتها.
- تحسين قابلية الاختبارلأن المسؤوليات منفصلة بوضوح.
- قابلية عالية لإعادة الاستخدام والتوسع بفضل تقليل الاعتمادات الصارمة بين الوحدات.
- أخطاء جانبية أقلعندما تقوم بتغيير شيء ما في وحدة واحدة، فإنك لا تكسر خمسة أشياء أخرى عن طريق الخطأ.
مبدأ المسؤولية الفردية (S)
ينص المبدأ الأول على أن لا ينبغي أن يكون لدى أي فئة سوى سبب واحد للتغيير.بمعنى آخر، يجب أن تضطلع بمسؤولية واحدة محددة بوضوح. وهذا لا يعني امتلاك طريقة واحدة فقط، بل يعني أن منطقها برمته يجب أن يصب في هدف واحد متماسك.
تخيل فئة بايثون تمثل مستخدمًا، بالإضافة إلى تخزين بياناته، تتولى أيضًا الوصول إلى قاعدة البيانات وإنشاء التقارير:
class User:
def __init__(self, name: str):
self.name = name
def get_user_from_database(self, user_id: int) -> dict:
# Recupera datos desde la base de datos
# ...
pass
def save_user_to_database(self) -> None:
# Persiste el usuario en la base de datos
# ...
pass
def generate_user_report(self) -> str:
# Genera un informe del usuario
# ...
pass
إليكم الفصل يجمع بين ثلاث مسؤوليات متميزةتمثيل المستخدم، وإدارة استمرارية البيانات، وإنشاء التقارير. تتطلب التغييرات في قاعدة البيانات، أو تنسيق التقرير، أو سمات المستخدم تعديل نفس الفئة، مما يزيد من خطر ظهور أخطاء شاملة.
إذا فصلنا هذه المخاوف، فإن التصميم يتحسن بشكل ملحوظ:
class User:
def __init__(self, name: str):
self.name = name
class UserDB:
@staticmethod
def get_user(user_id: int) -> User:
# Lógica para obtener usuarios de la base de datos
# ...
return User("John Doe")
@staticmethod
def save_user(user: User) -> None:
# Lógica para guardar el usuario
# ...
pass
class UserReportGenerator:
@staticmethod
def generate_report(user: User) -> str:
# Lógica para generar informes de usuario
# ...
return f"Report for user: {user.name}"
والآن الفصل لا يمثل المستخدم سوى المستخدم ككيانإذا تغيرت طريقة إنشاء التقارير، فما عليك سوى النقر UserReportGeneratorإذا قمت بتغيير قاعدة البيانات، فما عليك سوى لمسها UserDBلكل فئة سبب واحد للتغيير، مما يبسط عملية تصحيح الأخطاء وتطوير النظام.
تطبيق مبدأ المسؤولية الاجتماعية للشركات على مثال أكثر واقعية: البط والتواصل
دعونا نلقي نظرة على سيناريو كلاسيكي تم تعديله: فصل دراسي بطة ثم تُضاف إليها المسؤوليات تدريجياً في البداية حتى تصبح عبئاً ثقيلاً يصعب الحفاظ عليه. تخيل تطبيقاً بسيطاً:
class Duck:
def __init__(self, name: str):
self.name = name
def fly(self) -> None:
print(f"{self.name} is flying not very high")
def swim(self) -> None:
print(f"{self.name} swims in the lake and quacks")
def do_sound(self) -> str:
return "Quack"
def greet(self, other_duck: "Duck") -> None:
print(f"{self.name}: {self.do_sound()}, hello {other_duck.name}")
فصل ينبغي تعريفها ببساطة بأنها "بطة".لكنها تُدير أيضًا كيفية تواصلهم مع بعضهم البعض. إذا غيّرتَ غدًا منطق المحادثة (عبارات إضافية، لغات أخرى، قنوات مختلفة)، فسيتعين عليك تعديل فئة "البطة"، التي تعمل بالفعل بشكل جيد ككيان.
الحل الذي يحترم مبدأ المسؤولية الواحدة هو استخراج تلك المسؤولية الثانية من فئة أخرى متخصصة في الاتصالات:
class Duck:
def __init__(self, name: str):
self.name = name
def fly(self) -> None:
print(f"{self.name} is flying not very high")
def swim(self) -> None:
print(f"{self.name} swims in the lake and quacks")
def do_sound(self) -> str:
return "Quack"
class Communicator:
def __init__(self, channel: str):
self.channel = channel
def communicate(self, duck1: Duck, duck2: Duck) -> None:
sentence1 = f"{duck1.name}: {duck1.do_sound()}, hello {duck2.name}"
sentence2 = f"{duck2.name}: {duck2.do_sound()}, hello {duck1.name}"
conversation =
print(*conversation, f"(via {self.channel})", sep="\n")
بفضل هذا الفصل، يمكنك تطوير منطق الاتصال دون المساس بتعريف البطةعلاوة على ذلك، يسهل اختبار الكود: إذ يمكنك اختبار سلوك Duck ومن جهة أخرى، أحد Communicatorدون خلط المسؤوليات.
O – مبدأ الانفتاح/الإغلاق
ينص مبدأ OCP على ما يلي: ينبغي أن تكون الكيانات البرمجية منفتحة على توسيع سلوكها، ولكنها مغلقة أمام التعديلات المباشرة.بمعنى آخر، عندما تريد إضافة وظائف جديدة، من الناحية المثالية، لا ينبغي عليك إعادة كتابة الفئات التي تعمل بالفعل وتستخدمها وحدات أخرى.
من الأمثلة الكلاسيكية حساب مساحات الأشكال الهندسية. لنلقِ نظرة أولاً على نسخة من لا يحترم OCP:
class Rectangle:
def __init__(self, width: float, height: float):
self.width = width
self.height = height
class Circle:
def __init__(self, radius: float):
self.radius = radius
class AreaCalculator:
def calculate_area(self, shape) -> float:
if isinstance(shape, Rectangle):
return shape.width * shape.height
elif isinstance(shape, Circle):
return 3.14159 * shape.radius * shape.radius
else:
raise ValueError("Forma no soportada")
إذا أردت إضافة مثلث غدًا، فسوف تُجبر على قم بتعديل رمز AreaCalculatorأضف آخر elifهذا ينتهك مبدأ الانفتاح والإغلاق، لأن الفئة لم تعد "مغلقة" أمام التغييرات.
تتضمن النسخة الصحيحة إدخال تجريد Shape باستخدام طريقة area() والتي ينفذها كل شخص بطريقته الخاصة:
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float:
pass
class Rectangle(Shape):
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
class Circle(Shape):
def __init__(self, radius: float):
self.radius = radius
def area(self) -> float:
return 3.14159 * self.radius * self.radius
class AreaCalculator:
def calculate_area(self, shape: Shape) -> float:
return shape.area()
بفضل هذا التصميم، لـ أضف مثلثًا لا تلمسه AreaCalculatorما عليك سوى إنشاء فئة فرعية جديدة:
class Triangle(Shape):
def __init__(self, base: float, height: float):
self.base = base
self.height = height
def area(self) -> float:
return 0.5 * self.base * self.height
يتناسب مبدأ الانفتاح/الإغلاق تمامًا مع فكرة تحديد نقاط التوسع الواضحة من خلال التجريدات: الواجهات، والفئات المجردة، والخطافات، وما إلى ذلك. في بايثون، الوحدة abc فهو يسمح لك بالتعبير عن ذلك بشكل صريح، حتى لو كانت اللغة ديناميكية.
تطبيق مبدأ الانفتاح والإغلاق على مثال جهاز الاتصال
إذا عدنا إلى مثال مراسلاتيمكننا المضي قدمًا خطوةً أخرى، وإعداد التصميم لدعم أنواع مختلفة من المحادثات دون الحاجة إلى إعادة كتابة برنامج التواصل في كل مرة. ولتحقيق ذلك، نُعرّف نموذجًا تجريديًا للمحادثة، ونجعل برنامج التواصل يستخدمه فقط.
from typing import final
from abc import ABC, abstractmethod
class AbstractConversation(ABC):
@abstractmethod
def do_conversation(self) -> list:
pass
class SimpleConversation(AbstractConversation):
def __init__(self, duck1: Duck, duck2: Duck):
self.duck1 = duck1
self.duck2 = duck2
def do_conversation(self) -> list:
sentence1 = f"{self.duck1.name}: {self.duck1.do_sound()}, hello {self.duck2.name}"
sentence2 = f"{self.duck2.name}: {self.duck2.do_sound()}, hello {self.duck1.name}"
return
class Communicator:
def __init__(self, channel: str):
self.channel = channel
@final
def communicate(self, conversation: AbstractConversation) -> None:
print(*conversation.do_conversation(), f"(via {self.channel})", sep="\n")
في هذا الإصدار ، إذا كنت ترغب في إضافة طريقة جديدة للتحدث (على سبيل المثال، محادثة عدائية، محادثة تبادل الأدوار، إلخ)، ما عليك سوى إنشاء فئة فرعية أخرى من AbstractConversation. طريقة communicate() de Communicator لا يتغير، فهو يلتزم بقانون حماية المستهلك التزاماً تاماً.
L – مبدأ استبدال ليسكوف
ينص مبدأ الاستبدال لليسكوف، الذي صاغته باربرا ليسكوف، على ما يلي: ينبغي أن تكون الفئات الفرعية قادرة على استبدال فئاتها الأساسية دون تغيير السلوك المتوقع للبرنامج.من الناحية العملية، هذا يعني أنه إذا كان الكود يعمل مع نسخة واحدة من الفئة الأساسية، فيجب أن يعمل بنفس الكفاءة مع أي نسخة من الفئة الفرعية.
من الأمثلة النموذجية على انتهاك مبدأ LSP هو نمذجة جميع الطيور باستخدام طريقة واحدة fly()بما في ذلك النعام:
class Bird:
def fly(self) -> None:
pass
class Duck(Bird):
def fly(self) -> None:
print("¡El pato está volando!")
class Ostrich(Bird):
def fly(self) -> None:
# Las avestruces no vuelan
raise NotImplementedError("Las avestruces no pueden volar")
أي كود يفترض أن كل طائر قادر على الطيران سيفشل عندما يتلقى نعامة. أقصد ، Ostrich لا يُعد بديلاً صالحاً لـ Birdوبذلك ينتهك مبدأ LSP.
يكمن الحل في تعديل التسلسل الهرمي ليعكس الواقع بشكل أفضل: فليست كل الطيور تطير، لذلك ينبغي أن تتبع هذه الطريقة نسبة معينة فقط من الطيور fly():
class Bird:
pass
class FlyingBird(Bird):
def fly(self) -> None:
pass
class Duck(FlyingBird):
def fly(self) -> None:
print("¡El pato está volando!")
class Ostrich(Bird):
# No vuela, así que no implementa fly()
pass
مع هذا التصميم، أي دالة تتطلب طائرًا طائرًا ستعلن أنها تتطلب واحدًا. FlyingBirdولن يتلقى أي رد فعل سلبي. وبهذه الطريقة، يتم احترام مبدأ استبدال ليسكوف (LSP) وتجنب استثناءات وقت التشغيل غير المتوقعة.
LSP ومحادثات الطيور
بالعودة إلى مثال المحادثات، من الشائع البدء بالبرمجة بالتفكير في البط فقط، ثم الرغبة في إضافة الغربان أو الطيور الأخرى. إذا كانت فئة المحادثة تعتمد على Duck, لن تتمكن من إعادة استخدامه مع أنواع أخرى من الطيور دون المساس بالكود:
class Crow:
# Implementación específica del cuervo
...
Si SimpleConversation هذا النوع مخصص للبط فقط؛ لن تتمكن من تطبيقه على الغراب دون تعديله. والنهج الصحيح هو إنشاء تجريد مشترك. Bird واجعل المحادثة تعتمد على هذا التجريد:
from abc import ABC, abstractmethod
class Bird(ABC):
def __init__(self, name: str):
self.name = name
@abstractmethod
def do_sound(self) -> str:
pass
class Crow(Bird):
def do_sound(self) -> str:
return "Caw"
class Duck(Bird):
def do_sound(self) -> str:
return "Quack"
class SimpleConversation(AbstractConversation):
def __init__(self, bird1: Bird, bird2: Bird):
self.bird1 = bird1
self.bird2 = bird2
def do_conversation(self) -> list:
sentence1 = f"{self.bird1.name}: {self.bird1.do_sound()}, hello {self.bird2.name}"
sentence2 = f"{self.bird2.name}: {self.bird2.do_sound()}, hello {self.bird1.name}"
return
وبهذه الطريقة، أي فئة فرعية من Bird بما يحترم العقد (do_sound()(الاسم، إلخ) هو بديل صالح ولن يخالف السلوك المتوقع لـ SimpleConversation.
أولاً - مبدأ فصل الواجهات
ينص مبدأ مزود خدمة الإنترنت على ما يلي: لا ينبغي إجبار أي عميل على الاعتماد على أساليب لا يستخدمها.وإذا ترجمنا ذلك إلى فئات أو واجهات مجردة، فهذا يعني أنه من الأفضل أن يكون لديك عدة واجهات صغيرة ومحددة بدلاً من واجهة واحدة ضخمة وعامة.
لاحظ هذا التصميم الذي يتضمن واجهة Worker يتطلب ذلك من جميع من يقومون بتنفيذه اتباع أساليب محددة في العمل وتناول الطعام:
from abc import ABC, abstractmethod
class Worker(ABC):
@abstractmethod
def work(self) -> None:
pass
@abstractmethod
def eat(self) -> None:
pass
class Human(Worker):
def work(self) -> None:
print("El humano está trabajando")
def eat(self) -> None:
print("El humano está comiendo")
class Robot(Worker):
def work(self) -> None:
print("El robot está trabajando")
def eat(self) -> None:
# El robot no come, pero está obligado a declarar este método
pass
فصل يعتمد الروبوت على طريقة eat() هذا لا يحتاجأي تغيير يتعلق بالطعام سيؤثر على الروبوت، حتى لو لم يكن له علاقة بهذا السلوك.
من خلال تطبيق ISP، قمنا بتقسيم الواجهة إلى واجهتين أصغر وأكثر تحديدًا:
class Workable(ABC):
@abstractmethod
def work(self) -> None:
pass
class Eatable(ABC):
@abstractmethod
def eat(self) -> None:
pass
class Human(Workable, Eatable):
def work(self) -> None:
print("El humano está trabajando")
def eat(self) -> None:
print("El humano está comiendo")
class Robot(Workable):
def work(self) -> None:
print("El robot está trabajando")
الآن لا تُنفذ كل فئة إلا الطرق التي تحتاجها فعلياً.هذا يقلل من الترابط، ويسهل تطور التصميم، ويجعل الكود أكثر تعبيرًا: يصبح من الواضح جدًا من يمكنه فعل ماذا.
ISP في نمذجة الطيور: الطيران والسباحة
يحدث شيء مشابه عند تصميم نماذج للطيور التي تطير وتسبح. إذا كان التجريد الأساسي Bird يتطلب ذلك تنفيذ كليهما fly() كما swim()سينتهي بك الأمر بفصول دراسية مثل Crow الذين يضطرون للتظاهر بأنهم يعرفون السباحة:
class Bird(ABC):
def __init__(self, name: str):
self.name = name
@abstractmethod
def fly(self) -> None:
pass
@abstractmethod
def swim(self) -> None:
pass
@abstractmethod
def do_sound(self) -> str:
pass
الحل وفقًا لمزود خدمة الإنترنت هو قم بتقسيم الواجهة إلى قدرات أكثر تحديدًا:
class Bird(ABC):
def __init__(self, name: str):
self.name = name
@abstractmethod
def do_sound(self) -> str:
pass
class FlyingBird(Bird):
@abstractmethod
def fly(self) -> None:
pass
class SwimmingBird(Bird):
@abstractmethod
def swim(self) -> None:
pass
class Crow(FlyingBird):
def fly(self) -> None:
print(f"{self.name} is flying high and fast!")
def do_sound(self) -> str:
return "Caw"
class Duck(SwimmingBird, FlyingBird):
def fly(self) -> None:
print(f"{self.name} is flying not very high")
def swim(self) -> None:
print(f"{self.name} swims in the lake and quacks")
def do_sound(self) -> str:
return "Quack"
إذا قررت يوماً ما تصميم نموذج لبطريق، فما عليك سوى تجعله يرث من SwimmingBird ولكن ليس من FlyingBirdولن تضطر إلى تنفيذ طرق فارغة أو إطلاق استثناءات مصطنعة.
د - مبدأ عكس التبعية
يمكن تلخيص المبدأ الأخير، DIP، في فكرتين رئيسيتين: لا ينبغي أن تعتمد الوحدات النمطية عالية المستوى على الوحدات النمطية منخفضة المستوى؛ بل يجب أن يعتمد كلاهما على التجريدات.وينبغي ألا تعتمد المفاهيم المجردة على التفاصيل، بل ينبغي أن تعتمد التفاصيل على المفاهيم المجردة.
عمليًا، هذا يعني أنه لا ينبغي ربط منطق عملك بتفاصيل محددة مثل "أستخدم MySQL" أو "أكتب إلى ملف محلي" أو "أرسل رسائل SMS باستخدام هذا المزود". بدلاً من ذلك، يمكنك تحديد واجهات مجردة (على سبيل المثال ، Database, Channel, NotificationService) وتجعل الكود عالي المستوى الخاص بك يتحدث معهم فقط.
تصميم كسر DIP سيكون هذا مستودعًا للمستخدم يقوم بإنشاء قاعدة بيانات MySQL بشكل مباشر:
class MySQLDatabase:
def connect(self) -> None:
# Conectar a MySQL
pass
def query(self, sql: str) -> list:
# Ejecutar consulta
return []
class UserRepository:
def __init__(self) -> None:
self.database = MySQLDatabase() # Dependencia directa
def get_users(self) -> list:
return self.database.query("SELECT * FROM users")
إذا قررت استخدام PostgreSQL غدًا، فعليك أن تعديل الفئة عالية المستوى UserRepositoryأنت مرتبط بتفاصيل تنفيذ محددة.
بتطبيق مبدأ عكس التبعية (DIP)، نقوم أولاً بتعريف تجريد قاعدة البيانات، ثم نجعل التطبيقات الملموسة ترث منه:
from abc import ABC, abstractmethod
class Database(ABC):
@abstractmethod
def connect(self) -> None:
pass
@abstractmethod
def query(self, sql: str) -> list:
pass
class MySQLDatabase(Database):
def connect(self) -> None:
# Conexión a MySQL
pass
def query(self, sql: str) -> list:
# Consulta en MySQL
return []
class PostgreSQLDatabase(Database):
def connect(self) -> None:
# Conexión a PostgreSQL
pass
def query(self, sql: str) -> list:
# Consulta en PostgreSQL
return []
class UserRepository:
def __init__(self, database: Database) -> None:
self.database = database # Depende de una abstracción
def get_users(self) -> list:
return self.database.query("SELECT * FROM users")
وهكذا، يمكنك إدخال أي تطبيق لـ Database عند إنشاء المستودع، دون المساس بشفرته الداخلية:
mysql_db = MySQLDatabase()
user_repo = UserRepository(mysql_db)
postgres_db = PostgreSQLDatabase()
user_repo = UserRepository(postgres_db)
يُعرف هذا النمط باسم حقن التبعية وهي الطريقة الأكثر شيوعًا لتطبيق DIP: لا تقوم الفئات بإنشاء تبعياتها الخاصة، ولكنها تستقبلها من الخارج (من خلال المُنشئ أو من خلال طرق محددة)، وتستخدم دائمًا التجريدات كنوع.
تطبيق DIP على القنوات وأجهزة الاتصال
في مثال محادثات الطيور، يمكننا أيضًا تحسين إدارة القنوات من خلال تطبيق مبدأ عكس التجريد (DIP). لنفترض أنك قمت بتعريف تجريد واحد للقناة وآخر للمتصل:
class AbstractChannel(ABC):
@abstractmethod
def get_channel_message(self) -> str:
pass
class AbstractCommunicator(ABC):
@abstractmethod
def get_channel(self) -> AbstractChannel:
pass
@final
def communicate(self, conversation: AbstractConversation) -> None:
print(*conversation.do_conversation(),
self.get_channel().get_channel_message(),
sep="\n")
يمكن أن يكون التنفيذ الأولي البسيط كالتالي:
class SMSChannel(AbstractChannel):
def get_channel_message(self) -> str:
return "(via SMS)"
class SMSCommunicator(AbstractCommunicator):
def __init__(self) -> None:
self._channel = SMSChannel() # Depende de detalle concreto
def get_channel(self) -> AbstractChannel:
return self._channel
على الرغم من أنه يبدو صحيحاً، لا يزال هذا الجهاز متصلاً بشكل مباشر بـ SMSChannelلقد قمنا بتحسين التصميم من خلال جعل جهاز الاتصال يستقبل القناة من الخارج (حقن التبعية)، وبالتالي يعتمد فقط على التجريد:
class SimpleCommunicator(AbstractCommunicator):
def __init__(self, channel: AbstractChannel) -> None:
self._channel = channel
def get_channel(self) -> AbstractChannel:
return self._channel
وبهذا النهج، يتم تطبيق أي قناة جديدة (البريد الإلكتروني، الإشعارات الفورية، إلخ). AbstractChannel y يمكن استخدامه دون تغيير رمز الاتصال.مرة أخرى، تعتمد الفئات عالية المستوى على المفاهيم المجردة، وليس على التفاصيل.
ماذا يحدث عندما تتجاهل SOLID؟
إذا لم تُؤخذ هذه المبادئ في الاعتبار، فإن الكود يميل إلى المعاناة من مشاكل مثل رائحة الكود، وتلف الكود، والترابطات التي يستحيل فكهاأي أن هناك فئات ضخمة ذات ألف مسؤولية، وفئات فرعية تخرق العقود، وتبعيات دورية، وأساليب تتغير كل يومين لأنها تقوم بأشياء كثيرة للغاية.
العواقب واضحة ومؤلمة للغاية لأي فريق: المزيد من الثغرات الأمنية، والمزيد من الأخطاء، وإعادة هيكلة مستمرة، وفي أسوأ الأحوال، شفرة برمجية تصبح غير قابلة للاستخدام عمليًا.هذا ما يسمى عادةً "شفرة السباغيتي": يصعب اتباعها، ومليئة بالتصحيحات، ويكاد يكون من المستحيل توسيعها دون كسر شيء مهم.
مبادئ SOLID ليست ثابتة، وليس من المجدي دائمًا تطبيقها جميعًا بشكل صارم، خاصة في النماذج الأولية السريعة أو المشاريع الصغيرة جدًا. ومع ذلك، ضع هذه الأمور في اعتبارك وطبقها على معظم تصميماتك الموجهة للكائنات في بايثون. إنه يُحدث الفرق بين مشروع يتوسع بمرور الوقت ومشروع ينهار بمجرد أن ينمو قليلاً.