SOLID Principles 101 - Single Responsibility

الـ SOLID Principles عبارة عن مجموعة من القواعد البسيطة بتساعد المبرمجين على كتابة كود نظيف ومنظم وسهل الفهم والتعديل. تخيل كأنك بتبني بيت، لازم يكون كل جزء فيه له وظيفة واضحة ومكان محدد عشان البيت يبقى قوي ومستقر.
SOLID Principles 101 - Single Responsibility
SOLID Principles 101 - Single Responsibility

في هذه الصفحة

المقدمة

الـ SOLID Principles عبارة عن مجموعة من القواعد البسيطة بتساعد المبرمجين على كتابة كود نظيف ومنظم وسهل الفهم والتعديل. تخيل كأنك بتبني بيت، لازم يكون كل جزء فيه له وظيفة واضحة ومكان محدد عشان البيت يبقى قوي ومستقر.

نفس الكلام في البرمجة، الكود لازم يكون منظّم عشان يسهل تطويره وصيانته!


 ليه المبادئ دي مهمة؟ 

  1. كود نظيف ومنظم: بيسهل فهمه وتعديله 
  2. صيانة أسهل: لو فيه أي مشكلة، هتلاقيها بسرعة وتصلحها 
  3. توسعة أسرع: تقدر تضيف ميزات جديدة بسهولة 
  4. تعاون أفضل بين المبرمجين: كل واحد هيفهم شغله كويس 
  5. كفاءة أعلى: الكود هيشتغل بشكل أسرع وأكثر استقرارًا 
💡
 باختصار هي أدوات أساسية للمبرمج المحترف عشان يقدر يبني سوفت وير مرن و سهل التعديل عليه في المستقبل!
82ea0602a13c7ae723f4fabb4c971b46.png

مبدأ الـ Single Responsibility

💡
A class should have only one reason to change, or in other words, it should have a single responsibility.

هنبدأ ب رسمة بتوضح الكلام ده  

03cb134ab92f864467406825e8fff19d.png
Single Responsibility

الصورة بتقولنا قصة بسيطة عن الروبوتات والشغل: 

في الجانب الأيسر: فيه روبوت واحد بس، شايل كل المسؤوليات!

هو شيف بيطبخ، وبستاني بيزرع، ورسام بيرسم، وسواق بيودي ويجيب. يعني شغال في كل حاجة! بس المشكلة إن الروبوت ده هيبوظ بسرعة وهيتعب، و ممكن كمان يغلط في شغله.

تخيل لو انت طلبت منه يعمل لك أكلة، وهو في نفس الوقت بيرسم على الحيطة وبيسوق العربية! أكيد الأكلة مش هتبقى طعمها حلو والرسمة هتبقى وحشة والعربية هتخبط!

مشكلة كمان: تخيل معايا الروبوت فيه حاجة باظت كده كل المهام هتبوظ لأن هو بيعمل كل المهام فا لو مهمة باظت كل المهام هتبوظ!


الجانب اليمين: هنا عندنا أكتر من روبوت، كل واحد بيشتغل في حاجة واحدة بس.

فيه روبوت شيف بيطبخ بس، وروبوت بستاني بيزرع بس، وروبوت رسام بيرسم بس، وروبوت سواق بيسوق بس. كده كل واحد بيعرف يعمل شغله كويس ومش بيتشتت. لو طلبت من الروبوت الشيف يعمل لك أكلة، هيعملها أحسن طعم، لأن ده شغله الوحيد.

طيب تخيل معايا: فيه روبوت منهم باظ هل الباقي هيتأثر ؟ اكيد لأ  

إيه العبرة من الصورة دي؟ 

الصورة دي بتوضح لنا مبدأ مهم في البرمجة اسمه "مبدأ المسؤولية الواحدة".

المبدأ ده بيقول إن كل جزء من البرنامج (زي الروبوت في الصورة) لازم يكون مسؤول عن عمل واحد بس. لو كل جزء عمل أكتر من حاجة، البرنامج هيبقى معقد وصعب نفهمه ونصلحه لو فيه أي مشكلة. 

