اذهب إلى المحتوى

يأتي مصطلح SOLID من الأحرف الأولى للمبادئ الخمسة للتصميم الكائني التوجه Object Oriented Design -أو  OOD  اختصارًا- والتي وضعها روبرت سي مارتن حيث تؤسس هذه المبادئ لممارسات تطبيقية أثناء تطوير البرمجيات مع الأخذ بعين الاعتبار سهولة الصيانة وقابلية التوسع مع نمو المشروع. ويساعد تبني هذه الممارسات على تفادي مشكلات كتابة الشيفرة وتحسين إعادة إنتاجها وتطوير برمجيات تعتمد نهج Agile أو Adaptive.

مبادئ SOLID

مبادئ SOLID

يرتكز مفهوم على المبادئ الخمس التالية:

  • مبدأ المسؤولية المفردة Single-responsibility Principle الحرف S.
  • مبدأ المفتوح والمغلق Open-closed Principle الحرف O.
  • مبدأ استبدال ليسكوف Liskov Substitution Principle الحرف L.
  • مبدأ عزل الواجهة Interface Segregation Principle الحرف I.
  • مبدأ الاعتماديات المتبادلة Dependency Inversion Principle الحرف D.

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

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

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

ينص مبدأ المسؤولية المفردة SRP على مايلي:

اقتباس

يجب أن يكون هناك سبب واحد فقط لكي يتغير الصنف، بمعنى أن يمتلك الصنف عملًا واحدًا فقط.

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

لتنفيذ المطلوب، ننشئ أصنافًا للأشكال ونضبط المعاملات المطلوبة لحساب المساحة عبر الدوال البانية constructor funcations. سنحتاج هنا إلى المتغير length لتخزين طول ضلع المربع

class Square
{
  public $length;

  public function construct($length)
  {
    $this->length = $length;
  }
}

كما سنحتاج إلى المتغير radius لتحديد نصف قطر الدائرة:

class Circle
{
  public $radius;

  public function construct($radius)
  {
    $this->radius = $radius;
  }
}

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

class AreaCalculator
{
  protected $shapes;

  public function __construct($shapes = [])
  {
    $this->shapes = $shapes;
  }

  public function sum()
  {
    foreach ($this->shapes as $shape) {
      if (is_a($shape, 'Square')) {
        $area[] = pow($shape->length, 2);
      } elseif (is_a($shape, 'Circle')) {
        $area[] = pi() * pow($shape->radius, 2);
      }
    }

    return array_sum($area);
  }

  public function output()
  {
    return implode('', [
     '',
       'Sum of the areas of provided shapes: ',
       $this->sum(),
     '',
   ]);
  }
}

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

  • دائرة نصف قطرها 2
  • مربع طول ضلعه 5
  • مربع طول ضلعه 6
$shapes = [
 new Circle(2),
 new Square(5),
 new Square(6),
];

$areas = new AreaCalculator($shapes);

echo $areas->output();

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

ولحل المشكلة، بإمكاننا إنشاء صنف آخر SumCalculatorOutputter واستخدامه لمعالجة منطق الخرج:

class SumCalculatorOutputter
{
  protected $calculator;

  public function __constructor(AreaCalculator $calculator)
  {
    $this->calculator = $calculator;
  }

  public function JSON()
  {
    $data = [
     'sum' => $this->calculator->sum(),
   ];

    return json_encode($data);
  }

  public function HTML()
  {
    return implode('', [
     '',
       'Sum of the areas of provided shapes: ',
       $this->calculator->sum(),
     '',
   ]);
  }
}

نستخدم الصنف SumCalculatorOutputter كالتالي:

$shapes = [
 new Circle(2),
 new Square(5),
 new Square(6),
];

$areas = new AreaCalculator($shapes);
$output = new SumCalculatorOutputter($areas);

echo $output->JSON();
echo $output->HTML();

في هذه الحالة يتولى الصنف SumCalculatorOutputter عملية إخراج البيانات إلى المستخدم ويحقق مبدأ المسؤولية المفردة.

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

ينص المبدأ على ما يلي:

اقتباس

يجب أن تكون الكائنات أو الكيانات مفتوحة من ناحية القدرة على التوسع ومغلقة في وجه التعديلات.

ويعني ذلك أن الصنف ينبغي أن يكون قادرًا على التوسع دون الحاجة لتعديل أي شيء موجود فيه.

لنعد مجددًا إلى الصنف AreaCalculator ونركز هذه المرة على التابع sum:

class AreaCalculator
{
  protected $shapes;

  public function __construct($shapes = [])
  {
    $this->shapes = $shapes;
  }

