ما هي مبادئ SOLID؟ ولما يجب أن يعرفها كل مطور؟ (الجزء الثاني)

SOLID 5 principles
SOLID

3. مبدأ "liskov" للاستبدال (LSP).

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

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

لننظر إلى مثال الحيوانات ثانية:

//...
function AnimalLegCount(a: Array<Animal>) {
    for(int i = 0; i <= a.length; i++) {
        if(typeof a[i] == Lion)
            log(LionLegCount(a[i]));
        if(typeof a[i] == Mouse)
            log(MouseLegCount(a[i]));
        if(typeof a[i] == Snake)
            log(SnakeLegCount(a[i])); 
    }
}
AnimalLegCount(animals);
هذا الكود يخرق مبدأ LSPوكذلك مبدأ OCPكما عرفنا من قبل. يجب أن يكون على علم بنوع كل حيوان وبعدها ينادي دالة leg-counting الخاصة به.

وكذلك لجعل الدالة كذلك متوافقة مع LSP سوف نفعل الآتي:

  • إذا كان الصنف الرئيسي Animal يملك دالة تستقبل معامل من نفس النوع Animal فعلى الصنف المشتق أن يستقبل معاملات من كلا النوعين (الصنف الرئيسي والمشتق).
  • إذا كان الصنف الرئيسي animal يعيد return قيم من نفس النوع animal فعلى الصنف المشتق أن يعيد قيم من كلا النوعين.

و الأن نعيد صياغة دالة AnimalLegCount:

function AnimalLegCount(a: Array<Animal>) {
    for(let i = 0; i <= a.length; i++) {
        a[i].LegCount();
    }
}
AnimalLegCount(animals);
والأن لا تهتم الدالة بنوع الحيوان المدخل لها, فإنها تقوم فقط بمناداة الدالة legCount الخاصة به. وكل ما تعرفه أن المعامل المدخل لها يجب أن يكون من النوع Animal, سواء كان من الصنف الرئيسي أو من المشتق.

ويكون الصنف Animal على الشكل:

class Animal {
    //...
    LegCount();
}
والصنوف المشتقة تكون على الشكل:
//...
class Lion extends Animal{
    //...
    LegCount() {
        //...
    }
}
//...
وعندما يمرر الصنف الرئيسي Animal أو المشتق lion إلى الدالة AnimallegCount تعيد عدد الأرجل التي يملكها الحيوان عن طريق مناداة الدالة LegCount الخاصة به.

4. مبدأ فصل الواجهات (ISP).

ويعني صنع واجهات بالغة التحديد والدقة ومناسبة للعميل, فليس عليه فليس عليه الاعتماد على واجهات لا يستخدمها.

وهذا المبدأ يتعامل مع عيوب الواجهات (Interfaces) الكبيرة.

انظر إلى هذا المثال:

interface IShape {
    drawCircle();
    drawSquare();
    drawRectangle();
}
تحتوي هذه الواجهة على ثلاث دوال ترسم دائرة, مربع ومستطيل وعلى الصنوف التي ستستخدمها تعريف الدوال الثلاثة هكذا:
class Circle implements IShape {
    drawCircle(){
        //...
    }
    drawSquare(){
        //...
    }
    drawRectangle(){
        //...
    }

}

class Square implements IShape {
    drawCircle(){
        //...
    }
    drawSquare(){
        //...
    }
    drawRectangle(){ 
        //...
    }
}

class Rectangle implements IShape {
    drawCircle(){
        //...
    }
   drawSquare(){
        //...
    }
   drawRectangle(){
        //...
    }
}

إنه لمن المضحك النظر إلى المثال بالأعلى. فصنف رسم المربع مثلا لديه دوال رسم دائرة ومستطيل والتي ليس لها أي فائدة في الصنف, وكذلك الأصناف الأخرى.

وسوف يحدث الكثير من المشاكل عند إضافة دالة جديدة مثلا:

interface IShape {
    drawCircle();
    drawSquare();
    drawRectangle();
    drawTriangle();
}
ولكن التصميم بهذا الشكل غير عملي ويخرق مبدأ ISP, حيث أن الواجهة لا تؤدي وظيفة واحدة كم أنها تملك دال ليست ذات فائدة لبعض الصنوف اتي تستخدمها.

ولنجعل الواجهة تلائم المبدأ ISP سوف نقسمها إلى عدة واجهات أخرى كالآتي:

interface IShape {
    draw();
}
interface ICircle {
    drawCircle();
}
interface ISquare {
    drawSquare();
}
interface IRectangle {
    drawRectangle();
}
interface ITriangle {
    drawTriangle();
}

