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