skip to Main Content
شرح قواعد Solid Principles بلغة PHP

شرح قواعد solid principles بلغة PHP

Post Series: سلسة تطوير ماجنتو للمبتدئين

شرح قواعد solid principles بلغة PHP هو الدرس الثاني من سلسة تعليم تطوير الماجنتو للمبتدأين و هو درس هام جداً لكل مطور سواء أراد العمل علي ماجنتو أو غيرها

قبل البدء في تعلم تطوير الماجنتو؛ نفترض انك ملم تماماً بالبرمجة الكائنية object oriented programming فالماجنتو ليست للمبتدأين؛ لذلك ننصحك بمراجعة قواعد البرمجة الكائنية قبل البدء

SOLID هو مصطلح أطلقه روبرت مارتن عام 2000 في ورقة بحثية بعنوان مبادئ التصميم و أنماط التصميم؛ و هي مجموعة من مبادئ البرمجة الكائنية التي تهدف الي انتاج كود مرن يسهل التعامل معه لاحقا من حيث الصيانة و التوسع بسهولة و يسر كما أنها مجموعة من المبادئ التي يمكن تطبيقها علي أي لغة برمجة و لكننا سنركز علي كيفية تطبيقها في لغة PHP .

SOLID هي كلمة مختصرة ل5 مبادئ و هم

الترجمة العربية للمبادئ هي للشرح فقط و لاتستخدم في الواقع؛ يرجي استخدام اللغة الانجليزية عند ذكر مبادئ SOLID في أي مقابلة عمل او اثناء البحث علي الانترنت

  • Single responsibility principle مبدأ المسئولية الواحدة
  • Open/Closed Principle مبدأ مفتوح/مغلق
  • Liskov substitution principle مبدأ ليكسوف للاستبدال
  • Interface segregation principle مبدأ الفصل بين الواجهات
  • Dependency inversion principle مبدأ انعكاس التبعية

مبدأ المسئولية الواحدة Single responsibility principle

ينص هذا المبدأ علي أن يكون لكل كلاس (class) مسئولية واحدة فقط و سبب واحد للتغيير؛ بمعني انه اذا قمت بأي تغيير ينبغي ان لا يتأثر به أي كلاس اخر . لنفترض ان لدينا كلاس بإسم Order مسئول عن العمليات التي تخص الطلبات علي المتجر الالكتروني و قد قمنا باضافة دالة لحساب المبلغ النهائي و دالة اخرى لاصدار فاتورة بصيغة PDF . نريد الان اجراء تغيير علي دالة اصدار الفاتورة بصيغة PDF و لكن الاجراء يتطلب تغيير اخر في دالة حساب المبغ النهائي للطلب و بذلك أصبح لدينا الان سببان للتغير و ليس سبب واحد و هو مخالف لمبدأ المسئولية الواحدة. لنفترض أننا قمنا بإضافة جميع الدوال الخاص بالطلب في كلاس واحد ؛ إذا أردت التعديل علي دالة واحدة ربما يتأثر بها باقي الدوال الموجودة بالكلاس و بالتالي يصبح لدينا الكثير من الاسباب التي تستدعي التغيير في الكلاس و هذا ينتج العديد من المشاكل البرمجية bugs.

مثال

سنقوم في هذا المثال باضافة 3 دوال كل منهما يتظلب سبب واحد للتغيير. الدالة الاولي تقوم بحساب المبلغ النهائي للطلب . ربما يطلب منك العميل بشكل متغير تغيير طريقة حساب المبلغ النهائي مثلا (السبب الأول). الدالة الثانية تقوم باصدار فاتورة بصيغة PDF . ربما يطلب منك العميل مثلا اجرائ تغيير في شكل الفاتورة (السبب الثاني). أما الدالة الثالثة فمسئولية ان إحضار عدد من الطلبات من قاعدة البيانات و بالتالي أي تغيير في تكوين قاعدة البيانات سيتطلب تغييراً في هذه الدالة (السبب الثالث). نجد أن هذا الكلاس أصبح به 3 أسباب للتغيير و هو منافي لمبدأ المسئولية الواحدة

class Order 
{
    public function calculateTotalPrice() 
    {
        /* calculate order total price */
    }
    public function generatePdfInvoice() 
    {
        /* Code that generates a PDF Invoice */
    }
    public function getOrdersFromDatabase() 
    {
        /* Get list of orders from database */
    }
}

لحل هذه المشكلة لتتوافق مع مبدأ المسئولية الواحدة؛ سنقوم بفصل الدوال الي كلاسات جديدة كل منها به سبب واحد فقط للتغيير. فمثلا سنقوم بفصل دالة حساب مبلغ الطلب النهائي بكلاس مختلف عن كلاس استدعاء الطلبات من قاعدة البيانات . أما دالة اصدار الفاتورة فسنقوم بفصلها الا 2 كلاس واحد لاحضار بيانات الاورد مثلا وكلاس اخر لتوليد شكل الفاتورة كما بالمثال الاتي

