العودة إلى المدونة

SOLID: المبادئ الخمسة الأولى للتصميم كائني التوجه؟

SOLID: المبادئ الخمسة الأولى للتصميم كائني التوجه؟

مقدمة

SOLID هو اختصار تذكيري لخمسة مبادئ للتصميم كائني التوجه التي قدمها روبرت سي مارتن، والذي يُشار إليه شعبياً باسم Uncle Bob. تهدف هذه المبادئ إلى مساعدة مصممي البرمجيات، والمهندسين المعماريين، والمهندسين، والمطورين على إنشاء برمجيات أكثر مرونة، وقابلية للصيانة، والتوسع. من خلال اتباع هذه المبادئ، يمكنك تصميم فئات يسهل اختبارها، وإعادة هيكلتها، وإعادة استخدامها، وتوسيعها.

يرمز اختصار SOLID إلى:

S – مبدأ المسؤولية الفردية

O – مبدأ المفتوح-المغلق

L – مبدأ ليسكوف للاستبدال

I – مبدأ فصل الواجهات

D – مبدأ عكس الاعتمادية

في هذه المقالة، سنشرح كل مبدأ على حدة لفهم كيف يمكن أن يساعدك في كتابة كود أفضل. بالإضافة إلى ذلك، سنضيف مقتطفات كود لكل مبدأ لنوضح لك كيفية تطبيقها في رحلتك البرمجية، بالإضافة إلى ما يجب عليك تجنبه في بنية الكود النظيف. لتوضيح هذه المفاهيم، سنستخدم لغة برمجة Kotlin التي تم تطويرها بواسطة JetBrains والمساهمين في المصادر المفتوحة.

مبدأ المسؤولية الفردية

مبدأ المسؤولية الفردية (SRP) هو مبدأ تصميم برمجيات ينص على أن كل فئة أو وحدة في البرنامج يجب أن تكون لها مسؤولية واحدة محددة جيداً. هذا يعني أنه يجب أن يكون للفئة سبب واحد فقط للتغيير، ويجب أن تكون مسؤولة عن جزء واحد فقط من وظائف البرنامج.

ضع في اعتبارك مقتطف الكود التالي كمثال على كيفية تطبيق هذا المبدأ في Kotlin:

إن data  الكلمة المفتاحية في Kotlin تشير إلى أن هذه الفئة هي فئة بيانات، مما يعني أنها مخصصة للاحتفاظ بالبيانات ولا تحتوي على أي سلوك معقد. باستخدام فئة البيانات هذه، يمكننا إنشاء UserService  لإدارة المستخدمين كما هو موضح أدناه:

في هذا المثال، فئة UserService لديها مسؤولية واحدة: إدارة المستخدمين في قاعدة البيانات. يتم تمثيل كل مستخدم بواسطة data class User  الكود الذي تمت مشاركته سابقاً. جميع الدوال في فئة UserService مترابطة ومتعلقة بهذه المسؤولية. هذا يجعل الفئة أسهل في الفهم والصيانة، حيث يتضح أن جميع الدوال في الفئة مرتبطة بمهمة واحدة محددة جيداً.

الدالة التي تنتهك مبدأ المسؤولية الفردية (SRP) في فئة UserService ستكون دالة غير مرتبطة بالمسؤولية الأساسية للفئة، وهي إدارة المستخدمين في قاعدة البيانات. على سبيل المثال، ضع في اعتبارك البديل التالي لفئة UserService :

في هذا المثال، فإن طريقة sendEmail  لا تتعلق بالمسؤولية الأساسية لفئة UserService، وهي إدارة المستخدمين في قاعدة البيانات. هذه الطريقة مسؤولة عن إرسال رسائل البريد الإلكتروني، وهو أمر منفصل عن إدارة المستخدمين. ونتيجة لذلك، تنتهك هذه الطريقة مبدأ المسؤولية الفردية (SRP)، لأنها تقدم سببًا ثانيًا لتغيير فئة UserService.

للالتزام بمبدأ المسؤولية الفردية، سيكون من الأفضل فصل وظيفة إرسال البريد الإلكتروني في فئة منفصلة، مثل فئة EmailService . وهذا من شأنه أن يسمح لفئة UserService بالتركيز على مسؤوليتها الأساسية المتمثلة في إدارة المستخدمين، وفئة EmailService بالتركيز على مسؤوليتها في إرسال رسائل البريد الإلكتروني.

يجب أن تلاحظ أن مبدأ المسؤولية الفردية لا يتعلق بعدد الطرق التي تمتلكها الفئة، بل يتعلق بتماسك الطرق والفصل الواضح للمسؤوليات داخل الفئة.

مبدأ المفتوح المغلق

مبدأ المفتوح المغلق (OCP) هو مبدأ لتصميم البرمجيات ينص على أن الكيانات البرمجية (مثل الفئات أو الوحدات أو الدوال) يجب أن تكون مفتوحة للتوسيع، ولكن مغلقة للتعديل. هذا يعني أنه يجب أن يكون من الممكن إضافة وظائف جديدة إلى فئة أو وحدة دون تغيير كودها الحالي.

