
اصول SOLID یک اصول قانون مند در برنامه نویسی شی گرا هستند که در تمام زبان های برنامه نویسی شی گرا موجود و قابل پیاده سازی است .
کلمه SOLID مخفف پنج اصل بسیار مهم در مدیریت وابستگی (Dependency Management) در توسعه ی برنامه های شی گرا می باشد. در واقع هر کدام از حروف کلمه ی SOLID به یکی از این اصول بر می گردد.
یکی از مشکلاتی که طراحی نامناسب برنامه های شی گرا برای برنامه نویسان ایجاد می کند موضوع مدیریت وابستگی در اجزای برنامه می باشد. اگر این وابستگی به درستی مدیریت نشود مشکلاتی شبیه موارد زیر در برنامه ایجاد می شوند:
اصول SOLID که قصد رفع کردن این مشکلات و بسیاری مسائل گوناگون را دارد عبارت اند از:
با کنار هم گذاشتن حرف اول هر کدام از این اصول کلمه ی SOLID ایجاد می شود. با در نظر گرفتن این پنج اصل و پیاده سازی آنها در برنامه های خود می توانید به یک طراحی شی گرای پاک و درست دست پیدا کنید.
Single-responsibility Principle (SRP) states:
A class should have one and only one reason to change, meaning that a class should have only one job.
نقل قول زیر توضیح رسمی هست که برای SRP ارائه شده:
یک کلاس فقط باید به یک دلیل تغییر کنه.
این اصل به ما میگه که هر کلاسی که توی برنامهی ما وجود داره، باید یک مسئولیت خاص و مشخص داشته باشه. در واقع این کلاس باید فقط و فقط مسئول یک عملکرد توی برنامه باشه.
این جمله رو همه شنیدیم: یک کار انجام بده ولی درست انجام بده!
Open-closed Principle (OCP) states:
Objects or entities should be open for extension but closed for modification.
تعریف رسمی این اصل به این صورت هست:
موجودیتهای یک نرمافزار (کلاسها، ماژولها، توابع و ...) باید برای توسعه داده شدن، باز و برای تغییر دادن، بسته باشند.
توی این اصل از کلمههای باز و بسته استفاده شده. این کلمات با چیزی که توی ذهنمون داریم یکم متفاوت هست. اول بذارید معنی کلاس باز و بسته رو با هم بررسی کنیم و بعد به توضیح این اصل بپردازیم.
به کلاسی که بشه اون رو توسعه داد، بشه از اون extend کرد، متدها و پراپرتیهای جدید اضافه کرد و ویژگیها و رفتار اون رو تغییر داد، میگن باز.
کلاسی که کامل باشه. یعنی 100% تست شده باشه که بتونه توسط بقیه کلاسها استفاده بشه، پایدار باشه و در آینده تغییر نکنه، میگن بسته. توی بعضی از زبانهای برنامهنویسی یکی از راههای بسته نگه داشتن یک کلاس، استفاده از کلمه کلیدی final هست.
خب حالا بپردازیم به توضیح اصل OCP:
اصل OCP میگه که ما باید کد رو جوری بنویسیم که وقتی میخوایم اون رو توسعه بدیم و ویژگیهای جدید اضافه کنیم، مجبور نشیم اون رو تغییر بدیم و دستکاری کنیم. ویژگیهای جدید باید براحتی و بدون دستکاری کردن قسمتهای دیگه اضافه بشن.
طبق این اصل کلاس باید همزمان هم بسته باشه و هم باز! یعنی همزمان که توسعه داده میشه (باز بودن)، تغییر نکنه و دستکاری نشه (بسته بودن).
Liskov Substitution Principle states:
Let q(x) be a property provable about objects of x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T.
تعریف آکادمیک این اصل بصورت زیر هست:
اگر S یک زیر کلاس از T باشه، آبجکتهای نوع T باید بتونن بدون تغییر دادن کد برنامه با آبجکتهای نوع S جایگزین بشن
پس باید در نظر داشته باشیم وقتی که میخواهیم یک کلاس رو با مشتق کردن توسعه بدیم، جاهایی از برنامه که از کلاس والد استفاده شده، باید بتونه بدون مشکل با کلاسهای فرزند هم کار کنه. یعنی کلاس فرزند نباید ویژگیها و رفتار کلاس والد رو تغییر بده. مثلا اگه کلاس والد یک متد داره که خروجی اون عددی هست، کلاس فرزند نباید این متد رو جوری رونوشت کنه که خروجی آرایه باشه.
The interface segregation principle states:
A client should never be forced to implement an interface that it doesn’t use, or clients shouldn’t be forced to depend on methods they do not use.
توضیح رسمی و آکادمیک این اصل بصورت زیر هست:
کلاسها نباید مجبور باشن متدهایی که به اونها احتیاجی ندارن رو پیادهسازی کنن
این اصل میگه که ما باید اینترفیس (Interface) ها رو جوری بنویسیم که وقتی یک کلاس از اون استفاده میکنه، مجبور نباشه متدهایی که لازم نداره رو پیادهسازی کنه. یعنی متدهای بیربط نباید توی یک اینترفیس کنار هم باشن. در این مواقع باید اون اینترفیس، به دو یا چند اینترفیس دیگه شکسته بشه. این اصل شباهت زیادی به اصل اول SOLID داره که میگه کلاسها باید فقط مسئول انجام یک کار باشن.
رعایت کردن این اصل به ما کمک میکنه کدهای خواناتر و تمیزتری داشته باشیم. توی شیگرایی باید یک نکته رو درنظر داشته باشیم که هر چی از کلینویسی (عمومینویسی) دوری کنیم و کدهایی داشته باشیم که مجزا و تفکیک شده باشن، برنامهای منسجمتر و ساختاریافتهتر خواهیم داشت. بنابراین کدها قابل استفاده مجدد میشن، تست و Refactor هم راحتتر انجام میشه 👌
Dependency inversion principle states:
Entities must depend on abstractions, not on concretions. It states that the high-level module must not depend on the low-level module, but they should depend on abstractions.
توضیح رسمی و آکادمیک این اصل به صورت زیر هست. این توضیح رو بخونید تا با هم ریز به ریز جزییاتش رو بررسی کنیم:
کلاسهای سطح بالا نباید به کلاسهای سطح پایین وابسته باشن؛ هر دو باید وابسته به انتزاع (Abstractions) باشن. موارد انتزاعی نباید وابسته به جزییات باشن. جزییات باید وابسته به انتزاع باشن
خب دوستان این توضیحی بود که خیلی آکادمیک و یکم گنگ هست. مواردی مثل کلاس سطح بالا و سطح پایین، انتزاع و جزییات مواردی هستن که باید روشن بشن تا بتونیم این اصل رو خوب درک کنیم.
به کلاسهایی گفته میشه که مسئول عملیات اساسی و پایهای توی نرمافزار هستن. مثل کلاسی که با دیتابیس یا هارددیسک ارتباط برقرار میکنه، کلاسی که برای ارسال ایمیل استفاده میشه و ...
کلاسهایی که عملیات پیچیدهتر و خاصتری انجام میدن و برای انجام این کار از کلاسهای سطح پایین استفاده میکنن. برای مثال کلاس گزارشگیری برای ثبت و خوندن گزارش، به کلاس دیتابیس یا هارددیسک نیاز داره. کلاس Users، برای اطلاعرسانی به کاربرها به کلاس ایمیل نیاز داره.
کلاسهای انتزاعی کلاسهای هستن که قابل پیادهسازی نیستن اما به عنوان یک طرح و الگو برای کلاسهای دیگه در نظر گرفته میشن. مثلا یک کلاس انتزاعی برای گربه، زرافه، پلنگ و پنگوئن، میشه کلاس Animal. خود Animal به خودی خود قابل پیادهسازی نیست. بلکه یک طرح کلی برای حیوونایی هستن که مثال زدیم. پس تک تک این حیوونها یک ورژن کلیتر دارن که میتونیم اون رو Animal بنامیم.
منظور از جزییات توی تعریف این اصل، جزییات یک کلاس مثل نام و ویژگی پراپرتیها و متدهاست.
خب بپردازیم به بررسی این اصل. ابتدا کد زیر رو در نظر بگیرید:
class MySql { public insert() {} public update() {} public delete() {} } class Log { private database: MySql; constructor() { this.database = new MySql; } }
فرض کنیم یک کلاس سطح پایین داریم مثلا دیتابیس MySql. و یک سری کلاس سطح بالا مثلاً گزارشگیری (Log) از این کلاس استفاده میکنه. اگه بخوایم یک تغییر توی کلاس دیتابیس انجام بدیم، ممکنه بطور مستقیم تاثیر بذاره روی کلاسهایی که ازش استفاده میکنن. مثلا اگه توی کلاس MySql اسم متد رو تغییر بدیم و یا پارامترها رو کم و زیاد کنیم، نهایتا توی کلاس Log این تغییرات رو باید اعمال کنیم.
همچنین کلاسهای سطح بالا قابل استفاده مجدد نیستن. مثلاً اگه بخوایم برای کلاس Log از دیتابیسهای دیگه مثلا MongoDB یا هارددیسک استفاده کنیم باید کلاس Log رو تغییر بدیم یا یک کلاس جدا براساس هر نوع دیتابیس بسازیم.
خب همونطور که میبینید اگه یک کلاس سطح بالا وابسته به یک کلاس سطح پایین باشه این مشکلات به وجود میاد.
برای حل این مشکل باید با اینترفیس، یک لایه انتزاعی درست کنیم. با این کار کلاس Log دیگه وابسته به یک کلاس خاص برای ذخیرهسازی و خوندن اطلاعات نیست و میتونیم هر نوع دیتابیسی رو استفاده کنیم و برای کلاس Log اهمیتی نداره که با چه نوع دیتابیسی داره کار میکنه. چون وابسته به انتزاع هست.
ابتدا یک اینترفیس میسازیم برای اینکه کلاسهای سطح بالا و سطح پایین رو وابسته به این اینترفیس کنیم:
interface Database { insert(); update(); delete(); }
حالا کلاسهای سطح پایین باید این اینترفیس رو پیادهسازی کنن تا وابسته به انتزاع بشن:
class MySql implements Database { public insert() {} public update() {} public delete() {} } class FileSystem implements Database { public insert() {} public update() {} public delete() {} } class MongoDB implements Database { public insert() {} public update() {} public delete() {} }
و نهایتاً توی کلاسهای سطح بالا، وابستگی به یک کلاس خاص رو به اینترفیس واگذار میکنیم. کلاسهای سطح بالا زمانی وابسته به انتزاع میشن که بجای استفاده مستقیم از کلاسهای سطح پایین، از یک اینترفیس (رابط) استفاده کنن:
class Log { private db: Database; public setDatabase(db: Database) { this.db = db; } public update() { this.db.update(); } }
همونطور که میبینیم وابستگی به یک کلاس خاص از بین رفت و میتونیم هر نوع دیتابیسی رو برای کلاس Log استفاده کنیم:
logger = new Log; logger.setDatabase(new MongoDB); // ... logger.setDatabase(new FileSystem); // ... logger.setDatabase(new MySql); logger.update();