class Order 
{
    public function calculateTotalPrice() 
    {
        /* calculate price with order data */
    }
}
class OrderCollection 
{
    public function getCollection() 
    {
        /* Get orders from database */
    }
}
class InvoicePdf 
{
    public function generate() 
    {
        /* Code that generates a PDF */
        $pdfLayout = new PdfLayout($this);
    }
}
class PdfLayout 
{
    public function __construct(InvoicePdf $invoicePdf)
    {
        /* generate layout based on data for pdf */
    }
}

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

ينص هذا المبدأ علي أن يكون الكلاس قابل للتوسع و غير قابل للتعديل. بمعني أن يكون الكلاس الخاص بك قابل للتوسع و الوراثة ليأخذ أشكال متعددة بدلا من أن تكون جميع تعديلاتك في نفس الكلاس الرئيسي.

مثال

نفترض اننا نقوم بانشاء متجر الكتروني و لدينا كلاس باسم Customer يمثل الزبائن و لدينا أكثر من نوع من الزبائن مثلا زبون دائم و زبون ممثل عن شركة و زبون للجملة و انواع أخرى . و كل نوع له حساباته المختلفة . و لمعالجة كل حالة قررنا ان نستخدم if elseif او switch case . بالتالي يجب أن نقوم بإضافات هذه الجمل الشرطية في كل دالة تقوم بأي عملية علي نوع الزبون الموجود لدينا . تخيل ان لدينا 100 عملية تعتمد علي نوع الزبون سوف نقوم بكتابة الجمل الشرطية التي تخص التحقق من نوع الزبون 100 مرة و هذا يعني أن لدينا 100 احتمال علي الاقل لحدوث مشاكل برمجية .

class AllCustomers 
{
//نفترض ان لدينا هذه الدالة لعرض اسم الزبون و قد قمنا هنا باستخدام جمل شرطية للتحقق من نوع الزبون, لو أن لدينا أكثر من دالة تعتمد على نوع الزبون بهذا الشكل. إذن سنقوم باضافة هذه الجملة الشرطية لجميع هذه الدوال
    public function show($customers) {
        foreach ($customers as $customer) {
            switch ($customer->type) {
                case 'Business':
                    echo $customer->getFirstName().' '.$customer->getLastName();
                    break;
                case 'Retail':
                    echo $customer->getCompanyName();
                    break;
            }
        }
    }
}

و لحل هذه المشكلة سنقوم بتطبيق مبدأ مفتوح/مغلق Open/Closed principle حيث سنقوم بانشاء كلاسات جديدة لكل نوع من انواع الزبائن لدينا مما يقلل حجم الخطأ و امكانية حدوث مشاكل برمجية

interface Customer {
    public function getName();
}
class RetailCustomer implements Customer {
    private $firstName;
    private $lastName;
    public function __construct($customer) { ... }
    public function getName() {
        return $this->firstName.' '.$this->lastName;
    }
}
class BusinessCustomer implements Customer 
{
    private $companyName;
    public function __construct($customer) { ... }
    public function getName() 
    {
        return $this->companyName;
    }
}
class AllCustomers 
{
//لاحظ اننا أصبحنا لا نحتاج وجود جمل شرطية بعد تطبيق مبدأ مفتوح/مغلق
    public function show($customers) 
    {
        foreach ($customers as $customer) {
            echo $customer->getName();
        }
    }
}

مبدأ لكسوف للاستبدال Liksov substitution principle

يمكن تغيير الكائنات (objects) بأخري من الانواع الفرعية لها بدون التأثير علي صحة البرنامج. بمعنى أنه إذا كنت تفكر في انشاء كلاس جديد يرث من كلاس آخر فيجب ان تختار الكلاس الاب المناسب و المنطقي لهذا الكلاس الفرعي.

مثال

سوف نقوم في هذا المثال بانشاء كلاس يمثل شكل المربع ووراثته من كلاس للمستطيل ﻷن الشكلين لهما 4 أضلاع و المربع هو مستطيل أضلاعه متساوية في النهاية.

class Rectangle
{
    protected $height;
    protected $width;
    public function setHeight(int $height)
    {
        $this->height = $height;
    }
    public function setWidth(int $width)
    {
        $this->width = $width;
    }
}
class Square extends Rectangle
{
    public function setHeight(int $height)
    {
        $this->height = $height;
        $this->width = $height;
    }
    public function setWidth(int $width)
    {
        $this->height = $width;
        $this->width = $width;
    }
}

الان لو لدينا دالة تستخدم المستطيل و قمنا باستبدال كلاس المستطيل بالنوع الفرعي منه و هو المربع سنواجه مشكلة انه عند تعيين الطول يتغير العرض و الطول معاً و عند تغيير العرض أيضا يتغيير الطول و العرض معاً. و لحل هذه المشكلة سنلجأ الي مبدأ Liksov و نقوم بانشاء abstract class يمثل الشكل و نرث منه مرة للمستطيل و مرة للمربع و بالتالي لن نجد مشكلة في الاستبدال بينهم

