انماط التصميم SOLID فى هندسة البرمجيات

انماط التصميم SOLID فى هندسة البرمجيات

أنماط التصميم Design Patterns بشكل عام هى مجموعة من الحلول الشائعة فى هندسة البرمجيات لبعض انماط المشكلات المتكررة.
تعد أنماط التصميم مهمة في هندسة البرمجيات لأنها توفر طريقة لإعادة استخدام الكود وتحسين كفاءة عملية التطوير.

لاحظ التالى
لمعرفة المزيد عن انماط التصميم بشكل عام يمكنك قراءة هذه المقال هنا.

SOLID هي اختصار لخمس مبادئ لتصميم البرمجيات الموجهة للكائنات (Object-Oriented Programming) التي تساعد على تصميم برمجيات قابلة للصيانة والتعديل والتوسعة بشكل أفضل. وهذه المبادئ هي:

  • مبدأ المسؤولية الفردية The Single Responsibility Principle: يجب أن يكون للفئة Class مسؤولية واحدة فقط.
  • مبدأ مفتوح / مغلق: يجب أن تكون Classes مفتوحة للتمديد او التوسع ولكن مغلقة للتعديل.
  • مبدأ استبدال Liskov: يجب أن تكون الكلاسات المشتقة قابلة للاستبدال بالكلاسات الأساسية.
  • مبدأ تقسيم الواجهة Interface: لا ينبغي إجبار المستخدم للواجهة على تنفيذ دوال لا يحتاجونها.
  • مبدأ انعكاس التبعية Dependency Inversion: يجب عكس التبعيات بين الوحدات.

لا حظ ان هذا المبادئ مترابطة بشكل كبير اي ان تجاهل واحد منها قد يؤدى إلى مخالفة الباقى.

مبادئ SOLID فى هندسة البرمجيات

مبدأ المسؤولية الفردية The Single Responsibility Principle

Single Responsibility Principle (مبدأ المسؤولية الواحدة): وهو المبدأ الذي ينص على أن الكائن البرمجي يجب أن يكون له مسؤولية واحدة فقط، وأن يتم فصل المسؤوليات المختلفة في كائنات منفصلة.

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

مثال لتوضيح المعنى على مبدأ المسؤولية الفردية

لتوضيح ذلك، يمكن النظر إلى مثال يتعلق بإدارة الموظفين في شركة.
ونريد التالى :

  • إضافة موظف جديد
  • تحديث معلومات الموظف
  • حذف موظف
  • عرض معلومات الموظفين
  • توليد تقارير الموظفين

ستقول حسنًا يمكن كتابته بالشكل التالى

class Employee {
    private $id;
    private $name;
    private $email;
    private $salary;
    
    public function addEmployee($name, $email, $salary) {
        // إضافة موظف جديد
    }
    
    public function updateEmployee($id, $name, $email, $salary) {
        // تحديث معلومات الموظف
    }
    
    public function getEmployee($id) {
        // عرض تفاصيل الموظف
    }
    
    public function deleteEmployee($id) {
        // حذف الموظف
    }
}

 سيعمل بالتأكيد هذا التصميم ولكن يمكن أن يتسبب هذا التصميم في مشاكل مختلفة، حيث أن هذا الكلاس يقوم بتنفيذ وظائف متعددة، مما يجعله ينتهك مبدأ الـ Single Responsibility Principle. على سبيل المثال:

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

بالتالي، يجب تجنب تصميم الكائنات أو الدوال التي تنفذ وظائف متعددة والالتزام بمبدأ الـ Single Responsibility Principle لتحسين جودة الشفرة وتسهيل صيانتها.

أما باستخدام مبدأ الـ Single Responsibility Principle، يجب إنشاء كلاسات مختلفة لكل من هذه المهام، بدلاً من وضعها جميعاً في كلاس واحد. على سبيل المثال، يمكن إنشاء الكلاسات التالية:

  • EmployeeAdder: يتحمل مسؤولية إضافة موظف جديد.
  • EmployeeUpdater: يتحمل مسؤولية تحديث معلومات الموظف.
  • EmployeeDeleter: يتحمل مسؤولية حذف موظف.
  • EmployeeViewer: يتحمل مسؤولية عرض معلومات الموظفين.
  • EmployeeReporter: يتحمل مسؤولية توليد تقارير الموظفين.

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

مبدأ المفتوح / المغلق (Open/Closed Principle) يتمثل في أن الكود البرمجى يجب أن يكون “مغلقاً للتعديل، ومفتوحاً للامتداد”، أي أنه يجب تصميم البرنامج بحيث يكون من السهل إضافة وتغيير الميزات الجديدة دون تعديل الشفرة الحالية.

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

مثال لتوضيح المعنى على مبدأ مفتوح / مغلق

لتوضيح فكرة مبدأ المفتوح / المغلق، يمكن النظر إلى مثال بسيط يتعلق بتصميم نظام لإدارة السيارات. لنفرض أن لدينا نظام يعتمد على مجموعة من الكائنات المتعلقة بالسيارات، والتي يتم تمثيلها بواسطة كلاس Car.

