المقدمة

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

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

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

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

Single Responsibility

SOLID Principles 101 - Open Close
تخيل إنك بتبني بيت ، البيت ده زي برنامج، والأجزاء بتاعته زي الغرف والحمامات والمطبخ. دلوقتي، لو عايز تزود غرفة جديدة، هتضيفها للبيت من غير ما تخرب الغرف اللي موجودة. بس مش هتروح تغير في شكل الغرف القديمة وتبوظ الديكور بتاعها، صح؟

Open Close


مبدأ الـ Liskov Substitution

💡
If S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program.
https://miro.medium.com/v2/resize:fit:875/1*2X9Af2QGC0LaXt206Z0exg.png

الصورة بتشرح مبدأ "Liskov Substitution Principle" بطريقة بسيطة باستخدام روبوتات. الفكرة بتقول إن لو عندك كلاس (فئة) وورثت منها كلاس تاني، المفروض الكلاس الجديد يقدر يحل مكان القديم من غير ما يغير في السلوك الأساسي.

Liskov Substitution

في أول جزء (اللي فوق):

  • الروبوت الكبير "سام" بيقول: "أنا سام، بعمل قهوة."
  • بعدها يظهر روبوت أصغر اسمه "إيدن" بيقول: "أنا إيدن، ابن سام."
  • حد بيطلب من "سام" إنه يعمل له قهوة، وسام بيديله القهوة، والراجل بيشكره.

في الجزء اللي تحت:

  • على الشمال، حد بيطلب من "إيدن" يعمل قهوة لأن "سام" مش موجود. "إيدن" بدل ما يعمل قهوة، بيديله ماء! فاللي طلب القهوة مستغرب ووشه مستغرب كده.
  • على اليمين، نفس السيناريو، حد بيطلب من "إيدن" يعمل قهوة، وإيدن بيعمل كابتشينو وبيقدمه للشخص، والشخص مبسوط وبيقول له شكراً.

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

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

خليني أديك مثال عملي باستخدام مبدأ Liskov Substitution Principle (LSP) مع Car.


Simple Car Example

المبدأ بيقول إنك تقدر تستخدم أي كلاس ابن بدل الكلاس الأب بدون ما يحصل أي مشاكل أو تغييرات في السلوك المتوقع.

المثال بدون تطبيق مبدأ Liskov:

abstract class Car {
  void startEngine();
  void drive();
}

Abstract Car

class PetrolCar implements Car {
  @override
  void startEngine() {
    print("Starting petrol engine...");
  }

  @override
  void drive() {
    print("Driving petrol car...");
  }
}

Petrol Car

class ElectricCar implements Car {
  @override
  void startEngine() {
    print("Electric cars don't have engines to start!");
  }

  @override
  void drive() {
    print("Driving electric car...");
  }
}

Electric Car

void main() {
  Car petrolCar = PetrolCar();
  petrolCar.startEngine(); // Output: Starting petrol engine...
  petrolCar.drive(); // Output: Driving petrol car...

  Car electricCar = ElectricCar();
  electricCar.startEngine(); // Output: Electric cars don't have engines to start!
  electricCar.drive(); // Output: Driving electric car...
}

Main Function


المشكلة:
كسر مبدأ LSP:
لما بنستخدم كلاس ElectricCar، بنواجه مشكلة لأن الكلاس ده بيتعامل مع مفهوم "بدء المحرك" اللي مش موجود فعلًا في السيارات الكهربائية. وده بيكسر مبدأ LSP لأنك لو بدلت كلاس PetrolCar بـ ElectricCar، هتحصل على سلوك غير منطقي، وهو محاولة بدء "محرك" غير موجود.

الحل بعد تطبيق مبدأ LSP:

  1. الخطوة 1: نفصل السلوك اللي مختلف بين الكلاسات ، مش كل السيارات ليها محرك، وده مفهوم مش لازم يكون موجود في كل العربيات. فهنخلي واجهة Car تتعامل مع السلوك الأساسي اللي كل العربيات بتشاركه، زي القيادة.
  2. الخطوة 2: نضيف واجهة جديدة مخصصة للعربيات اللي ليها محرك.
abstract class Car {
  void drive();
}

Abstract Car

abstract class EngineCar extends Car {
  void startEngine();
}

Abstract Engine Car

class PetrolCar implements EngineCar {
  @override
  void startEngine() {
    print("Starting petrol engine...");
  }

  @override
  void drive() {
    print("Driving petrol car...");
  }
}

Petrol Car

class ElectricCar implements Car {
  @override
  void drive() {
    print("Driving electric car...");
  }
}

Electric Car

void main() {
  EngineCar petrolCar = PetrolCar();
  petrolCar.startEngine(); // Output: Starting petrol engine...
  petrolCar.drive(); // Output: Driving petrol car...

  Car electricCar = ElectricCar();
  electricCar.drive(); // Output: Driving electric car...
}

Main Function