يعني بالبلدي كده: كل واحد في الشغل لازم يعرف يعمل حاجة واحدة كويس، مش يتشتت في كل حاجة. كده الشغل هيخلص بسرعة وهيبقى مظبوط. 

نبدأ بشوية امثلة الامثلة عبارة عن صورتين صورة كود سيء و صورة بنحاول نطبق المبدأ الاول SRP بسم الله


Simple Age Calculator Example

class Person {
  String name;
  int birthYear;

  Person(this.name, this.birthYear);

  int calculateAge() {
    return DateTime.now().year - birthYear;
  }
}

 في المثال ده، تقوم فئة Person بمهمتين (مسؤليتين):

  1. تمثيل شخص: تخزين بيانات الشخص مثل الاسم وسنة الميلاد.
  2. حساب العمر: تحتوي على دالة calculateAge لحساب عمر الشخص.

طب ليه ده يعتبر انتهاكًا لـ SRP؟

مسؤولية واحدة: يجب أن تكون لكل فئة مسؤولية واحدة فقط. هنا، فئة Person مسؤولة عن تمثيل البيانات وحساب العمر.

قابلية التغيير: إذا أردنا تغيير طريقة حساب العمر أو إضافة خصائص جديدة للشخص، فسيتعين علينا تعديل فئة Person، مما قد يؤثر على أجزاء أخرى من الكود.

الكود بعد

class Person {
  String name;
  int birthYear;
  Person(this.name, this.birthYear);
}
class AgeCalculator {
  static int calculateAge(int birthYear) {
    return DateTime.now().year - birthYear;
  }
}

في هذا المثال، تم فصل المسؤوليات:

  1. فئة Person: تمثل فقط بيانات الشخص.
  2. فئة AgeCalculator: مسؤولة فقط عن حساب العمر بناءً على سنة ميلاد الشخص.

لماذا هذا يعتبر تطبيقًا جيدًا لـ SRP؟

مسؤولية واحدة: كل فئة تقوم بمهمة واحدة محددة.

قابلية التغيير: إذا أردنا تغيير طريقة حساب العمر، يمكننا تعديل فئة AgeCalculator فقط دون التأثير على فئة Person.

إعادة الاستخدام: يمكن استخدام فئة AgeCalculator لحساب عمر أي شخص آخر دون الحاجة إلى تكرار الكود.


Payment Processing Example

class PaymentProcessor {
  void charge(num amount) {
    // Initialize bank data
    // Send request to the bank
  }

  String createReport() {
    // Create and format a report
    return " '';
  }

  void printReport() {
    // Send a printing command
  }
  void savePayment() {
    // Saving to DB
  }
}

في المثال ده، تقوم فئة PaymentProcessor بمهام متعددة:

  1. معالجة الدفع: تنفيذ عملية الدفع.
  2. إنشاء تقرير: توليد تقرير عن عملية الدفع.
  3. طباعة التقرير: إرسال أمر طباعة للتقرير.
  4. حفظ الدفع: حفظ بيانات الدفع في قاعدة البيانات.

لماذا هذا يعتبر انتهاكًا لـ SRP؟

مسؤوليات متعددة: فئة واحدة تقوم بمهام متعددة.

صعوبة التعديل: إذا أردنا تغيير طريقة إنشاء التقرير أو الطباعة، فسيتعين علينا تعديل فئة PaymentProcessor بأكملها.

صعوبة الاختبار: يصعب اختبار كل وظيفة بشكل مستقل.


تقدروا دلوقتي تشتركوا في النشرة الأسبوعية لاقرأ-تِك بشكل مجاني تمامًا عشان يجيلكوا كل جديد بشكل أسبوعي فيما يخص مواضيع متنوعة وبشروحات بسيطة وسهلة وبجودة عالية 🚀

النشرة هيكون ليها شكل جديد ومختلف عن شكلها القديم وهنحاول انها تكون مميزة ومختلفة وخليط بين المحتوى الأساسي اللي بينزل ومفاجآت تانية كتير 🎉

