مقدمة
SOLID هو اختصار تذكيري لخمسة مبادئ للتصميم كائني التوجه التي قدمها روبرت سي مارتن، والذي يُشار إليه شعبياً باسم Uncle Bob. تهدف هذه المبادئ إلى مساعدة مصممي البرمجيات، والمهندسين المعماريين، والمهندسين، والمطورين على إنشاء برمجيات أكثر مرونة، وقابلية للصيانة، والتوسع. من خلال اتباع هذه المبادئ، يمكنك تصميم فئات يسهل اختبارها، وإعادة هيكلتها، وإعادة استخدامها، وتوسيعها.
يرمز اختصار SOLID إلى:
S – مبدأ المسؤولية الفردية
O – مبدأ المفتوح-المغلق
L – مبدأ ليسكوف للاستبدال
I – مبدأ فصل الواجهات
D – مبدأ عكس الاعتمادية
في هذه المقالة، سنشرح كل مبدأ على حدة لفهم كيف يمكن أن يساعدك في كتابة كود أفضل. بالإضافة إلى ذلك، سنضيف مقتطفات كود لكل مبدأ لنوضح لك كيفية تطبيقها في رحلتك البرمجية، بالإضافة إلى ما يجب عليك تجنبه في بنية الكود النظيف. لتوضيح هذه المفاهيم، سنستخدم لغة برمجة Kotlin التي تم تطويرها بواسطة JetBrains والمساهمين في المصادر المفتوحة.
مبدأ المسؤولية الفردية
مبدأ المسؤولية الفردية (SRP) هو مبدأ تصميم برمجيات ينص على أن كل فئة أو وحدة في البرنامج يجب أن تكون لها مسؤولية واحدة محددة جيداً. هذا يعني أنه يجب أن يكون للفئة سبب واحد فقط للتغيير، ويجب أن تكون مسؤولة عن جزء واحد فقط من وظائف البرنامج.
ضع في اعتبارك مقتطف الكود التالي كمثال على كيفية تطبيق هذا المبدأ في Kotlin:
|
1 2 3 4 5 6 7 8 9 |
data class User( val firstName: String, val lastName: String, val email: String ) |
إن data الكلمة المفتاحية في Kotlin تشير إلى أن هذه الفئة هي فئة بيانات، مما يعني أنها مخصصة للاحتفاظ بالبيانات ولا تحتوي على أي سلوك معقد. باستخدام فئة البيانات هذه، يمكننا إنشاء UserService لإدارة المستخدمين كما هو موضح أدناه:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
class UserService { fun createUser(user: User) { // كود لإنشاء مستخدم في قاعدة البيانات } fun deleteUser(user: User) { // كود لحذف مستخدم من قاعدة البيانات } fun updateUser(user: User) { // كود لتحديث مستخدم في قاعدة البيانات } fun getUser(id: Int): User { // كود لاسترداد مستخدم من قاعدة البيانات } } |
في هذا المثال، فئة UserService لديها مسؤولية واحدة: إدارة المستخدمين في قاعدة البيانات. يتم تمثيل كل مستخدم بواسطة data class User الكود الذي تمت مشاركته سابقاً. جميع الدوال في فئة UserService مترابطة ومتعلقة بهذه المسؤولية. هذا يجعل الفئة أسهل في الفهم والصيانة، حيث يتضح أن جميع الدوال في الفئة مرتبطة بمهمة واحدة محددة جيداً.
الدالة التي تنتهك مبدأ المسؤولية الفردية (SRP) في فئة UserService ستكون دالة غير مرتبطة بالمسؤولية الأساسية للفئة، وهي إدارة المستخدمين في قاعدة البيانات. على سبيل المثال، ضع في اعتبارك البديل التالي لفئة UserService :
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
class UserService { fun createUser(user: User) { // كود لإنشاء مستخدم في قاعدة البيانات } fun deleteUser(user: User) { // كود لحذف مستخدم من قاعدة البيانات } fun updateUser(user: User) { // كود لتحديث مستخدم في قاعدة البيانات } fun getUser(id: Int): User { // كود لاسترداد مستخدم من قاعدة البيانات } fun sendEmail(user: User, subject: String, message: String) { // كود لإرسال بريد إلكتروني إلى المستخدم } } |
في هذا المثال، فإن طريقة sendEmail لا تتعلق بالمسؤولية الأساسية لفئة UserService، وهي إدارة المستخدمين في قاعدة البيانات. هذه الطريقة مسؤولة عن إرسال رسائل البريد الإلكتروني، وهو أمر منفصل عن إدارة المستخدمين. ونتيجة لذلك، تنتهك هذه الطريقة مبدأ المسؤولية الفردية (SRP)، لأنها تقدم سببًا ثانيًا لتغيير فئة UserService.
للالتزام بمبدأ المسؤولية الفردية، سيكون من الأفضل فصل وظيفة إرسال البريد الإلكتروني في فئة منفصلة، مثل فئة EmailService . وهذا من شأنه أن يسمح لفئة UserService بالتركيز على مسؤوليتها الأساسية المتمثلة في إدارة المستخدمين، وفئة EmailService بالتركيز على مسؤوليتها في إرسال رسائل البريد الإلكتروني.
يجب أن تلاحظ أن مبدأ المسؤولية الفردية لا يتعلق بعدد الطرق التي تمتلكها الفئة، بل يتعلق بتماسك الطرق والفصل الواضح للمسؤوليات داخل الفئة.
مبدأ المفتوح المغلق
مبدأ المفتوح المغلق (OCP) هو مبدأ لتصميم البرمجيات ينص على أن الكيانات البرمجية (مثل الفئات أو الوحدات أو الدوال) يجب أن تكون مفتوحة للتوسيع، ولكن مغلقة للتعديل. هذا يعني أنه يجب أن يكون من الممكن إضافة وظائف جديدة إلى فئة أو وحدة دون تغيير كودها الحالي.
ضع في اعتبارك فئة UserService من المثال السابق. لنفترض أننا نريد إضافة ميزة جديدة إلى فئة UserService تتيح لنا البحث عن المستخدمين بواسطة عنوان البريد الإلكتروني. إحدى الطرق للقيام بذلك هي إضافة طريقة جديدة إلى فئة UserService كما هو موضح في الكود أدناه:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
class UserService { fun createUser(user: User) { // كود لإنشاء مستخدم في قاعدة البيانات } fun deleteUser(user: User) { // كود لحذف مستخدم من قاعدة البيانات } fun updateUser(user: User) { // كود لتحديث مستخدم في قاعدة البيانات } fun getUser(id: Int): User { // كود لاسترداد مستخدم من قاعدة البيانات } fun searchUsersByEmail(email: String): List<User> { // كود للبحث عن المستخدمين بواسطة عنوان البريد الإلكتروني } } |
هذا النهج يعمل، ولكنه ينتهك مبدأ المفتوح المغلق، حيث كان علينا تعديل فئة UserService الحالية من أجل إضافة الميزة الجديدة. النهج الأفضل هو استخدام الوراثة أو التركيب لتوسيع وظائف فئة UserService دون تعديل كودها.
لتحقيق ذلك، يمكننا إنشاء فئة جديدة تسمى UserSearchService توسع فئة UserService وتضيف وظيفة البحث بالبريد الإلكتروني:
|
1 2 3 4 5 6 7 8 9 |
class UserSearchService(private val userService: UserService) : UserService() { fun searchUsersByEmail(email: String): List<User> { // كود للبحث عن المستخدمين بواسطة عنوان البريد الإلكتروني } } |
في هذا المثال، فئة UserSearchService مفتوحة للتوسيع، لأنها توفر وظائف إضافية تتجاوز ما تقدمه فئة UserService . وفي الوقت نفسه، تظل فئة UserService مغلقة أمام التعديل، حيث لم نكن بحاجة إلى تعديل الكود الخاص بها من أجل إضافة ميزة البحث عن البريد الإلكتروني.
مبدأ ليسكوف للاستبدال
مبدأ ليسكوف للاستبدال (LSP) هو مبدأ في تصميم البرمجيات ينص على أنه يجب أن يكون من الممكن استبدال كائنات الفئة الأساسية بكائنات الفئة الفرعية دون التأثير على صحة البرنامج. وهذا يعني أن الفئة الفرعية يجب أن تكون بديلاً صالحًا لفئتها الأساسية، ويجب أن تتصرف بنفس الطريقة التي تتصرف بها الفئة الأساسية عند استخدامها في نفس السياق.
سنواصل العرض التوضيحي باستخدام User و UserService من الأمثلة السابقة. للسماح بتوسيع فئة User، نستخدم الكلمة المفتاحية open في لغة Kotlin:
|
1 2 3 4 5 6 7 8 9 |
open class User( val firstName: String, val lastName: String, val email: String ) |
إليك فئة UserService الأصلية التي تستخدم فئة البيانات User أعلاه:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
class UserService { fun createUser(user: User) { // كود لإنشاء مستخدم في قاعدة البيانات } fun deleteUser(user: User) { // كود لحذف مستخدم من قاعدة البيانات } fun updateUser(user: User) { // كود لتحديث مستخدم في قاعدة البيانات } fun getUser(id: Int): User { // كود لاسترداد مستخدم من قاعدة البيانات } } |
نفترض أننا نريد إنشاء فئة فرعية من User تسمى AdminUser تمثل المستخدمين ذوي الصلاحيات الإدارية. يمكننا القيام بذلك على النحو التالي:
|
1 2 3 4 5 6 7 8 9 |
class AdminUser( firstName: String, lastName: String, email: String, ) : User(firstName, lastName, email) |
في هذا المثال، تعد فئة AdminUser بديلاً صالحًا لفئة User، حيث تتصرف بنفس الطريقة التي تتصرف بها فئة User ويمكن استخدامها أينما كان من المتوقع وجود كائن User . على سبيل المثال، يمكننا استخدام فئة AdminUser مع فئة UserService على النحو التالي:
|
1 2 3 4 5 6 7 8 9 |
fun main() { val userService = UserService() val adminUser = AdminUser("John", "Doe", "john.doe@example.com") userService.createUser(adminUser) } |
هذا الكود صحيح، حيث أن فئة AdminUser تعد بديلاً صالحًا لفئة User ويمكن استخدامها بنفس الطريقة التي يُستخدم بها كائن User.
من المهم ملاحظة أن مبدأ ليسكوف للاستبدال يتعلق بما هو أكثر من مجرد الوراثة. يتعلق الأمر بضمان أن تتصرف كائنات الفئة الفرعية بنفس طريقة كائنات الفئة الأساسية، بغض النظر عن كيفية تنفيذ الفئة الفرعية. على سبيل المثال، إذا كانت فئة AdminUser تتصرف بشكل مختلف عن فئة User بطريقة ما، فإن ذلك سيشكل انتهاكًا لمبدأ ليسكوف للاستبدال، حيث لن تكون بديلاً صالحًا لفئة User .
مبدأ فصل الواجهات
مبدأ فصل الواجهات (ISP) هو مبدأ في تصميم البرمجيات ينص على أنه لا ينبغي إجبار العملاء على الاعتماد على واجهات لا يستخدمونها. وهذا يعني أنه من الجيد عمومًا إنشاء واجهات صغيرة ومحددة تؤدي شيئًا واحدًا بشكل جيد، بدلاً من إنشاء واجهات كبيرة عامة الأغراض تحاول القيام بأشياء كثيرة.
إليك مثال على كيفية تطبيق هذا المبدأ عن طريق إعادة كتابة فئتي User و UserService من الأمثلة السابقة:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
interface UserService { fun createUser(user: User) fun deleteUser(user: User) fun updateUser(user: User) fun getUser(id: Int): User } class DatabaseUserService : UserService { override fun createUser(user: User) { // كود لإنشاء مستخدم في قاعدة البيانات } override fun deleteUser(user: User) { // كود لحذف مستخدم من قاعدة البيانات } override fun updateUser(user: User) { // كود لتحديث مستخدم في قاعدة البيانات } override fun getUser(id: Int): User { // كود لاسترداد مستخدم من قاعدة البيانات } } |
في هذا المثال، الـ UserService تُعرّف أربع دوال تتعلق بإدارة المستخدمين في قاعدة البيانات. الـ DatabaseUserService يُنفّذ هذه الواجهة ويُقدّم تطبيقات ملموسة لهذه الدوال.
افترض أننا نريد إضافة ميزة جديدة إلى UserService والتي تتيح لنا البحث عن المستخدمين بواسطة عنوان البريد الإلكتروني. إحدى الطرق للقيام بذلك هي إضافة دالة جديدة إلى UserService :
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
interface UserService { fun createUser(user: User) fun deleteUser(user: User) fun updateUser(user: User) fun getUser(id: Int): User fun searchUsersByEmail(email: String): List<User> } |
لن يعمل الكود الخاص بك، ما لم تقم أيضًا بتنفيذ هذه الدالة في جميع الكلاسات التي تُنفّذ الـ UserService :
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
class DatabaseUserService : UserService { override fun createUser(user: User) { // كود لإنشاء مستخدم في قاعدة البيانات } override fun deleteUser(user: User) { // كود لحذف مستخدم من قاعدة البيانات } override fun updateUser(user: User) { // كود لتحديث مستخدم في قاعدة البيانات } override fun getUser(id: Int): User { // كود لاسترداد مستخدم من قاعدة البيانات } override fun searchUsersByEmail(email: String): List<User> { // كود للبحث عن مستخدمين بواسطة عنوان البريد الإلكتروني } } |
على الرغم من أن هذا النهج يعمل، إلا أنه ينتهك مبدأ فصل الواجهات (Interface Segregation Principle)، لأنه يجبر الـ DatabaseUserService على تنفيذ دالة ( searchUsersByEmail ) قد لا يحتاجها أو يستخدمها.
النهج الأفضل هو إنشاء واجهة منفصلة لوظيفة البحث بالبريد الإلكتروني:
|
1 2 3 4 5 |
interface UserSearchService { fun searchUsersByEmail(email: String): List<User> } |
الآن لدينا واجهات منفصلة وصغيرة ومحددة التركيز، أي UserService و UserSearchServiceالتي تملك مسؤولية واحدة. يمكن للفئة التي تتطلب جميع وظائف هذه الواجهات أن تقوم بتنفيذها كما هو موضح في مقتطف الكود أدناه:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
class DatabaseUserService : UserService, UserSearchService { override fun createUser(user: User) { // كود لإنشاء مستخدم في قاعدة البيانات } override fun deleteUser(user: User) { // كود لحذف مستخدم من قاعدة البيانات } override fun updateUser(user: User) { // كود لتحديث مستخدم في قاعدة البيانات } override fun getUser(id: Int): User { // كود لاسترداد مستخدم من قاعدة البيانات } override fun searchUsersByEmail(email: String): List<User> { // كود للبحث عن المستخدمين بواسطة عنوان البريد الإلكتروني } } |
يلتزم هذا بمبدأ فصل الواجهات (Interface Segregation Principle)، لأنه يضمن أن العملاء (مثل الفئة DatabaseUserService) ليسوا مجبرين على الاعتماد على واجهات لا يستخدمونها.
لفهم هذا المفهوم بشكل أفضل، لنفترض أن لدينا فئة أخرى تسمى MemoryUserService تقوم بتنفيذ الواجهة UserService ولكنها لا تحتاج إلى وظيفة البحث بالبريد الإلكتروني، يمكننا كتابة الكود على النحو التالي:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
class MemoryUserService : UserService { private val users = mutableListOf<User>() override fun createUser(user: User) { users.add(user) } override fun deleteUser(user: User) { users.remove(user) } override fun updateUser(user: User) { val index = users.indexOf(user) if (index >= 0) { users[index] = user } } override fun getUser(id: Int): User { return users[id] } } |
في هذا المثال، الفئة MemoryUserService تحتاج فقط إلى تنفيذ الدوال المعرفة في الواجهة UserService، ولا داعي للقلق بشأن وظيفة البحث بالبريد الإلكتروني. يتيح هذا للفئة MemoryUserService التركيز على مسؤوليتها الأساسية المتمثلة في إدارة المستخدمين في الذاكرة، بدلاً من إجبارها على تنفيذ وظائف غير ذات صلة.
مبدأ عكس الاعتمادية
مبدأ عكس الاعتمادية (DIP) هو مبدأ من مبادئ تصميم البرمجيات ينص على أن الوحدات عالية المستوى يجب ألا تعتمد على الوحدات منخفضة المستوى، بل يجب أن يعتمد كلاهما على التجريدات. هذا يعني أنه من الجيد عمومًا تصميم برمجياتك بطريقة لا تجعل المكونات عالية المستوى مرتبطة بتنفيذات محددة للمكونات منخفضة المستوى، بل تعتمد بدلاً من ذلك على التجريدات (مثل الواجهات أو الفئات المجردة) التي يمكن تنفيذها بطرق مختلفة.
دعونا نلقي نظرة على مثال لكيفية تطبيق هذا المبدأ على الفئتين User و UserService المستخدمتين في مقتطفات الكود السابقة:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
interface UserRepository { fun create(user: User) fun delete(user: User) fun update(user: User) fun get(id: Int): User } class UserService(private val repository: UserRepository) { fun createUser(user: User) { repository.create(user) } fun deleteUser(user: User) { repository.delete(user) } fun updateUser(user: User) { repository.update(user) } fun getUser(id: Int): User { return repository.get(id) } } |
في هذا المثال، تعتمد فئة UserService على تجريد يسمى واجهة UserRepository، بدلاً من الاعتماد على تطبيق محدد لمستودع المستخدمين. يتيح لنا هذا تطبيق واجهة UserRepository بطرق مختلفة، مثل استخدام قاعدة بيانات أو تخزين في الذاكرة، دون التأثير على فئة UserService.
على سبيل المثال، إليك تطبيق لواجهة UserRepository التي تستخدم قاعدة بيانات:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
class DatabaseUserRepository: UserRepository { override fun create(user: User) { // كود لإنشاء مستخدم في قاعدة البيانات } override fun delete(user: User) { // كود لحذف مستخدم من قاعدة البيانات } override fun update(user: User) { // كود لتحديث مستخدم في قاعدة البيانات } override fun get(id: Int): User { // كود لاسترداد مستخدم من قاعدة البيانات } } |
إليك تطبيق آخر لواجهة UserRepository التي تستخدم التخزين في الذاكرة:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
class MemoryUserRepository : UserRepository { private val users = mutableListOf<User>() override fun create(user: User) { users.add(user) } override fun delete(user: User) { users.remove(user) } override fun update(user: User) { val index = users.indexOf(user) if (index >= 0) { users[index] = user } } override fun get(id: Int): User { return users[id] } } |
هذا يجعل النظام أكثر مرونة وقابلية للصيانة، لأنه يتيح لنا تغيير تطبيق المستودع دون التأثير على بقية النظام. كما أنه يسهل اختبار فئة UserService، حيث يمكننا محاكاة تبعية UserRepository في اختباراتنا.
الخاتمة
في هذه المقالة، تحدثنا عن المبادئ الخمسة لـ SOLID الكود ومقتطفات الكود المشتركة التي تلبي كل مبدأ. يمكن أن يساعدك الالتزام بهذه المبادئ في تصميم أنظمة برمجية أكثر مرونة وقابلية للصيانة والتطوير. ومع ذلك، من المهم أن تضع في اعتبارك أن هذه المبادئ هي إرشادات وليست قواعد صارمة، والأمر متروك للمطور ليقرر متى وكيف يطبقها في سياق مشروعه المحدد. واصل تعلمك من خلال مراجعة مدونتنا للحصول على مقالات أكثر عمقًا وحداثة ودروس تعليمية حول الحوسبة السحابية و DevOps، تصميم البرمجيات وتطويرها، واتجاهات التكنولوجيا التي يجب ترقبها، والمزيد.
برمجة سعيدة!
التعليقات
لا توجد تعليقات بعد. كن أول من يعلق.