class Circle implements ICircle {
    drawCircle() {
        //...
    }
}

class Square implements ISquare {
    drawSquare() {
        //...
    }
}

class Rectangle implements IRectangle {
    drawRectangle() {
        //...
    }
}

class Triangle implements ITriangle {
    drawTriangle() {
        //...
    }
}

class CustomShape implements IShape {
    draw(){ 
        //...
    }
}
وبذلك تكون واجهة ICircle ترسم دوائر فقط وكذلك ISquare وIRectangel أما فيمكنها رسم كل الأشكال. ويمكن بدلا من ذلك أن يرث كل صنف الواجهة IShape ويقوم بعمل دالة Draw الخاصة به كالآتي:
class Circle implements IShape {
    draw(){
        //...
    }
}

class Triangle implements IShape {
    draw(){
        //...
    }
}

class Square implements IShape {
    draw(){
        //...
    }
}

class Rectangle implements IShape {
    draw(){
        //...
    }
}
وبعد ذلك يمكن استخدام الواجهة في إنشاء أي شكل نريد.

5.مبدأ انعكاس التبعية (DIP).

يجب أن تكون التبعيات من المجردات مثل الواجهات حيث:

    أ. لا يجب على الوحدات modules عالية المستوى الاعتماد على وحدات منخفضة المستوى أخرى بل يجب أن يعتمد كلاهما على التجريدات.

    ب. التجريدات لا يجب أن تعتمد على الأصناف بل العكس.

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

انظر إلى الكود الآتي:

class XMLHttpService extends XMLHttpRequestService {}

class Http {
    constructor(private xmlhttpService: XMLHttpService) { }
    get(url: string , options: any) {
        this.xmlhttpService.request(url,'GET');
    }
    post() {
        this.xmlhttpService.request(url,'POST');
    }
    //...
}
وهنا Http هو المكون عالي المستوى أما HttpService هي المكون منخفض المستوى, ولكن هذا البناء يخرق مبدأ DIP حيث لا يجب على المكونات عالية المستوى الاعتماد على أخرى منخفضة المستوى بل على تجريداتها.

وفي المثال أيضا الصنف Http يعتمد على الصنف XMLHttpService وفي حال أردنا تغيير طريقة الاتصال هذه فسوف نضطر الكثير من الكود في كل مرة نريد ذلك وهذ يخرق مبدأ OCP لذا سنقوم بإنشاء الواجهة Connection لحل هذه المشكلة:

interface Connection {
    request(url: string, opts:any);
}
ونعدل الكود ليصبح هكذا:
class Http {
    constructor(private httpConnection: Connection) { }
    get(url: string , options: any) {
        this.httpConnection.request(url,'GET');
    }
    post() {
        this.httpConnection.request(url,'POST');
    }
    //...
}
والأن أيا كن نوع الخدمة فسوف نتمكن من الاتصال من خلالها دون القلق عند تغير نوعها, ويمكننا الأن أن نعيد صياغة الصنف XMLHttpService:
class XMLHttpService implements Connection {
    const xhr = new XMLHttpRequest();
    //...
    request(url: string, opts:any) {
        xhr.open();
        xhr.send();
    }
}
ويمكننا إنشاء أنواع عدة من الاتصالات من الصنف Connection ونمررها للصنف Http:
class NodeHttpService implements Connection {
    request(url: string, opts:any) {
        //...
    }
}

class MockHttpService implements Connection {
     request(url: string, opts:any) {
        //...
    }
}
والأن نرى أن الصنوف عالية المستوى وكذلك منخفضة المستوى يعتمدان على التجريدات فالصنف Http عالي المستوى يعتمد على التجريد Connection وكذلك HttpServiceType منخفض المستوى يعتمد على نفس التجريد.

وكذلك لن يجعلنا هذا البناء نخرق LSP فالصنوف NodeHttpService و الصنف MockHttpService يمكن بسهولة استبدال الصنف الأب Connection.

وبهذا ننتهي من سرد المبادئ الخمسة ل SOLID وشرحها بالتفصيل.


إلى اللقاء.


تعليقات

المشاركات الشائعة من هذه المدونة

30 شيء تمنيت لو عرفتها عندما بدأت في البرمجة(الجزء الأول).

ما هي مبادئ SOLID؟ ولما يجب أن يعرفها كل مطور؟

كيف تصبح مطور ويب وتحصل على وظيفة في أسرع وقت؟ (الجزء الثاني)