Eqraatech Newsletter | Eqraatech - اقرأ-تِك | Substack
محتوى تقني متميز في مختلف مجالات هندسة البرمجيات باللغة العربية عن طريق تبسيط المفاهيم البرمجية المعقدة بشكل سلس وباستخدام صور توضيحية مذهلة. Click to read Eqraatech Newsletter, a Substack publication with hundreds of subscribers.

بفضل الله قمنا بإطلاق قناة اقرأ-تِك على التليجرام مجانًا للجميع 🚀

آملين بده اننا نفتح باب تاني لتحقيق رؤيتنا نحو إثراء المحتوى التقني باللغة العربية ، ومساعدة لكل متابعينا في انهم يوصلوا لجميع أخبار اقرأ-تِك من حيث المقالات ومحتوى ورقة وقلم والنشرة الأسبوعية وكل جديد بطريقة سريعة وسهلة

مستنينكوا تنورونا , وده رابط القناة 👇

https://t.me/eqraatechcom


class PaymentProcessor {
  void charge(num amount) {
    // Initialize bank data
    // Send request to the bank
  }
}
class ReportGenerator {
  String createReport(PaymentProcessor paymentProcessor) {
    // Create and format a report based on the payment processor's data
    return '';
  }
}
class ReportPrinter {
  void printReport(String report) {
    // Send a printing command for the given report
  }
}
class PaymentSaver {
  void savePayment(PaymentProcessor paymentProcessor) {
    // Save the payment processor's data to the database
  }
}

في هذا المثال، تم فصل المسؤوليات إلى فئات متخصصة:

  1. الـ PaymentProcessor: مسؤولة فقط عن معالجة الدفع.
  2. الـ ReportGenerator: مسؤولة فقط عن إنشاء التقرير.
  3. الـ ReportPrinter: مسؤولة فقط عن طباعة التقرير.
  4. الـ PaymentSaver: مسؤولة فقط عن حفظ بيانات الدفع.

لماذا هذا يعتبر تطبيقًا جيدًا لـ SRP؟

مسؤولية واحدة: كل فئة تقوم بمهمة محددة.

سهولة التعديل: يمكن تعديل كل فئة بشكل مستقل دون التأثير على الفئات الأخرى.

سهولة الاختبار: يمكن اختبار كل فئة بشكل منفصل.

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

هل PaymentSaver من ضمن مسؤوليات  PaymentProcessor  والمفروض تكون ضمن class PaymentProcessor

لا، PaymentSaver ليست جزءًا من PaymentProcessor في المثال الذي قدمته.

لتوضيح ذلك بشكل أوضح، دعنا نعود إلى الشرح السابق:

  1. الـ PaymentProcessor: هذه الفئة مسؤولة بشكل كامل عن عملية الدفع نفسها، بدءًا من إعداد بيانات البنك وحتى إرسال الطلب إليه. وظيفتها الأساسية هي تنفيذ المعاملة المالية.
  2. الـ PaymentSaver: هذه الفئة مسؤولة عن حفظ سجل لهذه المعاملة في قاعدة البيانات. وظيفتها هي تخزين البيانات المتعلقة بالدفع للمراجعات المستقبلية أو لأغراض أخرى.

لماذا تم فصلهما؟

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

مرونة: إذا أردنا تغيير طريقة حفظ البيانات، يمكننا تعديل فئة PaymentSaver فقط دون التأثير على عملية الدفع نفسها.

إعادة الاستخدام: يمكن استخدام فئة PaymentSaver لحفظ أنواع أخرى من البيانات، وليس فقط بيانات الدفع.

مثال توضيحي: تخيل أنك تدفع فاتورة كهرباء عبر الإنترنت.

الـ PaymentProcessor: هي النظام الذي يتعامل مع البنك لتحويل المبلغ من حسابك إلى حساب شركة الكهرباء.

الـ PaymentSaver: هي النظام الذي يسجل هذه المعاملة في سجل الدفع الخاص بك، بحيث يمكنك الرجوع إليها لاحقًا. كما ترى، كلا النظامين يقومان بمهام مختلفة ومتكاملة، ولكنهما مستقلان عن بعضهما البعض.

