اصول طراحی جامد در توسعه نرم افزار


SOLID مجموعه ای از پنج اصل طراحی است. این اصول به توسعه دهندگان نرم افزار کمک می کند تا سیستم های نرم افزاری شی گرا قوی، قابل آزمایش، توسعه پذیر و قابل نگهداری طراحی کنند.
هر یک از این پنج اصل طراحی، مشکل خاصی را که ممکن است هنگام توسعه سیستم های نرم افزاری ایجاد شود، حل می کند.
در این مقاله، من به شما نشان خواهم داد که اصول SOLID شامل چه مواردی است، هر قسمت از مخفف SOLID به چه معناست و چگونه آنها را در کد خود پیاده سازی کنید.
آنچه را پوشش خواهیم داد
اصول طراحی جامد چیست؟
SOLID مخفف عبارت زیر است:
اصل مسئولیت واحد (SRP)
اصل باز-بسته (OCP)
اصل جایگزینی لیسکوف (LSP)
اصل جداسازی رابط (ISP)
اصل وارونگی وابستگی (DIP)
در بخشهای بعدی، معنای هر یک از این اصول را به تفصیل تحلیل خواهیم کرد.
اصل طراحی جامد زیرمجموعه ای از بسیاری از اصول است که توسط دانشمند و مربی آمریکایی، رابرت سی. مارتین (Aka Uncle Bob) در مقاله ای در سال 2000 معرفی شده است.
پیروی از این اصول می تواند منجر به ایجاد یک پایگاه کد بسیار بزرگ برای یک سیستم نرم افزاری شود. اما در دراز مدت، هدف اصلی اصول هرگز شکست نمی خورد. یعنی کمک به توسعهدهندگان نرمافزار برای ایجاد تغییراتی در کد خود بدون ایجاد مشکل اساسی.
اصل مسئولیت واحد (SRP)
اصل مسئولیت واحد بیان می کند که یک کلاس، ماژول یا تابع باید تنها یک دلیل برای تغییر داشته باشد، یعنی باید یک کار را انجام دهد.
به عنوان مثال، کلاسی که نام یک حیوان را نشان می دهد، نباید همان کلاسی باشد که نوع صدای آن و نحوه تغذیه آن را نشان می دهد.
در اینجا یک مثال در جاوا اسکریپت آمده است:
class Animal { constructor(name, feedingType, soundMade) { this.name = name; this.feedingType = feedingType; this.soundMade = soundMade; } nomenclature() { console.log(`The name of the animal is ${this.name}`); } sound() { console.log(`${this.name} ${this.soundMade}s`); } feeding() { console.log(`${this.name} is a ${this.feedingType}`); } } let elephant = new Animal('Elephant', 'herbivore', 'trumpet'); elephant.nomenclature(); // The name of the animal is Elephant elephant.sound(); // Elephant trumpets elephant.feeding(); // Elephant is a herbivore
کد بالا اصل مسئولیت واحد را نقض میکند، زیرا کلاسی که مسئول چاپ نام حیوان است، صدای تولید شده و نوع تغذیه آن را نیز نشان میدهد.
برای رفع این مشکل، باید یک کلاس جداگانه برای روش های صدا و تغذیه مانند زیر ایجاد کنید:
class Animal { constructor(name) { this.name = name; } nomenclature() { console.log(`The name of the animal is ${this.name}`); } } let animal1 = new Animal('Elephant'); animal1.nomenclature(); // The name of the animal is Elephant // Sound class class Sound { constructor(name, soundMade) { this.name = name; this.soundMade = soundMade; } sound() { console.log(`${this.name} ${this.soundMade}s`); } } let animalSound1 = new Sound('Elephant', 'trumpet'); animalSound1.sound(); //Elephant trumpets // Feeding class class Feeding { constructor(name, feedingType) { this.name = name; this.feedingType = feedingType; } feeding() { console.log(`${this.name} is a/an ${this.feedingType}`); } } let animalFeeding1 = new Feeding('Elephant', 'herbivore'); animalFeeding1.feeding(); // Elephant is a/an herbivore
به این ترتیب، هر یک از کلاس ها فقط یک کار را انجام می دهند:
اولی نام حیوان را چاپ می کند
دومی نوع صدایی را که تولید می کند چاپ می کند
و سومی نوع تغذیه خود را چاپ می کند.
این کد بیشتر است، اما خوانایی و نگهداری بهتر است. توسعهدهندهای که کد را ننوشته است، میتواند سریعتر از داشتن همه آن در یک کلاس، به سراغ آن بیاید و بفهمد چه اتفاقی در حال رخ دادن است.
اصل باز-بسته (OCP)
اصل باز-بسته بیان میکند که کلاسها، ماژولها و توابع باید برای توسعه باز باشند اما برای اصلاح بسته باشند.
ممکن است به نظر برسد که این اصل با خودش تناقض داشته باشد، اما همچنان میتوانید آن را در کد معنا کنید. این بدان معناست که شما باید بتوانید عملکرد یک کلاس، ماژول یا تابع را با گفت ن کد بیشتر بدون تغییر کد موجود گسترش دهید.
در اینجا کدهایی وجود دارد که اصل بسته بودن باز را در جاوا اسکریپت نقض می کند:
class Animal { constructor(name, age, type) { this.name = name; this.age = age; this.type = type; } getSpeed() { switch (this.type) { case 'cheetah': console.log('Cheetah runs up to 130mph '); break; case 'lion': console.log('Lion runs up to 80mph'); break; case 'elephant': console.log('Elephant runs up to 40mph'); break; default: throw new Error(`Unsupported animal type: ${this.type}`); } } } const animal1 = new Animal('Lion', 4, 'lion'); animal1.getSpeed(); // Lion runs up to 80mph
کد بالا اصل باز-بسته را نقض میکند، زیرا اگر میخواهید یک نوع حیوان جدید اضافه کنید، باید کد موجود را با اضافه کردن یک مورد دیگر به دستور switch تغییر دهید.
به طور معمول، اگر از دستور switch
استفاده می کنید، به احتمال زیاد اصل باز-بسته را نقض خواهید کرد.
در اینجا نحوه ایجاد مجدد کد برای رفع مشکل آمده است:
class Animal { constructor(name, age, speedRate) { this.name = name; this.age = age; this.speedRate = speedRate; } getSpeed() { return this.speedRate.getSpeed(); } } class SpeedRate { getSpeed() {} } class CheetahSpeedRate extends SpeedRate { getSpeed() { return 130; } } class LionSpeedRate extends SpeedRate { getSpeed() { return 80; } } class ElephantSpeedRate extends SpeedRate { getSpeed() { return 40; } } const cheetah = new Animal('Cheetah', 4, new CheetahSpeedRate()); console.log(`${cheetah.name} runs up to ${cheetah.getSpeed()} mph`); // Cheetah runs up to 130 mph const lion = new Animal('Lion', 5, new LionSpeedRate()); console.log(`${lion.name} runs up to ${lion.getSpeed()} mph`); // Lion runs up to 80 mph const elephant = new Animal('Elephant', 10, new ElephantSpeedRate()); console.log(`${elephant.name} runs up to ${elephant.getSpeed()} mph`); // Elephant runs up to 40 mph
به این ترتیب، اگر میخواهید یک نوع حیوان جدید اضافه کنید، میتوانید یک کلاس جدید ایجاد کنید که SpeedRate
گسترش داده و بدون تغییر کد موجود، آن را به سازنده Animal ارسال کنید.
به عنوان مثال، من یک کلاس GoatSpeedRate
جدید مانند این اضافه کردم:
class GoatSpeedRate extends SpeedRate { getSpeed() { return 35; } } // Goat const goat = new Animal('Goat', 5, new GoatSpeedRate()); console.log(`${goat.name} runs up to ${goat.getSpeed()} mph`); // Goat runs up to 354 mph
این با اصل باز-بسته مطابقت دارد.
اصل جایگزینی لیسکوف (LSP)
اصل جایگزینی Liskov یکی از مهمترین اصولی است که در برنامه نویسی شی گرا (OOP) باید به آن پایبند بود. این توسط دانشمند کامپیوتر باربارا لیسکوف در سال 1987 در مقاله ای که او با ژانت وینگ نوشته بود معرفی شد.
این اصل بیان میکند که کلاسها یا زیر کلاسهای فرزند باید جایگزین کلاسهای والد یا کلاسهای فوقالعاده خود شوند. به عبارت دیگر، کلاس فرزند باید بتواند جایگزین کلاس والد شود. این مزیت را دارد که به شما امکان می دهد بدانید از کد خود چه انتظاری دارید.
در اینجا یک مثال از کدی است که اصل جایگزینی Liskov را نقض نمی کند:
class Animal { constructor(name) { this.name = name; } makeSound() { console.log(`${this.name} makes a sound`); } } class Dog extends Animal { makeSound() { console.log(`${this.name} barks`); } } class Cat extends Animal { makeSound() { console.log(`${this.name} meows`); } } function makeAnimalSound(animal) { animal.makeSound(); } const cheetah = new Animal('Cheetah'); makeAnimalSound(cheetah); // Cheetah makes a sound const dog = new Dog('Jack'); makeAnimalSound(dog); // Jack barks const cat = new Cat('Khloe'); makeAnimalSound(cat); // Khloe meows
کلاس های Dog
and Cat
می توانند با موفقیت جایگزین کلاس والد Animal
شوند.
از سوی دیگر، بیایید ببینیم که چگونه کد زیر اصل جایگزینی Liskov را نقض می کند:
class Bird extends Animal { fly() { console.log(`${this.name} flaps wings`); } } const parrot = new Bird('Titi the Parrot'); makeAnimalSound(parrot); // Titi the Parrot makes a sound parrot.fly(); // Titi the Parrot flaps wings
کلاس Bird
اصل جایگزینی Liskov را نقض می کند زیرا makeSound
خود را از کلاس Animal
والد اجرا نمی کند. در عوض، صدای عمومی را به ارث می برد.
برای رفع این مشکل، باید آن را از روش makeSound
نیز استفاده کنید:
class Bird extends Animal { makeSound() { console.log(`${this.name} chirps`); } fly() { console.log(`${this.name} flaps wings`); } } const parrot = new Bird('Titi the Parrot'); makeAnimalSound(parrot); // Titi the Parrot chirps parrot.fly(); // Titi the Parrot flaps wings
اصل جداسازی رابط (ISP)
اصل تفکیک واسط بیان می کند که کلاینت ها نباید مجبور به پیاده سازی رابط ها یا روش هایی شوند که استفاده نمی کنند.
به طور خاص، ISP پیشنهاد میکند که توسعهدهندگان نرمافزار باید رابطهای بزرگ را به واسطهای کوچکتر و خاصتر تقسیم کنند، بهطوریکه کلاینتها فقط باید به رابطهای مرتبط با آنها وابسته باشند. این میتواند حفظ پایگاه کد را آسانتر کند.
این اصل تقریباً مشابه اصل مسئولیت واحد (SRP) است. اما این فقط در مورد این نیست که یک اینترفیس تنها یک کار را انجام دهد - بلکه در مورد شکستن کل پایگاه کد به چندین رابط یا مؤلفه است.
در مورد این همان کاری فکر کنید که هنگام کار با فریمورکها و کتابخانههای frontend مانند React، Svelte و Vue انجام میدهید. شما معمولاً پایگاه کد را به اجزایی تقسیم می کنید که فقط در صورت نیاز وارد می شوید.
این بدان معنی است که شما اجزای جداگانه ای را ایجاد می کنید که عملکردی خاص برای آنها دارد. برای مثال، مؤلفهای که مسئول پیادهسازی اسکرول به بالا است، چیزی نیست که بین روشن و تاریک و غیره جابهجا شود.
در اینجا یک مثال از کدی است که اصل جداسازی رابط را نقض می کند:
class Animal { constructor(name) { this.name = name; } eat() { console.log(`${this.name} is eating`); } swim() { console.log(`${this.name} is swimming`); } fly() { console.log(`${this.name} is flying`); } } class Fish extends Animal { fly() { console.error("ERROR! Fishes can't fly"); } } class Bird extends Animal { swim() { console.error("ERROR! Birds can't swim"); } } const bird = new Bird('Titi the Parrot'); bird.swim(); // ERROR! Birds can't swim const fish = new Fish('Neo the Dolphin'); fish.fly(); // ERROR! Fishes can't fly
کد بالا اصل جداسازی رابط را نقض می کند زیرا کلاس Fish
به روش fly
نیاز ندارد. ماهی نمی تواند پرواز کند. پرندگان نیز نمی توانند شنا کنند، پس کلاس Bird
به روش swim
نیازی ندارد.
به این صورت است که من کد را برای مطابقت با اصل تفکیک رابط ثابت کردم:
class Animal { constructor(name) { this.name = name; } eat() { console.log(`${this.name} is eating`); } swim() { console.log(`${this.name} is swimming`); } fly() { console.log(`${this.name} is flying`); } } class Fish extends Animal { // This class needs the swim() method } class Bird extends Animal { // THis class needs the fly() method } // Making them implement the methods they need const bird = new Bird('Titi the Parrot'); bird.swim(); // Titi the Parrot is swimming const fish = new Fish('Neo the Dolphin'); fish.fly(); // Neo the Dolphin is flying console.log('\n'); // Both can also implement eat() method of the Super class because they both eat bird.eat(); // Titi the Parrot is eating fish.eat(); // Neo the Dolphin is eating
اصل وارونگی وابستگی (DIP)
اصل وارونگی وابستگی مربوط به جداسازی ماژول های نرم افزاری است. یعنی تا حد امکان آنها را از یکدیگر جدا کنید.
این اصل بیان می کند که ماژول های سطح بالا نباید به ماژول های سطح پایین وابسته باشند. در عوض، هر دو باید به انتزاعات وابسته باشند. علاوه بر این، انتزاع ها نباید به جزئیات بستگی داشته باشند، اما جزئیات باید به انتزاع ها بستگی داشته باشند.
به عبارت ساده تر، این بدان معناست که به جای نوشتن کدی که بر جزئیات خاصی از نحوه عملکرد کدهای سطح پایین متکی است، باید کدی بنویسید که به انتزاعات کلی تری بستگی دارد که می توانند به روش های مختلف پیاده سازی شوند.
این کار تغییر کدهای سطح پایین را بدون نیاز به تغییر کدهای سطح بالاتر آسان تر می کند.
در اینجا کدی وجود دارد که اصل وارونگی وابستگی را نقض می کند:
class Animal { constructor(name) { this.name = name; } } class Dog extends Animal { bark() { console.log('woof! woof!! woof!!'); } } class Cat extends Animal { meow() { console.log('meooow!'); } } function printAnimalNames(animals) { for (let i = 0; i < animals.length; i++) { const animal = animals[i]; console.log(animal.name); } } const dog = new Dog('Jack'); const cat = new Cat('Zoey'); const animals = [dog, cat]; printAnimalNames(animals);
کد بالا اصل وارونگی وابستگی را نقض می کند زیرا تابع printAnimalNames
به پیاده سازی های مشخص Dog and Cat بستگی دارد.
اگر میخواهید حیوان دیگری مانند میمون را اضافه کنید، باید تابع printAnimalNames
را برای مدیریت آن تغییر دهید.
در اینجا نحوه رفع آن آمده است:
class Animal { constructor(name) { this.name = name; } getName() { return this.name; } } class Dog extends Animal { bark() { console.log('woof! woof!! woof!!!'); } } class Cat extends Animal { meow() { console.log('meooow!'); } } function printAnimalNames(animals) { for (let i = 0; i < animals.length; i++) { const animal = animals[i]; console.log(animal.getName()); } } const dog = new Dog('Jack'); const cat = new Cat('Zoey'); const animals = [dog, cat, ape]; printAnimalNames(animals);
در کد بالا، یک متد getName
در کلاس Animal ایجاد کردم. این انتزاعی را فراهم می کند که تابع printAnimalNames
می تواند به آن وابسته باشد. اکنون، تابع printAnimalNames
فقط به کلاس Animal
بستگی دارد، نه پیادهسازی بتن Dog
and Cat
.
اگر می خواهید یک کلاس Ape اضافه کنید، می توانید بدون تغییر تابع printAnimalNames
این کار را انجام دهید:
class Animal { constructor(name) { this.name = name; } getName() { return this.name; } } class Dog extends Animal { bark() { console.log('woof! woof!! woof!!!'); } } class Cat extends Animal { meow() { console.log('meooow!'); } } // Add Ape class class Ape extends Animal { meow() { console.log('woo! woo! woo!'); } } function printAnimalNames(animals) { for (let i = 0; i < animals.length; i++) { const animal = animals[i]; console.log(animal.getName()); } } const dog = new Dog('Jack'); // Jack const cat = new Cat('Zoey'); // Zoey // Use the Ape class const ape = new Ape('King Kong'); // King Kong const animals = [dog, cat, ape]; printAnimalNames(animals);
نتیجه
در این مقاله، اصول طراحی SOLID را یاد گرفتید. ما هر اصل را با یک مثال مورد بحث قرار دادیم و راه هایی را برای پیاده سازی آنها در جاوا اسکریپت تحلیل کردیم.
امیدوارم این مقاله درک کاملی از اصول SOLID به شما بدهد. می بینید که اصول طراحی SOLID می تواند به شما کمک کند تا یک سیستم نرم افزاری بدون اشکال، قابل نگهداری، انعطاف پذیر، مقیاس پذیر و قابل استفاده مجدد ایجاد کنید.
با تشکر برای خواندن.
ارسال نظر