غاية كل مطور برمجيات هو ان يكون ضمن الصفوة في مجاله و هذا لن يكون الا بالعمل الدؤوب كتعلم تقنيات جديدة. او قراءة بعض الكتب و الأهم من كل هذا كيفية كتابة كود متكامل ذو آداء جيد قابل للقراءة و التعديل و الاختبار، مما يجعلك راضيا عن الكود الذي كتبته و سعيدا بانجازك، بالمقابل كتابة كود عشوائي سيجعل من تعديله او اختباره كالجحيم، وبما انني احب ان اراك سعيدا فدعونا ندردش عن مبادئ تصميم البرمجيات SOLID .
قبل ان نبدا ضع هذه الامور في الحسبان :
- يجب ان تكون محيطا بمفاهيم OOP في الجافا لفهم المقالة.
- الاكواد مأخودة من مصادر مختلفة مذكورة في الفقرة الاخيرة .
- اربع امثلة لها علاقة بحساب المساحة في الرياضيات لأنها توضح هذه المفاهيم بشكل جيد.
- و اخيرا لا تأخد النكات المذكورة على محمل الجد مهما كانت عبيطة.
ما هو SOLID ؟
ربما سمعت عن كتاب Clean code، أو عن كاتبه uncle bob (العم بوب)، إن لم تسمع به فلا مشكلة، لكن سيكون من الجيد الإطلاع على هذا الكتاب الذي سيقودك الى كتابة كود يستحق ان يسمى كود بدلا من كتابة اسطر من الخردة.
صاحبنا او العم بوب قام بجمع 5 مبادئ لتصميم البرمجيات و صادف ان أخد اول حرف من كل مبدا يعطينا اسم SOLID
- Single responsibility principle.
- Open-closed principle
- Liskov substitution principle
- Interface segregation principle
- Dependency Inversion Principle
و إن كنت تتسائل عن الفائدة منها فهو كتابة كود جيد و نظيف، و هو الهدف من هذا المقال تعريفك بالفائدة من مبادئ الـ SOLID.
كود سهل الفهم
ليس كل كود برمجي يفهمه الحاسوب فان المبرمج بدوره يجب ان يفهمه، بل أنت بنفسك قد لا تعرف معناه و الهدف منه إلا بعد شهر من كتابته ، ربما ستحتاج الى وسيط روحي 😊 ليساعدك في إستيعابه، آنذاك قد تود كتابة الكود من جديد من أجل تحسينه او تطويره او فقط من أجل جعله أكثر منطقية، و هذا بالتأكيد هذا سيكون مؤلما قليلا نظرا لعدم إستيعابك لخطوات الكود سابقه، و لهذا فان هذه المبادئ ستسهل عليك حياتك.
كود سهل التعديل
منطقيا، على الكود الذي يسهل فهمه في اغلب الحالات أن يكون سهل التعديل أيضا لأنه و كما ذكرنا سابقا الكود الذي لا تستطيع فهمه لا يمكنك التعديل عليه. الأمر لا يحتاج ان تقلق كثيرا حول جعل الكود قابل للتعديل، كل ما في الامر هو اتباع مجموعة من القواعد و التي من ضمنها SOLID DP، فمن بين الأهداف الرئيسية للـ SOLID توفير إمكانية صناعة كود برمجي قابل للتعديل في أي وقت.
كود سهل الاختبار
سيجيئ لاحقا الفترة التي سيتوجب عليك إختبار الكود الخاص بك إن كان يشتغل في إطار عمله دون التأثير على كود برمجي آخر سواء في مرحلة التطوير او التعديل، يتم ذلك عبر مجموعة من الـ Unit Tests التي يتم تطبيقها على الكود البرمجي لمعرفة النتيجة، فإما تكون نتيجة صالحة و مفهومة، و إما نتيجة سيئة تؤكد لك حقا ان الكود الذي قمت بكتابته هو خردة لا يجب لمسه او التعديل عليه، و التي تعتبر القاعدة الأولى في البرمجة للمبتدئين.
المبدأ رقم 1 : مبدأ المسؤولية الواحدة
هذا المبدأ من كثرة بساطته يبدوا معقدا و سأشرح لك لماذا: ينص هذا المبدأ على ان كل جزء من الكود يجب ان تكون لديه مهمة واحدة للقيام بها، لكن لا يمكننا فقط تنفيذ الأمر بتلك السهولة، يوجد جوارح جانبية علينا مراعاتها :
- المعضلة :
مثلا لنفترض ان لديك دالة اسمها SaveAndPrint() حيث تقوم هذه الدالة بحفظ الفاتورة في قاعدة البيانات و طباعتها للزبون، و هي تعمل بشكل جيد بعد ان تجربتها في احدى المطاعم لكنها ضد مبدأ المسؤولية الواحدة SRP إختصاراً لـ Single Responsibility Principle، أي ان الدالة تقوم بمهمتين في نفس الوقت، و الأصح سيكون فصل الدالتين الى Save و Print.
- التسائل :
كيف لك ان تعرف هذه المسؤولية الواحدة ؟ اعني فكر جيدا من الصعب معرفة إن كان هذا الكود البرمجي حقا يقوم بمسؤولية واحدة، الإجابة ببساطة حسب العم بوب هو ان يكون لدى الدالة أو الكلاس سبب واحد للتغير و ان كنت مصرا على فهم اكثر للموضوع انصحك بهذا الفيديو من العم بوب ، و إن لم يكن لديك الوقت لمشاهدة الفيديو فالأمر ببساطة ان الكلاس او الدالة تكون في حالتها المستقرة ثم يجب يتم تحريكها (Triggered) لسبب واحد فقط، بالعودة لدالة SaveAndPrint() سنجد ان هذه الدالة تتحرك لسببين الأول هو الـ Save و الثاني هو الـ Print، اما إن قمنا بفصلهما، فدالة Save() ستتغير حالتها فقط في حالة إرادتك الحفظ و الحال لا يختلف لدالة Print().
- مثال :
لنفهم الأمر بشكل برمجي أفضل، دعونا نفترض ان لدينا كلاسين كل واحد يمثل شكل هندسي دائرة و مربع، الأول يحمل شعاع الدائرة و الثاني يحمل طول المربع .
ساقوم بانشاء كلاس اخر يسمى AreaCalculator و مهمته حساب مجموع المساحات لمصفوفة من الأشكال الهندسية السابقة الذكر و طباعتها .
هذا ضرب لمبدأ SRP عرض الحائط، فهذا الكلاس المسكين يقوم بمهمتين و ذلك يصعب مهمة تعديل الكود لاحقا، لكن ما السيء بالامر افترض عزيزي القارئ انك ترغب في اضافة تعدد outputs كي يستطيع المستخدم استخراج مجموع المساحات بعدة انواع Json او HTML او PlainText .
بدلا من الحل السابق من الممكن ان نقوم بانشاء كلاس اخر يحمل اسم SumCalculatorOutputter يقوم بتلك المهمة و من الممكن القيام بعدة طرق تختلف حسب كل حالة .
النتيجة ستكون كالتالي :
حتى لا تتعقد الأمور لديك، فقط تذكر ان أي كلاس او دالة (Function) تقوم ببرمجتها يجب ان تكون لديه مهمة واحدة او حالة واحدة يتم تنشيطه فيها، و ذلك من أجل تسهيل عملية تعديله لاحقا إن تطلب الأمر و إبقاء الأمر بسيطاً للغاية.
المبدأ رقم 2 : Open/Close :
هذا هو التعريف بالإنجليزية :
Objects or entities should be open for extension, but closed for modification.
خلاصة هذا التعريف ان الكائنات يجب ان تكون قابلة للتمديد لكن دون الحاجة إلى تعديل محتوى الكلاس بعبارة أخرى يجب ان يكون الكلاس مرن و قابلة لإضافة أي محتوى جديد دون الحاجة الى تعديل أي محتوى آخر موجود مسبقا.
في الكود السابق لم نقم بانشاء دالة sum، فدعونا نقوم بإضافتها في الكلاس :
تقوم هذه الدالة بحساب مجموع مساحة الاشكال الموجودة داخل المصفوفة Shapes. وبما ان طريقة حساب مساحة الدائرة مختلفة عن المربع فسنحتاج الى if/else . حيث تتحقق من نوع الشكل الهندسي اذا ما كان مربع او دائرة ثم نقوم بعمل Cast و نحسب القيم .
لكن ماذا لو أردنا في الإصدار الثاني اضافة شكل جديد و هو المستطيل بالتاكيد ستحتاج الى اضافة if/else اخرى في كلاس AreaCalculator . و هذا يتعارض مع المبدا الثاني OCP .
البديل هو اضافة دالة area() الى interface بحيث يكون الشكل الهندسي Shape مسؤولا عن حساب مساحته بنفسه .
و بعد ذلك سنحتاج الى استدعاء دالة area مهما كان الشكل الهندسي :
الان مهما اضفت من الاشكال الهندسية open for extension فالكلاس لن تحتاج الى تعديل الكلاس closed for modification .
تلاحظ ان باستعمال هذه المبادئ فالأمر يسهل قراءة الكود و يجعله سهل التعديل في نفس الوقت، و بالطبع مرن كما جاز الذكر سابقا.
المبدأ الثالث : تعويض ليسكوف (Liskov Substitution) :
او Liskov substitution principle اختصارا LSP. ويسمى كذلك نسبة الى شخص يدعى ليسكوف (و الذي لا يهم) و ينص المبدا على التالي :
Let q(x) be a property provable about objects of x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T.
هذا المبدأ هو واحد من اكثر المبادئ صعوبة في الشرح حيث يصعب شرحه لكن بمجرد ان ترى العديد من الأمثلة ربما يمكنك فهمه آنذاك و لهذا انصح بعدم الاكتفاء بهذه المقالة فقط، بل و تطوير نفسك أكثر و البحث و التعمق في مبدأ الـ SOLID بشكل كامل.
دعونا نعطي تعريفا ابسط لهذا المبدأ :
اذا كان S فرع تابعا لـ T . فان كائنات T يجب ان يتم استبدالها بكائنات S دون وقوع شيء غير مرغوب فيه في البرنامج
لنفترض ان والدك لاعب كرة، هذا يعني في أي مكان وضعناه يجب ان يستطيع لعب الكرة و بما انك فرع منه فيجب ان تستطيع لعب الكرة أيضا هذا بالطبع من وجهة نظر الوارثة في البرمجة. يمكننا القول أن الأمر نفسه بالنسبة لهذا المبدأ .
دعونا نأخذ مثالا لذلك، في الرياضيات كل مربع فهو مثلث و العكس غير صحيح لهذا سنقوم بانشاء كلاس Rectangle و يرث منه كلاس اخر نسميه Square
بما ان الطول يجب ان يساوي العرض في المربع فسنقوم بتغير كلا المتغيرين سواءً تم وضع الطول أو العرض :
بالتالي فان سلوك هذا الكلاس سيكون كالتالي :
كما نشاهد فالمربع لا يتصرف كمستطيل ببساطة لان المربع سيبقى مربع حتى لو عملنا CastUp لهذا لا يجب ان يكون على هذا هو الحل بل يجب ان يرث من كلاس Shape بدلا من ذلك .
و من ثم يرث كل من Square و Rectangle من هذا الكلاس، و بهذا نتفادى ان مستعمل الكود الخاص بك لن يحصل على سلوك غير مرغوب فيه .
المبدأ الرابع : Interface segregation
حسنا هذا مبدأ جميل و بسيط ينص على انه لا يجب إلزام كائن على اخد دالة لن يستخدمها أبدا :
A client should never be forced to implement an interface that it doesn't use or clients shouldn't be forced to depend on methods they do not use.
كما العادة هذه التعاريف قد تبقى دائما مبهمة حتى نستكشفها و نحللها بشكل أفضل عبر بضعة أمثلة، و سنتعامل مع الأشكال الهندسية مجددا، فهي أبسط حيلة الآن لفهم مبدأ الـ SOLID بشكل جيد :
طيب أشكال هندسية ،اومال ليه أنا عامل أشكال هندسية و هعمل أشكال هندسية هي ليا . و هو انتو فاكرين لما تتكلموا بالباطل هتخوفوني و لا ايه ؟ سيادة الزعيم المشير لو كان مبرمج
دعونا من النكت السياسية و لنفترض اننا نرغب في حساب الحجم و المساحة لكل شكل هندسي و لهذا عزيزي المبرمج ستقترح علي الحل التالي :
لكن المربع شكل ثنائي الابعاد لا يمكن حساب حجمه و انما يمكن حساب مساحته فقط لهذا اعتبره قطعة من الخردة (و لكنني انا من اقترحه ^_^ مررها يا صديقي لا تلقي اللوم علي). و لهذا يقول العم بوب بما ان المربع لن يحتاج لا اليوم و لا غدا دالة Volume فسنحتاج الى تقسيم هذا Interface .
فالمربع لن تكون لديه دالة من نوع Volume و التي لن يحتاجها في كل حال، هل بدأت تفهم فائدة هذه المبادئ ؟
إن لم تفهم فإليك شرح أكثر بساطة، بشكل عام لا تلزم أي كلاس بأي شيئ لن يستخدمه، لا حاجة لأن نضيف دوال ( حتى و إن كانت منطقية ) لأي كلاس نعلم جيدا انه لا يحجز سوى بضعة أسطر من الكود البرمجي، و أخذنا مثال المربع الذي نعلم انه واقعيا في الحياة ثلاثية الأبعاد انه يمكننا حساب حجمه، لكن برمجيا ذو مجسم ثنائي الأبعاد فنحن لسنا بحاجة الى حساب حجمه و لسنا بحاجة الى دالة لفعل ذلك لأننا لن نستخدمها من أساسه، إنه الـ Interface Segregation !
إن لم تفهم فإليك شرح أكثر بساطة، بشكل عام لا تلزم أي كلاس بأي شيئ لن يستخدمه، لا حاجة لأن نضيف دوال ( حتى و إن كانت منطقية ) لأي كلاس نعلم جيدا انه لا يحجز سوى بضعة أسطر من الكود البرمجي، و أخذنا مثال المربع الذي نعلم انه واقعيا في الحياة ثلاثية الأبعاد انه يمكننا حساب حجمه، لكن برمجيا ذو مجسم ثنائي الأبعاد فنحن لسنا بحاجة الى حساب حجمه و لسنا بحاجة الى دالة لفعل ذلك لأننا لن نستخدمها من أساسه، إنه الـ Interface Segregation !
المبدأ الخامس : Dependency Inversion principle
بالنسبة لي، هذا المبدأ المفضل لدي و يتطلب الحديث عنه مقالة خاصة به، لكن لا مشكلة يمكننا توضيحه بشكل بسيط في هذا المقال ، ينص المبدأ على التالي :
the high level module must not depend on the low level module, but they should depend on abstractions.
حسنا بما ان الأمثلة السابقة مبنية على الأشكال الهندسية، دعونا نغير القواعد قليلا في هذا المثال. يقول التعريف بان الكائنات يجب ان تعتمد على التجريد بدل من الاعتماد على بعضها البعض .
سنقوم بانشاء كلاس للسيارة Car و لنفترض ان السيارة تحتوي على محرك و عجلات. ستقترح كتابة هذ االكود من أجل صناعة هذه الكلاس و بكل صراحة لا أتحمل رؤية هذا الكود . لان حجم الألم الذي قد يسببه لك هائل خصوصا إذا كان مشروعك مشروعا لا أقول ضخم و إنما كبير .
حسب هذا المبدأ فمن الافضل القيام بذلك بطريقة افضل . حيث ان كلاس Car لا يهتم بنوع المحرك و العجلات و ما الى ذلك، ببساطة ما يقوله هذا الكلاس :
لا تعلمني كيف اقوم بانشاء المحرك و العجلات اعطني محركا و عجلات ايا كان نوعهما و ساعطيك سيارة جاهزة . - كلاس الـ Car
لانك إذا علمته كيفية إنشاء محرك و عجلات ببساطة ففي كل مرة سيقوم بإنشاء نفس المحرك و نفس العجلات و لن تستطيع تغيير ذلك.
حاولت جاهدا الا اجلب شيئا من كيسي و ذلك تفاديا لأي أخطاء أو مشاكل في الفهم، و حاولت أخذ مثال من مواقع مخصصة و فقط تفسيرها و شرحها لك في هذا المقال. و لهذا سأكون سعيدا بمناقشة أي خطا أو اقتراح معك في التعليقات .
بعض المصادر :
كاتب المقال : ISMAIL BELLA