لماذا هذا مهم؟

وضوح الكود: كل جزء من الكود مسؤول عن شيء واحد فقط، مما يسهل فهمه وتعديله.

صيانة أسهل: إذا حدث خطأ في جزء معين من الكود، يمكنك تصحيحه دون التأثير على الأجزاء الأخرى.

قابلية التوسعة: يمكنك إضافة ميزات جديدة بسهولة، مثل إرسال إشعار بالبريد الإلكتروني بعد إتمام الدفع، دون الحاجة إلى تعديل كل الكود فباختصار:

💡
الـ PaymentSaver ليست جزءًا من PaymentProcessor لأن كل منهما لديه مسؤولية مختلفة ومستقلة. هذا التقسيم للمسؤوليات هو تطبيق لمبدأ المسؤولية الواحدة (SRP)، وهو مبدأ أساسي في البرمجة الموجهة للكائنات!

Quiz App Example

class QuizManager {
  final FirebaseFirestore firestore = FirebaseFirestore.instance;

  // Method to add a new question
  Future<void> addQuestion(String quizId, String question, List<String> choices, String answer) async {
    await firestore.collection('quizzes').doc(quizId).collection('questions').add({
      'question': question,
      'choices': choices,
      'answer': answer,
    });
  }

  // Method to validate question data
  bool validateQuestion(String question, List<String> choices, String answer) {
    if (question.isEmpty || choices.length < 2 || !choices.contains(answer)) {
      return false;
    }
    return true;
  }

  // Method to fetch all questions for a quiz
  Future<List<Map<String, dynamic>>> fetchQuestions(String quizId) async {
    QuerySnapshot snapshot = await firestore.collection('quizzes').doc(quizId).collection('questions').get();
    return snapshot.docs.map((doc) => doc.data() as Map<String, dynamic>).toList();
  }
}

الكود بعد التعديل 

class QuestionValidator {
  // Method to validate question data
  bool validate(String question, List<String> choices, String answer) {
    if (question.isEmpty || choices.length < 2 || !choices.contains(answer)) {
      return false;
    }
    return true;
  }
}
class FirestoreQuestionService {
  final FirebaseFirestore firestore = FirebaseFirestore.instance;

  // Method to add a new question
  Future<void> addQuestion(String quizId, String question, List<String> choices, String answer) async {
    await firestore.collection('quizzes').doc(quizId).collection('questions').add({
      'question': question,
      'choices': choices,
      'answer': answer,
    });
  }

  // Method to fetch all questions for a quiz
  Future<List<Map<String, dynamic>>> fetchQuestions(String quizId) async {
    QuerySnapshot snapshot = await firestore.collection('quizzes').doc(quizId).collection('questions').get();
    return snapshot.docs.map((doc) => doc.data() as Map<String, dynamic>).toList();
  }
}

تحسينات الكود الجديد:

  1. فصل المسؤوليات: الآن لدينا فئة QuestionValidator للتحقق من صحة بيانات الأسئلة وفئة FirestoreQuestionService للتعامل مع قاعدة البيانات.
  2. أوضح وأسهل للصيانة: أصبح الكود أكثر تنظيماً وسهولة في الفهم، حيث يركز كل قسم على مهمة محددة.
  3. إعادة الاستخدام: يمكن إعادة استخدام فئة QuestionValidator في أجزاء أخرى من التطبيق للتحقق من صحة البيانات. 

في الختام

بكده نكون شوفنا مع بعض أهمية مبدأ الـ Single Responsibility في فصل المسؤؤليات وكونه أوضح وأسهل للصيانة والتعديل وكمان لإعادة الاستخدام وشوفنا ده من خلال أكتر من مثال مختلف .

ده كان أول مبدأ من الـ SOLID Principles ولسه فيه مبادئ تانية هنتكلم عنها باذن الله.

اشترك الآن بنشرة اقرأ-تِك الأسبوعية

لا تدع أي شيء يفوتك. واحصل على أحدث المقالات المميزة مباشرة إلى بريدك الإلكتروني وبشكل مجاني!