  public function sum()
  {
    foreach ($this->shapes as $shape) {
      if (is_a($shape, 'Square')) {
        $area[] = pow($shape->length, 2);
      } elseif (is_a($shape, 'Circle')) {
        $area[] = pi() * pow($shape->radius, 2);
      }
    }

    return array_sum($area);
  }
}

لنتأمل الآن حالة يرغب فيها المستخدم بجمع مساحات أشكال أخرى مثل المثلثات والمخمسات والمسدسات. سيكون علينا في هذا الحالة إضافة شروط جديدة في كتلة if/else، وهذا سيخرق مبدأ المفتوح والمغلق.

سيكون أحد الحلول التي تحسن التابع sum هو إزالة منطق حساب مساحة كل شكل وتنفيذ العملية في كل صنف على حدة.

نضيف هنا التابع area للصنف square:

class Square
{
  public $length;

  public function __construct($length)
  {
    $this->length = $length;
  }

  public function area()
  {
    return pow($this->length, 2);
  }
}

نضيف هنا التابع area للصنف Circle:

class Circle
{
  public $radius;

  public function construct($radius)
  {
    $this->radius = $radius;
  }

  public function area()
  {
    return pi() * pow($shape->radius, 2);
  }
}

ويمكن الآن إعادة كتابة التابع sum للصنف AreaCalculator كالتالي:

class AreaCalculator
{
  // ...

  public function sum()
  {
    foreach ($this->shapes as $shape) {
      $area[] = $shape->area();
    }

    return array_sum($area);
  }
}

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

ولأن بناء واجهة Interface هو جزء أساسي من SOLID، لننشئ واجهة ShapeInterface تملك التابع area:

interface ShapeInterface
{
  public function area();
}

نعدّل أصناف الأشكال لتتخذ من ShapeInterface واجهة لها، ونبدأ بالصنف Square:

class Square implements ShapeInterface
{
  // ...
}

ثم الدائرة Circle:

class Circle implements ShapeInterface
{
  // ...
}

نتحقق بعد ذلك أثناء تنفيذ التابع sum إن كان للكائن الممرر واجهة شكل، أي أنه نسخة عن ShapeInterface ويرمي استثناء Exceptionخلاف ذلك:

 class AreaCalculator
{
  // ...

  public function sum()
  {
    foreach ($this->shapes as $shape) {
      if (is_a($shape, 'ShapeInterface')) {
        $area[] = $shape->area();
        continue;
      }

      throw new AreaCalculatorInvalidShapeException();
    }

    return array_sum($area);
  }
}

وهكذا نكون قد حققنا مبدأ المفتوح والمغلق.

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

ينص المبدأ على ما يلي:

اقتباس