التحسينات بعد تطبيق المبدأ:

  1. فصل السلوكيات المختلفة: دلوقتي السيارات اللي ليها محرك بتستخدم واجهة EngineCar، واللي مش ليها محرك زي السيارات الكهربائية مش محتاجة الدالة startEngine.
  2. تطبيق مبدأ LSP: دلوقتي تقدر تستخدم ElectricCar أو PetrolCar بشكل منطقي من غير ما تحصل مشاكل. كل كلاس بيقوم بسلوكاته الخاصة من غير ما يورث دوال مش محتاجها.
  3. قابلية التوسع: لو أضفت أنواع جديدة من السيارات، تقدر تعملها بطريقة أسهل بدون ما تحصل تعقيدات بسبب توارث دوال مش مطلوبة.

Simple Shape Area Example

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

class RectangleWithoutLiskov {
  double _width;
  double _length;

  RectangleWithoutLiskov(this._width, this._length);

  set width(double width) {
    _width = width;
  }

  set length(double length) {
    _length = length;
  }

  double getArea() {
    return _width * _length;
  }
}

Rectangle Without Liskov Principle

class SquareWithoutLiskov extends RectangleWithoutLiskov {
  SquareWithoutLiskov(super._width, super._length);

  @override
  set width(double width) {
    super._width = width;
    super._length = width;
  }

  @override
  set length(double length) {
    super._length = length;
    super._width = length;
  }
}

Square Without Liskov Principle

void main() {
  RectangleWithoutLiskov rectangleWithoutLiskov = RectangleWithoutLiskov(5, 2);
  rectangleWithoutLiskov.width = 5;
  rectangleWithoutLiskov.length = 2;
  print(rectangleWithoutLiskov.getArea()); // Output: 10

  SquareWithoutLiskov squareWithoutLiskov = SquareWithoutLiskov(5, 2);
  squareWithoutLiskov.width = 5;
  // squareWithoutLiskov.length = 2; // هذا السطر سيؤدي إلى خرق مبدأ ليسكوف
  print(squareWithoutLiskov.getArea()); // Output: 25
}

Main Function


الكود اللي كتبته هنا بيكسر مبدأ Liskov Substitution Principle (LSP)، اللي هو جزء من الـ SOLID Principles. المشكلة بتظهر لما بنخلط بين الـ Rectangle والـ Square بطريقة تخلي السلوك بتاع الكود غير منطقي.

المشكلة في الكود:

خليط بين المستطيل والمربع: الـ SquareWithoutLiskov هو كلاس موروث من RectangleWithoutLiskov. ده بيعمل مشاكل لأن المستطيل ليه خواص مختلفة عن المربع.

المربع هو حالة خاصة من المستطيل، بحيث يكون الطول يساوي العرض. لكن هنا، لما تورث المربع من المستطيل وتعدل في width وlength، بتكسر المنطق الخاص بالمستطيل.

الاختلاف في السلوك:

في حالة الـ RectangleWithoutLiskov العادي، لما تحسب المساحة باستخدام width * length، الحساب بيكون سليم.
لكن في حالة الـ SquareWithoutLiskov، بيتم تعديل كلًا من width وlength ليكونوا نفس القيمة، وده بيؤدي لسلوك غير متوقع لو حاولت تستخدم الكلاس كـ Rectangle. وده اللي بنشوفه في السطر اللي بنعرف فيه:

هنا إنت عرّفت كائن من SquareWithoutLiskov بس إديته length مختلف عن width. لكن بعد ما تديله width جديد (5)، القيمة بتاعة length برضه بتتغير وتبقى نفس قيمة width، وده مش منطقي في حالة إنك عايز تتعامل مع الكائن كـ Rectangle.
السلوك الحالي:

هنا الحسابات متلغبطتش بسبب إن المربع فرض إنه يساوي الطول بالعرض وده كسر مبدأ LSP اللي بيقول إن الكلاس الأب لازم يكون ممكن تستبدله بالكلاس الابن من غير مشاكل.

لما نطبّق مبدأ Liskov Substitution، مش المفروض نخلّي Square يرث من Rectangle لأن المربع ليه قواعد مختلفة عن المستطيل. بدل ما نستخدم الوراثة، ممكن نستخدم (interface) أو (abstract class) بتحدد سلوك حساب المساحة لأي شكل هندسي.

الكود المحسن:

abstract class Shape {
  double getArea();
}

Shape Abstract Class

class Rectangle implements Shape {
  double width;
  double height;

  Rectangle(this.width, this.height);

  @override
  double getArea() {
    return width * height;
  }
}

Rectangle Shape

class Square implements Shape {
  double size;

  Square(this.size);

  @override
  double getArea() {
    return size * size;
  }
}

Square Shape


في الختام

بكده نكون شوفنا مع بعض أهمية مبدأ الـ Liskov Substitution في التوسع وفصل السلوكيات المختلفة وكمان شوفنا قد ايه بيتميز بالمرونة والصيانة الأسهل.

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