ضع في اعتبارك فئة UserService من المثال السابق. لنفترض أننا نريد إضافة ميزة جديدة إلى فئة UserService تتيح لنا البحث عن المستخدمين بواسطة عنوان البريد الإلكتروني. إحدى الطرق للقيام بذلك هي إضافة طريقة جديدة إلى فئة UserService كما هو موضح في الكود أدناه:

هذا النهج يعمل، ولكنه ينتهك مبدأ المفتوح المغلق، حيث كان علينا تعديل فئة UserService الحالية من أجل إضافة الميزة الجديدة. النهج الأفضل هو استخدام الوراثة أو التركيب لتوسيع وظائف فئة UserService دون تعديل كودها.

لتحقيق ذلك، يمكننا إنشاء فئة جديدة تسمى UserSearchService  توسع فئة UserService وتضيف وظيفة البحث بالبريد الإلكتروني:

في هذا المثال، فئة UserSearchService مفتوحة للتوسيع، لأنها توفر وظائف إضافية تتجاوز ما تقدمه فئة UserService . وفي الوقت نفسه، تظل فئة UserService مغلقة أمام التعديل، حيث لم نكن بحاجة إلى تعديل الكود الخاص بها من أجل إضافة ميزة البحث عن البريد الإلكتروني.

مبدأ ليسكوف للاستبدال

مبدأ ليسكوف للاستبدال (LSP) هو مبدأ في تصميم البرمجيات ينص على أنه يجب أن يكون من الممكن استبدال كائنات الفئة الأساسية بكائنات الفئة الفرعية دون التأثير على صحة البرنامج. وهذا يعني أن الفئة الفرعية يجب أن تكون بديلاً صالحًا لفئتها الأساسية، ويجب أن تتصرف بنفس الطريقة التي تتصرف بها الفئة الأساسية عند استخدامها في نفس السياق.

سنواصل العرض التوضيحي باستخدام User و UserService من الأمثلة السابقة. للسماح بتوسيع فئة User، نستخدم الكلمة المفتاحية open في لغة Kotlin:

إليك فئة UserService الأصلية التي تستخدم فئة البيانات User أعلاه:

نفترض أننا نريد إنشاء فئة فرعية من User تسمى AdminUser تمثل المستخدمين ذوي الصلاحيات الإدارية. يمكننا القيام بذلك على النحو التالي:

في هذا المثال، تعد فئة AdminUser بديلاً صالحًا لفئة User، حيث تتصرف بنفس الطريقة التي تتصرف بها فئة User ويمكن استخدامها أينما كان من المتوقع وجود كائن User . على سبيل المثال، يمكننا استخدام فئة AdminUser مع فئة UserService على النحو التالي:

هذا الكود صحيح، حيث أن فئة AdminUser تعد بديلاً صالحًا لفئة User ويمكن استخدامها بنفس الطريقة التي يُستخدم بها كائن User.

من المهم ملاحظة أن مبدأ ليسكوف للاستبدال يتعلق بما هو أكثر من مجرد الوراثة. يتعلق الأمر بضمان أن تتصرف كائنات الفئة الفرعية بنفس طريقة كائنات الفئة الأساسية، بغض النظر عن كيفية تنفيذ الفئة الفرعية. على سبيل المثال، إذا كانت فئة AdminUser تتصرف بشكل مختلف عن فئة User بطريقة ما، فإن ذلك سيشكل انتهاكًا لمبدأ ليسكوف للاستبدال، حيث لن تكون بديلاً صالحًا لفئة User .

مبدأ فصل الواجهات

مبدأ فصل الواجهات (ISP) هو مبدأ في تصميم البرمجيات ينص على أنه لا ينبغي إجبار العملاء على الاعتماد على واجهات لا يستخدمونها. وهذا يعني أنه من الجيد عمومًا إنشاء واجهات صغيرة ومحددة تؤدي شيئًا واحدًا بشكل جيد، بدلاً من إنشاء واجهات كبيرة عامة الأغراض تحاول القيام بأشياء كثيرة.

إليك مثال على كيفية تطبيق هذا المبدأ عن طريق إعادة كتابة فئتي User و UserService من الأمثلة السابقة:

في هذا المثال، الـ UserService تُعرّف أربع دوال تتعلق بإدارة المستخدمين في قاعدة البيانات. الـ DatabaseUserService  يُنفّذ هذه الواجهة ويُقدّم تطبيقات ملموسة لهذه الدوال.

افترض أننا نريد إضافة ميزة جديدة إلى UserService والتي تتيح لنا البحث عن المستخدمين بواسطة عنوان البريد الإلكتروني. إحدى الطرق للقيام بذلك هي إضافة دالة جديدة إلى UserService :

لن يعمل الكود الخاص بك، ما لم تقم أيضًا بتنفيذ هذه الدالة في جميع الكلاسات التي تُنفّذ الـ UserService :

