چرا اصول SOLID هنوز هم پایه‌های طراحی نرم افزار هستند؟

چرا اصول SOLID هنوز هم پایه‌های طراحی نرم افزار هستند؟

نوشته شده توسط: تیم فنی نیک آموز
تاریخ انتشار: ۱۵ آذر ۱۴۰۰
آخرین بروزرسانی: ۰۷ مرداد ۱۴۰۲
زمان مطالعه: 14 دقیقه
۰
(۰)

مقدمه

باگذشت ۲۰ سال از زمان شکل ‌گیری اصول SOLID، این اصول هنوز هم بهترین روش برای طراحی نرم‌ افزار هستند. اصول SOLID روش ‌های آزمایش شده برای ایجاد نرم‌ا فزار باکیفیت است. اما در دنیای برنامه‌ نویسی چند پارادایم و رایانش ابری، آیا آنها هنوز هم این اصول قابل ‌استفاده هستند؟
در این مقاله من قصد دارم مفهوم SOLID (به معنای واقعی و مجازی) را بررسی کنم، توضیح دهم که چرا هنوز منطقی است، و نمونه‌ هایی از نحوه سازگاری این اصول با محاسبات مدرن را به اشتراک بگذارم.

SOLID چیست؟

SOLID مجموعه ای از اصول است که از نوشته های رابرت سی مارتین در اوایل دهه ۲۰۰۰ استخراج شده است. این اصول به عنوان راهی برای تفکر خاص در مورد کیفیت برنامه نویسی شی گرا (OO) پیشنهاد شد. به طور کلی، اصول SOLID برای اینکه کد چگونه باید تقسیم شود، کدام بخش ها باید داخلی یا در معرض دید قرار گیرند و چگونه کد باید از کدهای دیگر استفاده کند، توضیح می دهد. در زیر به هر کدام از این قسمت ها می پردازم و معنای اصلی آن و همچنین معنای گسترده ای را که می تواند خارج از برنامه نویسی OO اعمال شود را توضیح خواهم داد

چه مواردی از اصول اولیه تغییر کرده است؟

