اصل طراحی S.O.L.I.D از رهنمودهای برنامه نویسی شی گرا گرفته شده که برای توسعه نرمافزاری طراحی گردیده و به راحتی قابل نگهداری و گسترش است. همچنین تغییرات سریع و بدون اشکال را شامل میشود.
به طور کلی، اصول فنی نتیجه اولویت بندی تحویل سریع نسبت به کد کامل است. برای کنترل آن، از قواعد SOLID در حین توسعه استفاده کنید.
رابرت مارتین، با نوشتن اصول SOLID شناخته میشود و 4 مسئله اصلی نرمافزار را بیان کرد که شامل موارد زیر هستند:
سختی:
اجرای حتی یک تغییر کوچک دشوار است، زیرا احتمالا تبدیل به یک آبشار عظیم از تغییرات است.
ظرافت:
هر تغییری باعث خراب شدن نرمافزار در بسیاری از موارد میشود، حتی در بخشهایی که از نظر مفهومی به تغییر صورت گرفته مربوط نمیشوند.
عدم تحرک:
ما قادر به استفاده مجدد از ماژولهای پروژههای دیگر نیستیم، زیرا این ماژولها وابستگی زیادی دارند.
چسبندگی:
اجرای ویژگیهای جدید به روش صحیح دشوار است.
SOLID یک راهنما است و یک قانون نیست. مهم است که اصل آن را درک کنید و آن را زود قضاوت نکنید. این میتواند زمانی اتفاق بیفتد که تنها چند اصل از تمام اصول مورد نیاز باشد.
SOLID ترکیبی از موارد زیر است:
- اصل مسئولیت واحد - Single Responsibility Principle (SRP)
- اصل open-closed - Open Closed Principle (OCP)
- اصل جایگزینی لیسکوف - Liskov Substitution Principle (LSP)
- اصل تفکیک رابط - Interface Segregation Principle (ISP)
- اصل وارونگی وابستگی - Dependency Inversion Principle (DIP)
اصل مسئولیت واحد
هر تابع، کلاس یا ماژول باید یکی باشد و تنها یک دلیل برای تغییر آن وجود داشته باشد. این بدان معنی است که باید فقط یک کار انجام دهد و درون کلاس قرار بگیرد (انسجام قویتر در کلاس).
این اصل از "تفکیک آشفتگیها" پشتیبانی میکند و میگوید فقط یک کار انجام دهید ولی به خوبی انجام دهید.
به عنوان مثال، این کلاس را در نظر بگیرید:
class Menu {
constructor(dish: string) {}
getDishName() {}
saveDish(a: Dish) {}
}
این کلاس SRP را نقض میکند. در اینجا دلیل آن وجود دارد. این خواص منو و همچنین پایگاه داده را مدیریت میکند. اگر در توابع مدیریت پایگاه داده به روزرسانی وجود داشته باشد، بر توابع مدیریت خواص نیز تأثیر گذار است، بنابراین منجر به تزویج میشود.
نمونه زیر منسجمتر است و کمتر دچار تزویج میشود.
// Responsible for menu management
class Menu {
constructor(dish: string) {}
getDishName() {}
}
// Responsible for Menu management
class MenuDB {
getDishes(a: Dish) {}
saveDishes(a: Dish) {}
}
اصل open-closed
کلاسها، توابع یا ماژولها باید برای توسعهپذیری باز شوند، اما برای اصلاح بسته هستند.
اگر یک کلاس ایجاد و منتشر کردهاید، دادن تغییراتی در این کلاس میتواند اجرای بخشهایی را که شروع به استفاده از این کلاس میکنند، مختل کند.
به عنوان مثال، کلاس زیر را در نظر بگیرید:
class Menu {
constructor(dish: string) {}
getDishName() {}
}
ما میخواهیم لیستی از ظرفها را تکرار کنیم و غذاهای آنها را برگردانیم.
class Menu {
constructor(dish: string){ }
getDishName() { // ... }
getCuisines(dishName) {
for(let index = 0; index <= dishName.length; index++) {
if(dishName[index].name === "Burrito") {
console.log("Mexican");
}
else if(dishName[index].name === "Pizza") {
console.log("Italian");
}
}
}
}
تابع ()getCuisines با اصل open-closed مطابقت ندارد، زیرا نمیتواند در برابر نوع جدیدی از ظروف بسته شود.
اگر یک ظرف جدید به نام Croissant اضافه کنیم، باید تابع را تغییر دهیم و کد جدید را به این ترتیب اضافه کنیم.
class Menu {
constructor(dish: string){ }
getDishName() { // ... }
getCuisines(dishName) {
for(let index = 0; index <= dishName.length; index++) {
if(dishName[index].name === "Burrito") {
console.log("Mexican");
}
if(dishName[index].name === "Pizza") {
console.log("Italian");
}
if(dishName[index].name === "Croissant") {
console.log("French");
}
}
}
}
اگر دقت کنید، برای هر ظرف جدید منطق جدیدی به تابع ()getCuisines اضافه میشود. طبق اصل open-closed، تابع باید برای توسعه باز باشد، نه برای اصلاح.
در اینجا چگونگی ایجاد کد با استاندارد OCP آورده شده است.
class Menu {
constructor(dish: string) {}
getCuisines() {}
}
class Burrito extends Menu {
getCuisine() {
return "Mexican";
}
}
class Pizza extends Menu {
getCuisine() {
return "Italian";
}
}
class Croissant extends Menu {
getCuisine() {
return "French";
}
}
function getCuisines(a: Array<dishes>) {
for (let index = 0; index <= a.length; index++) {
console.log(a[index].getCuisine());
}
}
getCuisines(dishes);
به این ترتیب هر زمان که افزودن یک ظرف جدید لازم باشد، نیازی به تغییر کد نداریم. ما فقط میتوانیم یک کلاس ایجاد کنیم و آن را با کلاس پایه گسترش دهیم.
اصل جایگزینی لیسکوف
یک زیر کلاس باید جایگزین نوع پایهاش شود و بیان میکند که میتوانیم یک زیر کلاس را جایگزین کلاس پایه آن کنیم بدون اینکه بر رفتارش تأثیر بگذارد. از این رو به ما کمک میکند تا مطابق با رابطه "is-a" عمل کنیم.
به عبارت دیگر، زیر کلاسها باید قراردادی را که توسط کلاس پایه تعریف شده است، انجام دهند. از این نظر، مربوط به Design by Contract است که اولین بار توسط برتراند مایر تعریف شد.
به عنوان مثال، منو یک تابع getCuisines دارد که توسطBurrito ،Pizza و Croissant استفاده میشود و توابع منفرد ایجاد نمیکند.
class Menu {
constructor(dish: string) {}
getCuisines(cuisineName: string) {
return cuisineName;
}
}
class Burrito extends Menu {
constructor(cuisineName: string) {
super();
this.cuisine = cuisineName;
}
}
class Pizza extends Menu {
constructor(cuisineName: string) {
super();
this.cuisine = cuisineName;
}
}
class Croissant extends Menu {
constructor(cuisineName: string) {
super();
this.cuisine = cuisineName;
}
}
const burrito = new Burrito();
const pizza = new Pizza();
burrito.getCuisines(burrito.cuisine);
pizza.getCuisines(pizza.cuisine);
اصل تفکیک رابط
کاربر هرگز مجبور به اجرای رابطی نمیشود که از آن استفاده نمیکند و یا مجبور نیست به متدهایی که استفاده نمیکند وابسته باشد.
کلمه "interface" در اصل به معنای دقیق یک رابط نیست، این میتواند یک کلاس انتزاعی باشد.
مثلا
interface ICuisines {
mexican();
italian();
french();
}
class Burrito implements ICuisines {
mexican() {}
italian() {}
french() {}
}
اگر یک متد جدید به interface اضافه کنیم، تمام کلاسهای دیگر باید اعلام کنند که متد یا خطا اجرا میشود.
برای حل آن
interface BurritoCuisine {
mexican();
}
interface PizzaCuisine {
italian();
}
class Burrito implements BurritoCuisine {
mexican();
}
بسیاری از رابطهای کاربری خاص بهتر از یک رابط کاربری عمومی هستند.
اصل وارونگی وابستگی
موجودیتها باید به انتزاع وابسته باشند نه به ذات. این بیان میکند که ماژول سطح بالا نباید به ماژول سطح پایین بستگی داشته باشد، بلکه باید آنها را جدا کرده و از انتزاعات استفاده کند.
ماژولهای سطح بالا بخشی از برنامهای هستند که مشکلات واقعی را حل میکنند و موارد را استفاده میکنند.
آنها انتزاعیتر هستند و به دامنه تجارت (منطق تجارت) ترسیم میشوند.
همچنین به ما میگویند که نرمافزار چه کاری باید انجام دهد (نه چگونه، بلکه فقط چه کاری).
ماژولهای سطح پایین حاوی جزئیات پیادهسازی هستند که برای اجرای سیاستهای تجاری لازم مورد استفاده قرار میگیرند. مثلا
const pool = mysql.createPool({});
class MenuDB {
constructor(private db: pool) {}
saveDishes() {
this.db.save();
}
}
در اینجا کلاس MenuDB یک کامپوننت سطح بالا است، در حالی که متغیر pool یک کامپوننت سطح پایین است. برای حل آن، میتوانیم نمونه ارتباط را از هم جدا کنیم.
interface Connection {
mysql.createPool({})
}
class MenuDB {
constructor(private db: Connection) {}
saveDishes() {
this.db.save();
}
}
سخن پایانی
کدی که S.O.L.I.D را دنبال میکند، با آن میتوان به راحتی اصول را به اشتراک گذاشت، توسعه داد، اصلاح کرد، آزمایش کرد و بدون هیچ مشکلی اجرا میشود. با استفاده از این اصول مزایای این رهنمودها آشکارتر میشود.
پیروی نکردن از اصول و درک نامناسب آن میتواند منجر به نوشتن کدی بیهوده شود: پراکنده بودن، تزویج، عدم هماهنگی، غیر قابل تست، بهینه نبودن، نام گذاری توصیفی و تکثیر را شامل میشود. در نظر داشته باشید SOLID میتواند به توسعه دهندگان کمک کند تا از این موارد دور باشند.
دیدگاه و پرسش
در حال دریافت نظرات از سرور، لطفا منتظر بمانید
در حال دریافت نظرات از سرور، لطفا منتظر بمانید