على الرغم من أن هذا النهج يعمل، إلا أنه ينتهك مبدأ فصل الواجهات (Interface Segregation Principle)، لأنه يجبر الـ DatabaseUserService  على تنفيذ دالة ( searchUsersByEmail ) قد لا يحتاجها أو يستخدمها.

النهج الأفضل هو إنشاء واجهة  منفصلة لوظيفة البحث بالبريد الإلكتروني:

الآن لدينا واجهات منفصلة وصغيرة ومحددة التركيز، أي UserService  و UserSearchServiceالتي تملك مسؤولية واحدة. يمكن للفئة التي تتطلب جميع وظائف هذه الواجهات أن تقوم بتنفيذها كما هو موضح في مقتطف الكود أدناه:

يلتزم هذا بمبدأ فصل الواجهات (Interface Segregation Principle)، لأنه يضمن أن العملاء (مثل الفئة DatabaseUserService) ليسوا مجبرين على الاعتماد على واجهات لا يستخدمونها.

لفهم هذا المفهوم بشكل أفضل، لنفترض أن لدينا فئة أخرى تسمى MemoryUserService  تقوم بتنفيذ الواجهة UserService  ولكنها لا تحتاج إلى وظيفة البحث بالبريد الإلكتروني، يمكننا كتابة الكود على النحو التالي:

في هذا المثال، الفئة MemoryUserService تحتاج فقط إلى تنفيذ الدوال المعرفة في الواجهة UserService، ولا داعي للقلق بشأن وظيفة البحث بالبريد الإلكتروني. يتيح هذا للفئة MemoryUserService التركيز على مسؤوليتها الأساسية المتمثلة في إدارة المستخدمين في الذاكرة، بدلاً من إجبارها على تنفيذ وظائف غير ذات صلة.

مبدأ عكس الاعتمادية

مبدأ عكس الاعتمادية (DIP) هو مبدأ من مبادئ تصميم البرمجيات ينص على أن الوحدات عالية المستوى يجب ألا تعتمد على الوحدات منخفضة المستوى، بل يجب أن يعتمد كلاهما على التجريدات. هذا يعني أنه من الجيد عمومًا تصميم برمجياتك بطريقة لا تجعل المكونات عالية المستوى مرتبطة بتنفيذات محددة للمكونات منخفضة المستوى، بل تعتمد بدلاً من ذلك على التجريدات (مثل الواجهات أو الفئات المجردة) التي يمكن تنفيذها بطرق مختلفة.

دعونا نلقي نظرة على مثال لكيفية تطبيق هذا المبدأ على الفئتين User و UserService المستخدمتين في مقتطفات الكود السابقة:

في هذا المثال، تعتمد فئة UserService على تجريد يسمى واجهة UserRepository، بدلاً من الاعتماد على تطبيق محدد لمستودع المستخدمين. يتيح لنا هذا تطبيق واجهة UserRepository بطرق مختلفة، مثل استخدام قاعدة بيانات أو تخزين في الذاكرة، دون التأثير على فئة UserService.

على سبيل المثال، إليك تطبيق لواجهة UserRepository التي تستخدم قاعدة بيانات:

إليك تطبيق آخر لواجهة UserRepository التي تستخدم التخزين في الذاكرة:

هذا يجعل النظام أكثر مرونة وقابلية للصيانة، لأنه يتيح لنا تغيير تطبيق المستودع دون التأثير على بقية النظام. كما أنه يسهل اختبار فئة UserService، حيث يمكننا محاكاة تبعية UserRepository في اختباراتنا.

الخاتمة

في هذه المقالة، تحدثنا عن المبادئ الخمسة لـ SOLID  الكود ومقتطفات الكود المشتركة التي تلبي كل مبدأ. يمكن أن يساعدك الالتزام بهذه المبادئ في تصميم أنظمة برمجية أكثر مرونة وقابلية للصيانة والتطوير. ومع ذلك، من المهم أن تضع في اعتبارك أن هذه المبادئ هي إرشادات وليست قواعد صارمة، والأمر متروك للمطور ليقرر متى وكيف يطبقها في سياق مشروعه المحدد. واصل تعلمك من خلال مراجعة مدونتنا للحصول على مقالات أكثر عمقًا وحداثة ودروس تعليمية حول الحوسبة السحابية و DevOps، تصميم البرمجيات وتطويرها، واتجاهات التكنولوجيا التي يجب ترقبها، والمزيد.

برمجة سعيدة!

author

Preslav Dobrev

المؤلف · CloudSigma

Preslav Dobrev هو مصمم إبداعي في CloudSigma، يركز على هوية أعمال متسقة باستخدام قنوات التسويق التقليدية والمبتكرة. هو بارع في دمج الرؤية الفنية مع التسويق الاستراتيجي لخلق سرد قصصي مؤثر للعلامة التجارية.

التعليقات

لا توجد تعليقات بعد. كن أول من يعلق.