در اوایل دهه ۲۰۰۰، جاوا و سی پلاس پلاس پادشاه بودند. در کلاس ‌های دانشگاه من، جاوا زبان انتخابی بود و در بیشتر درس ‌ها و تمرین ‌ها از این زبان استفاده می ‌کردند. محبوبیت جاوا باعث ایجاد انواع کتاب ‌ها، کنفرانس‌ ها، دوره‌ ها و سایر موارد برای ترغیب مردم از کد نوشتن به سمت کد خوب نوشتن شود.
از آن روز تا به حالا تغییرات صنعت نرم‌افزار بسیار عمیق بوده است. چند مورد از این تغییرات را در زیر بررسی می ‌کنم:·

  • به‌ وجود آمدن زبان‌ های پویا، مانند پایتون، روبی و به‌ویژه جاوا اسکریپت که به ‌اندازه جاوا محبوب شدند و در برخی صنایع و انواع شرکت‌ ها از جاوا پیشی گرفتند.·
  • پارادایم ‌های غیر شی گرا، به ‌ویژه برنامه‌ نویسی تابعی (FPدر زبان ‌های جدید متداول ‌تر هستند. حتی خود جاوا هم لامبدا را معرفی کرد! تکنیک‌ هایی شبیه برنامه ‌نویسی پیشرفته (افزودن و تغییر روش ‌ها و ویژگی ‌های اشیا) نیز محبوبیت پیدا کردند. همچنین وجود افزونه‌ های نرم ‌افزار مانند GO که استاتیک هستند ولی ارث‌ بری ندارند. این مطالب به این معنی است که کلاس‌ ها و ارث ‌بری در نرم ‌افزارهای مدرن نسبت به گذشته اهمیت کمتری دارند.·
  • نرم‌ افزارهای متن ‌باز گسترش یافتند. در حالی که پیش‌ از این، رایج‌ ترین روش نوشتن، نرم‌افزار کامپایل ‌شده منبع بسته برای استفاده توسط مشتریان بود. امروزه بسیار متداول‌ تر است که وابستگی ‌های شما که می ‌خواهید به زبان برنامه‌ نویسی اضافه کنید متن ‌باز باشند. به همین دلیل، نوع الگوریتم و پنهان کردن داده ‌هایی که در زمان نوشتن یک کتابخانه ضروری بود، دیگر اهمیتی ندارد.·
  • میکروسرویس ‌ها و نرم‌ افزارها به‌عنوان یک سرویس همه‌ جا گسترده شدند. به‌ جای نصب یک برنامه کاربردی به ‌عنوان یک فایل اجرایی بزرگ که تمام وابستگی ‌ها را به هم مرتبط می‌ کند، استفاده از یک سرویس کوچک که با سرویس ‌های دیگر، توسط خود شما و یا توسط شخص ثالث اجرا می‌ شود، بسیار رایج ‌تر است.در مجموع، بسیاری از چیزهایی که SOLID واقعاً به آنها اهمیت می ‌داد – مانند کلاس ‌ها و رابط‌ها، پنهان کردن داده‌ ها و چندشکلی – چیزهایی نیستند که امروزه برنامه‌ نویسان هر روز با آن‌ها سروکار دارند.

دوره آموزش معماری میکروسرویس نیک‌آموز

چه چیزی تغییر نکرده است؟

این صنعت در حال حاضر در بسیاری از ویژگی‌ ها متفاوت است، اما برخی ویژگی ‌ها هستند که تغییر نکرده ‌اند و احتمالاً تغییر هم نخواهند کرد. این ویژگی ‌ها شامل موارد زیر هستند:·

  • کد توسط افراد نوشته و اصلاح می ‌شود. کد یک  ‌بار نوشته می ‌شود و بارها و بارها اجرا می ‌شود. همیشه نیاز به مستند سازی کدها، به ‌ویژه مستند سازی کدهای API، داخلی یا خارجی وجود خواهد داشت.·
  • کد در بخش ‌های مختلف (ماژول‌ها) مدیریت می ‌شود. در برخی از زبان‌ ها این کار در داخل کلاس ‌ها انجام می‌ شود. در برخی دیگر، این ماژول‌ ها در فایل‌ های مجزا مدیریت می ‌شوند. در جاوا اسکریپت، امکان دارد توسط اشیا ساخته شوند. به‌ غیراز این روش، راهی برای تفکیک و سازماندهی کد به واحدهای مجزا و محدود وجود دارد؛ بنابراین، همیشه نیاز است تا درباره بهترین روش گروه ‌بندی کدها تصمیم بگیریم وجود دارد.·
  • کد می‌ تواند داخلی یا خارجی باشد. برخی از کدها برای استفاده توسط شما یا تیمتان نوشته شده است. کدهای دیگر برای استفاده توسط تیم‌ های دیگر یا حتی مشتریان خارجی (از طریق یک API) نوشته شده است. این یعنی باید راهی برای تصمیم‌گیری وجود داشته باشد که کدام کد “قابل‌مشاهده” و چه چیزی “پنهان” باشد. 

اصول مدرن SOLID

در بخش ‌های بعدی، من هر یک از پنج اصل SOLID را برای یک عبارت کلی‌ تر که برای برنامه‌ نویسی OO، FP یا چند پارادایم به کار رود، دوباره بیان می‌ کنم و مثال‌ هایی را ارائه می ‌دهم. در بسیاری از موارد، این اصول می‌ تواند برای کل سرویس ‌ها یا حتی سیستم‌ ها اعمال شود!توجه داشته باشید که من از کلمه ماژول در پاراگراف ‌های بعدی برای اشاره به گروه ‌بندی کدها استفاده خواهم کرد. گروه ‌بندی کدها می‌ تواند یک کلاس، یک ماژول، یک فایل و غیره باشد.

(اصل پاسخگویی واحد)Single responsibility principleتعریف اصلی

“هرگز نباید بیش از یک دلیل برای تغییر یک کلاس وجود داشته باشد.”اگر شما یک کلاس بابت تغییرات زیاد می ‌نویسید، سپس باید هر زمان که هر یک از دلایل تغییر کرد، همان کد را تغییر دهید. احتمال اینکه تغییر دادن یک ویژگی به طور تصادفی یک ویژگی دیگر را از بین ببرد، افزایش می ‌دهد. به‌عنوان‌ مثال، در اینجا یک کلاس ساده (franken-class) وجود دارد که هرگز نباید تولید شود:

class Frankenclass {
  public void saveUserDetails(User user) {
     //...
  }
 public void performOrder(Order order) {
     //...
  }
public void shipItem(Item item, String address) {
     //...
  }
 }

تعریف جدید: “هر ماژول باید یک کار را انجام دهد و آن را به‌خوبی انجام دهد.”این اصل ارتباط نزدیکی با موضوع انسجام بالا دارد. به‌طورکلی کد شما نباید چندین نقش یا هدف را با هم ترکیب کند.در قسمت زیر یک ورژن FP از همین مثال با استفاده از جاوا اسکریپت نوشته شده است:

const saveUserDetails = (user) => { ... }
const performOrder = (order) => { ...}
const shipItem = (item, address) => { ... }
export { saveUserDetails, performOrder, shipItem };
// calling code
import { saveUserDetails, performOrder, shipItem } from "allActions";

این اصل می تواند در طراحی میکروسرویس نیز اعمال شود. اگر یک سرویس واحد دارید که هر سه این توابع را کنترل می کند، تلاش میکند که کارهای زیادی انجام دهد.

(اصل باز – بسته)Open-closed principle

تعریف اصلی: “موجودیت‌ های نرم ‌افزار باید برای توسعه باز باشند، اما برای اصلاح بسته باشند.”
این بخشی از طراحی زبان ‌هایی مانند جاوا است – شما می ‌توانید کلاس‌ هایی ایجاد کنید و آنها را گسترش دهید (با ایجاد یک کلاس فرعی)، اما نمی‌توانید کلاس اصلی را تغییر دهید.
یکی از دلیل ‌هایی که باعث می‌شود همه چیز «قابل توسعه باشد» باشد، محدودکردن وابستگی به نویسنده کلاس است – اگر به تغییری در کلاس نیاز دارید، باید دائماً از نویسنده اصلی بخواهید آن را برای شما تغییر دهد، یا اینکه باید آن کلاس را کاملاً درک کنید، تا خودتان آن را تغییر دهید. علاوه بر این، کلاس شروع به ترکیب بسیاری از روابط مختلف می‌کند که اصل مسئولیت واحد را زیر پا می‌گذارد.
بسته‌ شدن کلاس ‌ها برای اصلاح به این دلیل است که ممکن است به هیچ‌کدام از مصرف‌کنندگان پایین‌دستی برای فهمیدن کد «خصوصی» که برای کارکرد ویژگی خود استفاده می‌کنیم اعتماد نداشته باشیم و می ‌خواهیم از آن در برابر افرادی که ماهر نیستند محافظت کنیم.

class Notifier {
   public void notify(String message) {
 		// send an e-mail
   }
 }
 class LoggingNotifier extends Notifier {
   public void notify(String message) {
     super.notify(message); // keep parent behavior
     // also log the message
  }
 }

تعریف جدید: “شما باید بتوانید بدون بازنویسی یک ماژول از آن استفاده و به آن اضافه کنید.”
این تعریف در زمینه شی گرایی به‌راحتی استفاده می‌شود. در دنیای FP، کد شما باید «نقاط قلاب» صریح را تعریف کند تا امکان اصلاح را فراهم شود. در این بخش مثالی هست که در آن نه‌ تنها قبل و بعد از قلاب اجازه تغییر را می ‌دهد، بلکه حتی رفتار پایه را نیز می ‌توان با ارسال یک تابع به تابع خود لغو کرد:

// library code
 const saveRecord = (record, save, beforeSave, afterSave) => {
 const defaultSave = (record) => {
  // default save functionality
}
if (beforeSave) beforeSave(record);
if (save) { save(record);
 }
else {
  defaultSave(record);
  }
 if (afterSave) afterSave(record);
}
// calling code
const customSave = (record) => {... }
saveRecord(myRecord, customSave);

(اصل جایگزینی لیسکوف) Liskov substitution principle

تعریف اصلی: “اگر S زیرنوعی از T باشد، اشیای نوع T را می توان با اشیایی از نوع S جایگزین کرد، بدون اینکه هیچ یک از ویژگی های مطلوب برنامه را تغییر دهد.”
این ویژگی اساسی زبان‌های OO (شی گرا) است. این اصل یعنی شما باید بتوانید از هر زیر کلاس به جای کلاس والد آن استفاده کنید. این اصل اجازه می دهد تا به قرارداد خود اطمینان داشته باشید—شما می توانید با خیال راحت به هر شی که از نوع T وابسته است برای ادامه عملکرد به T وابسته باشید.

class Vehicle {
  public int getNumberOfWheels() {
    return 4;
  }
}
class Bicycle extends Vehicle {
  public int getNumberOfWheels() {
    return 2;
  }
}
 // calling code public
static int COST_PER_TIRE = 50;
public int tireCost(Vehicle vehicle) {
   return COST_PER_TIRE * vehicle.getNumberOfWheels();
  }
Bicycle bicycle = new Bicycle();
System.out.println(tireCost(bicycle)); // 100

تعریف جدید: شما باید بتوانید یک شی را با شی دیگر جایگزین کنید اگر آن شی ها به یک‌ شکل رفتار می‌کنند.
در هنگام کار با زبان‌ های پویا، باید به این نکته توجه کنید که اگر برنامه شما قول انجام کاری را می ‌دهد (مانند اجرای یک رابط یا یک تابع)، باید به قول خود عمل کند و مشتریان را غافلگیر نکند.
بسیاری از زبان ‌های پویا از تایپ اردک (Duck Typing) برای انجام این کار استفاده می‌کنند. اساساً، عملکرد شما اعلام می‌کند که ورودی به قانون خاصی رفتار می‌کند و بر اساس آن فرض پیش می‌رود.
یک مثال با استفاده از Ruby در زیر آورده شده است:

# @param input [#to_s]
def split_lines(input)
input.to_s.split("\n")
 end

در این مورد، تابع به این که نوع ورودی چیست، اهمیت نمی‌دهد – فقط تابع to_s دارد، رفتاری را دارد که قرار است همه توابع to_s رفتار کنند، یعنی ورودی را به یک‌رشته تبدیل می‌کند. بسیاری از زبان‌های پویا راهی برای اجبار این رفتار ندارند، بنابراین این موضوع بیشتر از اینکه تکنیک باشد دررابطه‌با نظم است.یک مثال FP با استفاده از TypeScript در زیر است. در این مورد، یک تابع مرتبه بالاتر، یک تابع فیلتر را فراخوانی می‌کند که ورودی عدد می‌گیرد و یک مقدار بولین را برمی‌گرداند:

const isEven = (x: number) : boolean => x % 2 == 0;
const isOdd = (x: number) : boolean => x % 2 == 1;
const printFiltered = (arr: number[], filterFunc: (int) => boolean) => {
arr.forEach((item) => {
   if (filterFunc(item)) {
    console.log(item);
    }
  })
}
const array = [1, 2, 3, 4, 5, 6]; printFiltered(array, isEven);
printFiltered(array, isOdd);

اصل تفکیک رابط (Interface segregation principle)تعریف اصلی

“به‌ جای استفاده از رابط چندمنظوره بهتر است از چند رابط خاص استفاده شود.”در OO، می‌توانید یک “View” به‌جای کلاس خود در نظر بگیرید. به‌جای اینکه پیاده‌سازی کامل خود را به همه مشتریان خود بدهید، رابط ‌هایی را در بالای کلاس برای مشتری ایجاد می‌کنید و از مشتریان خود می‌خواهید که از آن رابط ‌ها استفاده کنند.همانند اصل مسئولیت واحد، این امر اتصال بین سیستم ‌ها را کاهش می ‌دهد. تضمین می‌کند که مشتری نیازی به دانستن یا وابستگی به ویژگی‌هایی که از آنها استفاده نمی ‌کند را ندارد.در زیر نمونه‌ای آورده شده آزمون SRP را گذرانده است:

class PrintRequest {
  public void createRequest() {}
  public void deleteRequest() {}
  public void workOnRequest() {}
 }

این کد به طور کلی تنها یک «دلیل برای تغییر» خواهد داشت—همه به درخواست ‌های چاپ مربوط می ‌شود، که همگی بخشی از یک دامنه هستند، و هر سه روش احتمالاً وضعیت مشابهی را تغییر می‌ دهند. با این حال، بعید است که همان مشتری که درخواست ایجاد می کند، همان مشتری باشد که روی درخواست ها کار می کند. منطقی تر است که این رابط ها را از هم جدا کنیم:

interface PrintRequestModifier {
   public void createRequest();
  public void deleteRequest();
}
interface PrintRequestWorker {
  public void workOnRequest()
}
 class PrintRequest implements PrintRequestModifier, PrintRequestWorker {
  public void createRequest() {}
  public void deleteRequest() {}
  public void workOnRequest() {}
}

تعریف جدید: «به مشتریان خود بیش از آنچه نیاز است نشان ندهید».فقط آنچه را که مشتری شما باید بداند مستند کنید. این ممکن است به معنای استفاده از تولیدکننده‌های اسناد باشد تا فقط توابع یا مسیرهای «عمومی» را تولید کنند و توابع یا مسیرهای «خصوصی» را حذف کنند.در دنیای میکروسرویس، می‌توانید از اسناد یا جداسازی واقعی برای اعمال وضوح استفاده کنید. به‌عنوان‌مثال، مشتریان خارجی شما ممکن است فقط بتوانند به‌عنوان کاربر وارد سیستم شوند، اما خدمات داخلی شما ممکن است نیاز به دریافت لیستی از کاربران یا ویژگی‌های اضافی داشته باشند. می‌توانید یک سرویس کاربری جداگانه «فقط خارجی» ایجاد کنید که سرویس اصلی شما را فراخوانی می‌کند، یا می‌توانید اسناد خاصی که مسیرهای داخلی را پنهان می‌کنند را فقط برای کاربران خارجی، تولید کنید.

اصل وارونگی وابستگی (Dependency inversion principle)

تعریف اصلی: “به عوامل و ساختار درونی به‌جای ویژگی ‌های ظاهری وابسته است.”
در OO، این بدان معناست که کلاینت‌ها باید تاحدامکان به رابط‌ ها به‌جای کلاس‌ های مشخص وابسته باشند. این اصل تضمین می ‌کند که کد به کوچک ‌ترین سطح ممکن تکیه می ‌کند – در واقع، به ‌هیچ‌ وجه به کد بستگی ندارد، فقط یک قرارداد تعیین می‌ کند که آن کد چگونه باید رفتار کند. مانند سایر اصول، این کار باعث درست شدن یک ماژول می ‌شود اما باعث خطا در جای دیگر به طور تصادفی می‌ شود. در زیر یک مثال ساده آورده شده است:

interface Logger {
  public void write(String message);
}
class FileLogger implements Logger {
  public void write(String message) {
    // write to file
  }
}
class StandardOutLogger implements Logger {
  public void write(String message) {
    // write to standard out
  }
}
public void doStuff(Logger logger) {
   // do stuff
  logger.write("some message")
}

اگر در حال نوشتن کدی هستید که نیاز به تنظیم‌کننده دارد، خود را محدود به نوشتن در فایل‌ها نکنید، زیرا مهم نیست. شما فقط متد نوشتن را فراخوانی می‌کنید و اجازه می‌دهید کلاس اصلی آن را مرتب کند.
تعریف اصلی: “به عوامل و ساختار درونی به‌جای ویژگی‌های ظاهری وابسته است.”
بله تعریف را همان‌طور که هست رها می‌کنم! ایده انتزاعی نگه‌داشتن چیزها در صورت امکان هنوز هم یک ایده مهم است، حتی اگر مکانیسم انتزاعی در کدهای جدید به‌اندازه شی گرایی قوی نباشد.
در عمل، این تقریباً مشابه اصل جایگزینی لیسکوف است که در بالا موردبحث قرار گرفت. تفاوت اصلی این است که در اینجا، هیچ پیاده‌سازی پیش‌فرض وجود ندارد. به همین دلیل، بحث مربوط به تایپ اردک (Duck Type) و توابع قلاب در آن بخش به همان اندازه برای وارونگی وابستگی صدق می‌کند.
شما همچنین می‌توانید انتزاع را در دنیای میکروسرویس اعمال کنید. به‌عنوان‌مثال، می‌توانید ارتباط مستقیم بین سرویس‌ها را با یک گذرگاه پیام یا پلت فرم صف مانند Kafka یا RabbitMQ جایگزین کنید. انجام این کار به سرویس‌ها امکان می‌دهد پیام‌ها را به یک مکان عمومی ارسال کنند، بدون اینکه اهمیتی بدهند کدام سرویس آن پیام‌ها را انتخاب می‌کند و وظیفه خود را انجام می‌دهد.

نتیجه گیری

برای بیان مجدد “SOLID” یک‌بار دیگر:

  •  افرادی که کد شما را می‌خوانند تعجب نکنند.
  •  افرادی که از کد شما استفاده می‌کنند غافلگیر نشوند.
  •  افرادی که کد شما را می‌خوانند گیج نشوند.
  •  از مرزهای منطقی برای کد خود استفاده کنید.
  •  از اتصالات جفت استفاده کنید – چیزهایی را که به هم تعلق دارند کنار هم نگه دارید و اگر از هم جدا هستند آنها را از هم جدا نگه دارید.

کد خوبی است که خوب نوشته شده باشد – این کد تغییر نخواهد کرد، و SOLID یک‌پایه محکم برای نظارت بر آن است!

منبع

https://stackoverflow.blog/2021/11/01/why-solid-principles-are-still-the-foundation-for-modern-software-architecture/

 

چه رتبه ای می‌دهید؟

میانگین ۰ / ۵. از مجموع ۰

اولین نفر باش

title sign
دانلود مقاله
چرا اصول SOLID هنوز هم پایه‌های طراحی نرم افزار هستند؟
فرمت PDF
9 صفحه
حجم 1 مگابایت
دانلود مقاله
title sign
معرفی نویسنده
تیم فنی نیک آموز
مقالات
402 مقاله توسط این نویسنده
محصولات
0 دوره توسط این نویسنده
تیم فنی نیک آموز
title sign
دیدگاه کاربران