لتكن الخاصية (q(x محققة من أجل كائن x من النوع T، يجب عندها أن تتحقق الخاصية بالنسبة لكائن y من النوع S إذا كان S نوع فرعي من T.

ويعني ذلك أن أي صنف فرعي subclass أو مشتق قابل للاستبدال بواسطة الصنف الأب. لنعد إلى مثالنا السابق ولنفرض وجود صنف جديد VolumeCalculator يوسّع الصنف AreaCalculator:

class VolumeCalculator extends AreaCalculator
{
  public function construct($shapes = [])
  {
    parent::construct($shapes);
  }

  public function sum()
  {
    // logic to calculate the volumes and then return an array of output
    return [$summedData];
  }
}

نتذكر أن الصنف SumCalculatorOutputter هو كالتالي:

class SumCalculatorOutputter {
  protected $calculator;

  public function __constructor(AreaCalculator $calculator) {
    $this->calculator = $calculator;
  }

  public function JSON() {
    $data = array(
      'sum' => $this->calculator->sum();
    );

    return json_encode($data);
  }

  public function HTML() {
    return implode('', array(
      '',
        'Sum of the areas of provided shapes: ',
        $this->calculator->sum(),
      ''
    ));
  }
}

فلو حاولنا تنفيذ المثال كالتالي:

$areas = new AreaCalculator($shapes);
$volumes = new VolumeCalculator($solidShapes);

$outputArea = new SumCalculatorOutputter($areas);
$outputVolume = new SumCalculatorOutputter($volumes);

عندما نستدعي التابع ()HTML للكائن outputVoulme$ سنحصل على خطأ E_NOTICE يخبرنا بوجود عملية تحويل من مصفوفة إلى نص. ولحل المشكلة نعيد المتغير summedData$ في الصنف VolumeCalculator بدلًا من إعادة مصفوفة:

class VolumeCalculator extends AreaCalculator
{
  public function construct($shapes = [])
  {
    parent::construct($shapes);
  }

  public function sum()
  {
    // logic to calculate the volumes and then return a value of output
    return $summedData;
  }
}

قد يكون المتغير summedData$ من النوع float أو double أو integer وهذا سيحقق مبدأ استبدال ليسكوف.

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

ينص المبدأ على ما يلي:

اقتباس

لا يُجبر المستخدم على تنفيذ واجهة لا يحتاجها، أو لا يُجبر العميل على اعتماد توابع لا يستخدمها.

نوضح هذا المبدأ بمتابعة العمل على مثالنا السابق وننطلق من الواجهة ShapeInterface. سنحتاج الآن إلى دعم ثلاث أشكال ثلاثية الأبعاد هي Cuboid و Spheroid، ونريد حساب حجومها volume.

لنتأمل ما قد يحدث إن عدّلنا الواجهة ShapeInterface لإضافة تابع جديد:

interface ShapeInterface
{
  public function area();

  public function volume();
}

في هذه الحالة سيكون كل شكل مجبرًا على تبني تابع الحجم volume، لكن كما نعلم لا أحجام للأشكال ثنائية البعد مثل الدائرة وهذه الواجهة ستجبر الصنف Circle على تنفيذ هذا التابع الذي لا يحتاجه.

يُعد هذا الأمر خرقًا لمبدأ عزل الواجهة، وبدلًا من ذلك، بإمكانك إنشاء واجهة جديدة تُدعى ThreeDimensionalShapeInterface تقدم التابع volume وتتبناها الأشكال ثلاثية الأبعاد:

interface ShapeInterface
{
  public function area();
}

interface ThreeDimensionalShapeInterface
{
  public function volume();
}

class Cuboid implements ShapeInterface, ThreeDimensionalShapeInterface
{
  public function area()
  {
    // calculate the surface area of the cuboid
  }

  public function volume()
  {
    // calculate the volume of the cuboid
  }
}

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

interface ManageShapeInterface
{
  public function calculate();
}

class Square implements ShapeInterface, ManageShapeInterface
{
  public function area()
  {
    // calculate the area of the square
  }

  public function calculate()
  {
    return $this->area();
  }
}

class Cuboid implements ShapeInterface, ThreeDimensionalShapeInterface, ManageShapeInterface
{
  public function area()
  {
    // calculate the surface area of the cuboid
  }

  public function volume()
  {
    // calculate the volume of the cuboid
  }

  public function calculate()
  {
    return $this->area();
  }
}

في هذا النهج، بإمكاننا استبدال التابع area في الصنف AreaCalculatot بالتابع calculate، ونتحقق أيضًا أن الكائن هو نسخة عن الصنف ManageShapeInterface وليس ShapeInterface وهكذا يكون مبدأ فصل الواجهة قد تحقق لدينا.

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

ينص هذا المبدأ على ما يلي:

اقتباس

ينبغي أن تعتمد الكيانات البرمجية على التجريد abstraction وليس على طريقة التنفيذ implementation. فلا يجب أن تعتمد الوحدات ذات المستوى الأعلى على وحدات المستوى الأدنى بل على الشيفرة المجردة.

يسمح هذا المبدأ بفصل الشيفرة. ولإيضاح المبدأ لنأخذ مثالًا عن صنف PasswordReminder يربط الشيفرة بقاعدة البيانات MySQL:

class MySQLConnection
{
  public function connect()
  {
    // handle the database connection
    return 'Database connection';
  }
}

class PasswordReminder
{
  private $dbConnection;

  public function __construct(MySQLConnection $dbConnection)
  {
    $this->dbConnection = $dbConnection;
  }
}

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

لا ينبغي أن يهتم الصنف PasswordReminder بنوع قاعدة البيانات المستخدمة، ولتلافي ذلك، بإمكانك بناء واجهة طالما أن المبدأ الأخير ينص على ضرورة اعتماد وحدات المستوى الأعلى والأدنى على التجريد:

interface DBConnectionInterface
{
  public function connect();
}

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

class MySQLConnection implements DBConnectionInterface
{
  public function connect()
  {
    // handle the database connection
    return 'Database connection';
  }
}

class PasswordReminder
{
  private $dbConnection;

  public function __construct(DBConnectionInterface $dbConnection)
  {
    $this->dbConnection = $dbConnection;
  }
}

وهكذا ستعتمد وحدات المستوى الأعلى والأدنى على التجريد.

الخلاصة

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

ترجمة-وبتصرف- للمقال: SOLID: the first 5 principles of object oriented design لمؤلفيه Samuel Oloruntoba و Anish Singh Walia

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...