abstract class AbstractShape
{
    abstract public function Area();
}
class Rectangle extends AbstractShape
{
    private $height;
    private $width;
    public function setHeight($height)
    {
        $this->height = $height;
    }
    public function setWidth($width)
    {
        $this->width = $width;
    }
    public function Area()
    {
        return $this->height * $this->width;
    }
}
class Square extends AbstractShape
{
    private $sideLength;
    public function setSideLength($sideLength)
    {
        $this->sideLength = $sideLength;
    }
    public function Area()
    {
        return $this->sideLength * $this->sideLength;
    }
}

مبدأ الفصل بين الواجهات Interface segregation principle

ينص هذا المبدأ علي انه يمكنك استخدام الكثير من الواجهات interfaces المخصصة أفضل من واجهة واحدة عامة.

لا يجب أن تستخدم واجهات لا معني لها. و أيضا لا يجب ان تستخدم واجهات بها دوال لا تنوي استخدامها في الكلاس الذي تقوم بانشاءه و الافضل من ذلك ان تقوم بانشاء أكثر من واجهة مخصصة لاستخدامها

مثال

سنقوم بانشاء واجهة تعبر عن طائر و نقوم بانشاء دوال تعبر عن الاكل و الطيران، ربما تعبر هذه الواجهة فعلا عن جميع أنواع الطيور و لكن اذا انشأنا فئة (class) يعبر عن النعامة مثلا فهي طائر و لكنها لاتطير إذن ستكون لدينا مشكلة مع دالة الطيران التي قمنا بانشاءها مسبقاً.

interface Bird 
{
    public function eat();
    public function fly();
}
class Sparrow implements Bird
{
    public function eat() { ... }
    public function fly() { ... }
}
class Ostrich implements Bird
{
    public function eat() { ... }
    public function fly() { /* exception */ }
}

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

interface Bird
{
    public function eat();
}
interface FlyingBird 
{
    public function fly();
}
interface RunningBird 
{
    public function run();
}
class Sparrow implements Bird, FlyingBird
{
    public function eat() { ... }
    public function fly() { ... }
}
class Ostrich implements Bird, RunningBird 
{
    public function eat() { ... }
    public function run() { ... }
}

Dependency inversion principle مبدأ انعكاس التبعية

في المشاريع البرمجية يوجد لدينا دائما مستويان للتطوير : المستوي الاعلي (high level module) و المستوي الادني (low level module) و المستوي الاعلي هو المستوي الذي نقوم باستدعائه مباشرة بدون الحاجة الي معرفة كيف يعمل المستوي الادني. فمثلا اذا كنا نريد اضافة تعليق علي بوست معين هذا هو المستوي الاعلي اما المستوي الادني فهو المسئول عن اضافة هذا التعليق لقاعدة البيانات . و قد يكون المستوي الادني هو مستوي أعلي لمستوي أدني اخر ، ففي هذه الحالة مثلا اضافة التعليق لقاعدة البيانات هو مستوي اعلي لمستوي ادني يقوم بالتحقق من البيانات اولا و هكذا

نجد في هذه المستويات اعتماد المستوي الاعلي علي المستوي الادني و بالتالي يقلل من فرص توسع المشروع scalability و بالتالي نلجأ لمبدأ انعكاس التبعية حيث نقوم بعمل (decoupling) بين المستويات بحيث نقوم باستخدام المستوي الاعلي دون الاعتماد علي نوعية المستوي الادني.

interface Adapter
{
    public function getData();
}
class Mysql implements Adapter
{
    public function getData()
    {
        return 'some data from database';
    }
}
class Controller
{
    private $adapter;
    public function __construct(Mysql $mysql)
    {
        $this->adapter = $mysql;
    }
    function getData()
    {
        $this->adapter->getData();
    }
}

في المثال السابق نجد ان المستوي الاعلي (controller) يعتمد علي المستوي الادني Mysql في اضافة البيانات و لكن مالحل اذا اردنا تغيير قاعدة البيانات ؟!. لذلك سنقوم بتطبيق مبدأ انعكاس التبعية بانشاء واجهة جديدة تمثل قاعدة البيانات من أي نوع يعتمد عليها المستوي الاعلي (controller) و بالتالي نستطيع الوصول الي المستوي الاعلي بدون الاعتماد علي نوع المستوي الادني سواء كان Mysql او غيرها من قواعد البيانات

interface Adapter
{
    public function getData();
}
class Mysql implements Adapter
{
    public function getData()
    {
        return 'some data from database';
    }
}
class Controller
{
    private $adapter;
    public function __construct(Adapter $adapter)
    {
        $this->adapter = $adapter;
    }
    function getData()
    {
        $this->adapter->getData();
    }
}

نصائح

هذا الموضوع مهم جدا ﻷي مبرمج و ننصحك دائما بالتوسع في قراءته و تطبيقه. كما ننصحك بقراءة الموضوع باللغة الانجليزية من الروابط الموجودة في المصادر.

روابط هامة

https://www.thinktocode.com/2017/10/10/solid-principles-in-php/
https://scotch.io/bar-talk/s-o-l-i-d-the-first-five-principles-of-object-oriented-design
https://devdocs.magento.com/guides/v2.4/ext-best-practices/extension-coding/coding-faq.html

This Post Has 0 Comments

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *

Back To Top