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

 
الصورة www.dereuromark.de



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

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

ولهذا فقد طورت خمسة مبادئ رئيسية من قبل "Robert C. Martin", وهذه المبادئ الخمسة جعلت من السهل إنشاء برامج سهلة القراءة والإصلاح. وهذه المبادئ الخمسة تسمى "S.O.L.I.D".

  • S: مبدأ المسئولية الواحدة (Single Responsibility principle).
  • O: مبدأ المفتوح المغلق (Open-Closed principle).
  • L: مبدأ "liskov" للاستبدال (Liskov Substitution Principle).
  • I: مبدأ فصل الواجهات (Interface Segregation Principle).
  • D: مبدأ انعكاس التبعية (Dependency Inversion Principle).

وسوف نناقش كل مبدأ على حدا بالتفصيل.

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

حسنا فلنبدأ بالمبدأ الأول.

  • مبدأ المسئولية الواحدة (SRP).
ويعني أنه على كل صنف (Class) القيام بوظيفة واحدة. فإذا كانت له أكثر من وظيفة واحدة أصبح مزدوجا, أي أن التغيير في احدى وظائفه ينتج عنه بالتبعية تغيير في الأخرى.

وهذا المبدأ لا ينطبق على الأصناف فقط بل على أشياء أخرى مثل المكونات (Components) والخدمات المصغرة (MicroServices) أيضا.

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


class Animal {
    constructor(name: string){ }
    getAnimalName() { }
    saveAnimal(a: Animal) { }
}
فصنف (Animal) يحرق مبدأ SRP, ولكن كيف؟

ينص هذا المبدأ على أن كل صنف يجب أن يكون له وظيفة أو مسئولية وحيدة, ولكن في هذا المثال يمكن أن نعد وظيفتان:

إدارة خصائص الصنف وإدارة قاعدة البيانات حيث أن المكون "getAnimalName" يدير خصائص الصنف (Animal) و الدالة "saveAnimal" قاعدة البيانات الخاصة به.

ولكن كيف سيسبب هذا التصميم مشاكل في المستقبل؟

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

إنها مثل قطع الدومينو, المس أحداها وسوف تتأثر الأخريات.

ولنجعل هذا الصنف متلاءم مع القاعدة. ننشئ صنف آخر والذي سوف يكون له وظيفة واحه وهي إدارة قاعدة البيانات, وسيصبح الصنف على الشكل.


class Animal {
    constructor(name: string){ }
    getAnimalName() { }
}
class AnimalDB {
    getAnimal(a: Animal) { }
    saveAnimal(a: Animal) { }
}
"عند تصميم الصنوف نهدف إلى وضع الميزات المتشابهة مع بعضها, لذا عندما تتغير فإنها تتغير لنفس السبب, ويجب فصل الميزات التي تتغير لأسباب مختلفة" - steve fenton

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

  • مبدأ المفتوح المغلق (OCP).
ويعني أن مكونات البرمجيات (الصنوف, الدوال والوحدات البرمجية) يجب أن تكون قابلة للتمديد وليس التعديل.

لنكمل مع صنف Animal.


class Animal {
    constructor(name: string){ }
    getAnimalName() { }
}
إذا أردت أن تمر على قائمة من ال Animals وتطبع صوت كلا منهم فيمكنك عمل هذا.
//...
const animals: Array<Animal> = [
    new Animal('lion'),
    new Animal('mouse')
];
function AnimalSound(a: Array<Animal>) {
    for(int i = 0; i <= a.length; i++) {
        if(a[i].name == 'lion')
            log('roar');
        if(a[i].name == 'mouse')
            log('squeak');
    }
}
AnimalSound(animals);

ولكن هذه الدال ة لا تتوافق مع القاعدة OCP. فلو أردنا أن نضيف حيوان جديد مثلا.

//...
const animals: Array<Animal> = [
    new Animal('lion'),
    new Animal('mouse'),
    new Animal('snake')
]
//...
 فيجب علينا أن نعدل الدالة "AnimalSound" وإضافة الحيوان الجديد.

//...
function AnimalSound(a: Array<Animal>) {
    for(int i = 0; i <= a.length; i++) {
        if(a[i].name == 'lion')
            log('roar');
        if(a[i].name == 'mouse')
            log('squeak');
        if(a[i].name == 'snake')
            log('hiss');
    }
}
AnimalSound(animals);
ترى هنا أنه علينا اضافة كل حيوان جديد داخل الدالة, ولكن هذا مثال بسيط, فعندما يصبح البرنامج كبيرا ومعقدا سترى أن جملة IF الشرطية تتكرر عدد كبير من المرات, ولكن هذا شيء غير عملي. فكيف إذا نجعل هذا الكود متوافق مع ال OCP؟

class Animal {
        makeSound();
        //...
}class Lion extends Animal {
    makeSound() {
        return 'roar';
    }
}class Squirrel extends Animal {
    makeSound() {
        return 'squeak';
    }
}
class Snake extends Animal {
    makeSound() {
        return 'hiss';
    }
}//...
function AnimalSound(a: Array<Animal>) {
    for(int i = 0; i <= a.length; i++) {
        log(a[i].makeSound());
    }
}
AnimalSound(animals);
في المثال أعلاه أستخدمنا ما يسمى بالوراثة, فلدينا الأن صنف رئيسي animal وبه دالة تخيلية "makeSound" ويقوم بعدها صنف كل حيوان بأخذ animal كصنف أب ويقوم بعدها بإعادة تعريف الدالة لتعيد الصوت الخاص به, وبعدها تقوم "AnimalSound" بالمرور على قائمة الحيوانات وتنفذ دالة makesound الخاصة بكل منهم.

الآن عند إضافة حيوان جديد لن نحتاج لتعديل أي شيء و لا لإضافة المزيد من الكود داخل الدالة. والأن الدالة AnimalSound تتناسب مع مبدأ OCP.

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

class Discount {
    giveDiscount() {
        return this.price * 0.2
    }
}
 في المثال أعلاه إذا أردت أن تغطي خصما بمقدار 40% لمستخدمي VIP فقد تعدل الصنف بهذا الشكل:

class Discount {
    giveDiscount() {
        if(this.customer == 'fav') {
            return this.price * 0.2;
        }
        if(this.customer == 'vip') {
            return this.price * 0.4;
        }
    }
}
ولكن هذا الشكل يخرق مبدأ OCP فإذا أردنا أن نضيف خصما آخر لنوع آخر من الزبائن, فسوف نضيف المزيد من الكود بداخل الدالة كما في المثال السابق.

فأما إذا أردناه أن يتوافق مع OCP فسنقوم بعمل صنف آخر يرث Discount ونحدد فيه قيمة الخصم المراد.


class VIPDiscount: Discount {
    getDiscount() {
        return super.getDiscount() * 2;
    }
}
فمثلا إذا أردنا خصم 80% لمستخدمي VIP فسيكون على الشكل:

class SuperVIPDiscount: VIPDiscount {
    getDiscount() {
        return super.getDiscount() * 2;
    }
}
ترى هنا كيف قمنا بالتمديد للصنف دون التعديل عليه عند كل إضافة.

انتظر المزيد في المقالات القادمة من السلسلة للتعرف على المبادئ الثلاثة الأخرى.


إلى اللقاء. 



تعليقات

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

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

هذه هي العادات الخمسة للمطورين الناجحين.