بموجب مبدأ المفتوح / المغلق، يجب تصميم الكلاس بحيث يكون مفتوحًا للتوسع ومغلقًا للتعديل. يعني هذا أنه يمكن إضافة مزايا جديدة للكلاس، ولكن لا يجب تعديله لتحقيق ذلك.

على سبيل المثال، يمكن إضافة خاصية جديدة للسيارات مثل GPS دون الحاجة لتعديل الكود القائم. يمكن فعل ذلك عن طريق إنشاء فئة جديدة مثل GPSDevice وربطها بـ Car باستخدام واجهة مشتركة.

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

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

class Car {
  private $make;
  private $model;
  private $year;

  public function __construct($make, $model, $year) {
    $this->make = $make;
    $this->model = $model;
    $this->year = $year;
  }

  public function getMake() {
    return $this->make;
  }

  public function getModel() {
    return $this->model;
  }

  public function getYear() {
    return $this->year;
  }

  public function printDetails() {
    echo "Make: " . $this->make . ", Model: " . $this->model . ", Year: " . $this->year;
  }
}

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

interface GPS {
  public function getGPSData();
}

class CarWithGPS extends Car implements GPS {
  private $gpsDevice;

  public function __construct($make, $model, $year, $gpsDevice) {
    parent::__construct($make, $model, $year);
    $this->gpsDevice = $gpsDevice;
  }

  public function getGPSData() {
    return $this->gpsDevice->getData();
  }
}

في هذا المثال، تم إنشاء واجهة GPS لتحديد الخصائص التي يجب توفرها في الفئات التي ترثها.
ثم تم إنشاء فئة جديدة تسمى CarWithGPS، التي ترث من Car وتنفذ GPS. وبالتالي، يمكننا إنشاء كائنات CarWithGPS التي تحتوي على كلاس GPS مضافًا إليها، دون الحاجة إلى تعديل الكلاس الأصلي Car.

مبدأ استبدال Liskov

ببساط يعنى هذا المبدء إذا كان كلاس A يرث كلاس B فيمكننا إستبدل B ب A دون ان يلاحظ أحد الفرق.

مثال لتوضيح المعنى على مبدأ إستبدال Liskov

يمكن استخدام مبدأ Liskov Substitution عند استخدام قواعد البيانات على سبيل المثال, حيث يمكن إنشاء واجهة (Interface) التي تحتوي على الدوال العامة المستخدمة في الاتصال بقاعدة البيانات، وتعريفها على أنها تلبي مبدأ Liskov Substitution. ثم يمكن إنشاء فئات تنفذ هذه الواجهة وتقوم بتنفيذ الدوال اللازمة للاتصال بقاعدة البيانات المحددة.

لنفترض أن لدينا واجهة DatabaseInterface تحتوي على الدوال العامة للاتصال بقاعدة البيانات، وهذه الدوال هي connect() وexecuteQuery(). يمكن تعريف هذه الواجهة على النحو التالي:

interface DatabaseInterface {
  public function connect($host, $username, $password, $dbname);
  public function executeQuery($query);
}

ثم يمكن إنشاء فئات تنفذ هذه الواجهة للاتصال بقواعد البيانات المختلفة. على سبيل المثال، يمكن إنشاء فئة MySQLDatabase وفئة PostgreSQLDatabase، كلاهما ينفذ الواجهة DatabaseInterface بحيث يتم الاتصال بقاعدة البيانات المحددة وتنفيذ الاستعلامات اللازمة.

class MySQLDatabase implements DatabaseInterface {
  public function connect($host, $username, $password, $dbname) {
    $dsn = "mysql:host=$host;dbname=$dbname";
    $pdo = new PDO($dsn, $username, $password);
    return $pdo;
  }

  public function executeQuery($query) {
    // تنفيذ الاستعلام على MySQL
  }
}

class PostgreSQLDatabase implements DatabaseInterface {
  public function connect($host, $username, $password, $dbname) {
    $dsn = "pgsql:host=$host;dbname=$dbname";
    $pdo = new PDO($dsn, $username, $password);
    return $pdo;
  }

  public function executeQuery($query) {
    // تنفيذ الاستعلام على PostgreSQL
  }
}

فى حالة مثلًا الاعتماد على قاعدة بيانات MySQL وقمت بعمل الإستعلامات اللازمة فى كل مكان فى المشروع وبعد فترة أردت تغير نوع قاعدة البيانات إلى النوع PostgreSQL ففى هذه الحالة لا يجب أن يشكل هذا مشكلة أبدًا لأن كل انواع قواعد البيانات الموجودة (MySQL و PostgreSQL ) تقوم بتفيذ نفس الواجهة (اي كلامهما نوع DatabaseInterface) وبالتالى كلاهما لديه نفس الدوال connect او executeQuery.

مبدأ تقسيم الواجهة Interface

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

مثال لتوضيح المعنى على مبدأ تقسيم الواجهة

لنفترض أن لدينا واجهة لتخزين البيانات DataStorageInterface ولدينا عدة كلاسات تطبق هذه الواجهة مثل MySQLDataStorage و PostgreSQLDataStorage. يمكننا كتابة الواجهة كالتالي:

interface DataStorageInterface {
  public function saveData($data);
  public function fetchData($id);
}

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

على سبيل المثال يمكن تطبيق هذه الواجهة في كلاس MySQLDataStorage كما يلي:

class MySQLDataStorage implements DataStorageInterface {
  public function saveData($data) {
    // اتصال بقاعدة البيانات MySQL وحفظ البيانات
  }
  
  public function fetchData($id) {
    // اتصال بقاعدة البيانات MySQL وجلب البيانات
  }
}

ويمكن تطبيق نفس الواجهة في كلاس PostgreSQLDataStorage كما يلي:

class PostgreSQLDataStorage implements DataStorageInterface {
  public function saveData($data) {
    // اتصال بقاعدة البيانات PostgreSQL وحفظ البيانات
  }
  
  public function fetchData($id) {
    // اتصال بقاعدة البيانات PostgreSQL وجلب البيانات
  }
}

مبدأ إنعكاس التبعية Dependency Inversion

يشير مبدأ انعكاس التبعية إلى فصل وحدات البرامج. فبدلاً من أعتماد الوحدات عالية المستوى على الوحدات ذات المستوى المنخفض ، سيعتمد كلاهما على العقد او Abstraction.

ويتم ذلك عن طريق التالى:

  • استخدام الاعتماد على العقد (Abstractions) بدلاً من الاعتماد على التفاصيل المحددة لكل تنفيذ (Concretes).
  • تفادي إدخال تفاصيل التنفيذ في الأجزاء العليا من التطبيق، وبدلاً من ذلك استخدام حاويات Dependency Injection (DI) لحقن الاعتمادات في الكائنات.
  • فصل مكونات التطبيق إلى طبقات (Layers) من خلال استخدام تقنية Dependency Inversion.

ويمكن تلخيص هذا المبدأ في جملة بسيطة هي: “الوحدات العليا لا يجب أن تعتمد على التفاصيل المحددة للوحدات الدنيا، بل العكس”.

مثال لتوضيح المعنى على مبدأ إنعاكس التبعية

لدينا واجهة DataSource التي تحتوي على الدوال الأساسية للتعامل مع قاعدة البيانات، ولدينا اثنين من المصادر المحتملة: MySqlDataSource و PostgreSqlDataSource، وكل منها تنفذ الواجهة DataSource.

interface DataSource {
    public function connect();
    public function query(string $sql);
    public function fetch();
    public function close();
}

class MySqlDataSource implements DataSource {
    private $connection;
    public function connect() {
        // الاتصال بقاعدة البيانات MySQL
    }
    public function query(string $sql) {
        // تنفيذ الاستعلام على قاعدة البيانات MySQL
    }
    public function fetch() {
        // استرداد البيانات من قاعدة البيانات MySQL
    }
    public function close() {
        // إغلاق الاتصال بقاعدة البيانات MySQL
    }
}

class PostgreSqlDataSource implements DataSource {
    private $connection;
    public function connect() {
        // الاتصال بقاعدة البيانات PostgreSQL
    }
    public function query(string $sql) {
        // تنفيذ الاستعلام على قاعدة البيانات PostgreSQL
    }
    public function fetch() {
        // استرداد البيانات من قاعدة البيانات PostgreSQL
    }
    public function close() {
        // إغلاق الاتصال بقاعدة البيانات PostgreSQL
    }
}

class DataManager {
    private $dataSource;
    public function __construct(DataSource $dataSource) {
        $this->dataSource = $dataSource;
    }
    public function getData() {
        $this->dataSource->connect();
        $this->dataSource->query("SELECT * FROM data");
        $data = $this->dataSource->fetch();
        $this->dataSource->close();
        return $data;
    }
}

// استخدام MySqlDataSource
$dataManager = new DataManager(new MySqlDataSource());
$data = $dataManager->getData();

// استخدام PostgreSqlDataSource
$dataManager = new DataManager(new PostgreSqlDataSource());
$data = $dataManager->getData();

لاحظ في هذا المثال، يمكننا إنشاء مصادر بيانات أخرى تنفذ الواجهة DataSource واستخدامها في DataManager بدون تغيير الكود الحالي لأنه يعتمد فقط على الواجهة وليس على المصدر الفعلي للبيانات. هذا هو مبدأ Dependency Inversion.

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

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

// استخدام MySqlDataSource
$dataManager = new DataManager(new MySqlDataSource());
$data = $dataManager->getData();

// استخدام PostgreSqlDataSource
$dataManager = new DataManager(new PostgreSqlDataSource());
$data = $dataManager->getData();

لاحظ ان هذا الجزء من الكود يعتمد تنفيذه على الجزء السابق وهذا مانعنى به انه أعلى منه أي انه يعتمد عليه.

اشترك فى القائمة البريدية

عن الكاتب

شارك على وسائل التواصل