خوب در ابتدا بذارید یه بررسی داشته باشیم ببینیم SOLID چی هست و چه فایده ای میتونه برامون داشته باشه؟
به زبون ساده بخوایم بگیم SOLID یه مجموعه از قوانین برنامه نویسی در زبان های شی گراست که به ما کمک میکنه بتونیم قاعده مندتر و بهتر کلاس ها و توابع داخلشون رو تعریف کنیم تا هم کدمون خواناتر بشه و هم توسعه پروژه در آینده بهتر و کم چالش تر باشه برامون.
حالا شاید بگید مگه بدون این اصول نمیشه کد زد برنامه نویسی کرد؟ در پاسخ باید بگم چرا میشه اما ببینید برنامه نویسی یه کار مهندسی هست، یعنی مثل سایر رشته های مهندسی، اگه یه سری اصول و قوانین رو بدونی و تو کارت پیاده سازی کنی، کیفیت و ارزش کارت رو بالاتر میبره. اینجا هم همینطوره!
پس توصیه میکنم اگه قصد داری برنامه های تجاری تولید کنی که آینده روشنی براشون در نظر بگیریم، حتما به این نکات یه نیم نگاهی داشته باش و سعی کن خوب یادشون بگیری. پشیمون نمیشی :)
خوب کلا SOLID از ۵ اصل ساخته شده که حروف اون هر کدوم اشاره به یه اصل دارن. پس ابتدا از حرف S شروع میکنیم ببینیم منظورش چیه؟
تو این قانون میگه هر کلاس باید تنها یه وظیفه مشخص داشته باشه و پیش ببره. به عبارت دیگه همه کارها رو قرار نیست رو دوش یه کلاس بذاریم که برامون انجام بده :) نمونه کد زیر و ببینید خودتون متوجه میشید:
// Without SRP class User { constructor(name) { this.name = name; } sendSMS() { // Send welcome sms to the user } sendEmail() { // Send welcome email to the user } }
خوب همونطوری که میبینید اینجا SMS و Email رو لزومی نداره کلاس User انجام بده، پس بهتره این کار رو بسپاریم به دست کلاس های مختص به خودش:
// With SRP class User { constructor(name) { this.name = name; } } class SMSService { sendSMS(user) { // Send welcome sms to the user } } class EmailService { sendWelcomeEmail(user) { // Send welcome email to the user } }
حالا هر کلاس وظیفه خودش و به درستی انجام میده و کاری به بقیه نداره. چقدر کدمون مرتب تر شد! :)
این اصل میگه موجودیت های نرم افزاری مون مثل کلاس ها، باید برای توسعه باز باشن و برای اصلاح بسته! یعنی اگه کلاسی نوشتیم که به خوبی تعریف شده بود، نباید به خاطر توسعه های آتی سیستم، دائم کلاس رو اصلاح کنیم و تغییرش بدیم. اجازه بدید بریم روی مثال زیر تا واضح تر این مسئله دیده بشه:
// Without OCP class Square { constructor(side) { this.side = side; } } class AreaCalculator { calculateSquareArea(square) { return square.side * square.side; } }
خوب الان اینجا ما یه کلاس Square ایجاد کردیم، واسه محاسبه مساحت باید یه نمونه ازش بسازیم و به عنوان پارامتر به تابع کلاس AreaCalculator پاس بدیم. خوب به نظر بد نیست اما مشکل از اینجا شروع میشه که اگه شکل های دیگه مثل دایره، مستطیل و ... داشتیم، برای هر شکل که جدید به مجموعه اضافه بشه، باید این کلاس رو اصلاح کنیم و یه متد جدید براش بنویسیم! راه حل این مشکل رو در ادامه آوردم:
// With OCP class Shape { calculateArea() { } } class Square extends Shape { constructor(side) { super(); this.side = side; } calculateArea() { return this.side * this.side; } } class Circle extends Shape { constructor(radius) { super(); this.radius = radius; } calculateArea() { return Math.PI * this.radius * this.radius; } }
حالا Shape یه Abstraction کلاس شد و با ارث بری تونستیم این مشکل رو حل کنیم! حالا هر چی شکل دیگه اضافه بشه، ما اونو از کلاس Shape ارث بری میکنیم و بعد متد calculateArea رو داخلش پیاده سازی می کنیم. پس با این کار دیگه کلاس هامون با توسعه پروژه دائم نمیخواد اصلاح و بروز رسانی بشن که شد همون اصل دوم که دنبالش بودیم :)
این اصل میگه متدها و آبجکت هایی که در سوپر کلاس استفاده میکنیم، نباید با زیر کلاس هایی که اون رو ارث بری میکنن مغایرت داشته باشه. به عبارت دیگه هر متدی که در کلاس Parent میاد، نباید یا عملکرد کلاس های child در تضاد باشه. به مثال زیر توجه کنید تا موضوع روشن تر بشه برامون:
// Without LSP class Bird { fly() { // Fly logic } } class Penguin extends Bird { // Penguins can't fly fly() { throw new Error('Penguins can\'t fly'); } }
خوب تو این مثال ببینید تابع fly توی کلاس والدمون (Parent) که Bird هست اومده. خوب مشکلی که داره درست میکنه اینه که اگه پنگوئن ها رو بخوایم از این کلاس ارث بری کنیم، دیگه متد fly موجود در کلاس bird نمیتونه جوابگو باشه! پس در واقع اون متد غلط شد که نباید اینجا باشه! پس حالا بریم ببینیم راه حلش چیه
// With LSP class Bird { move() { } } class FlyingBird extends Bird { fly() { // Fly logic } } class Penguin extends Bird { // Penguins can't fly move() { } }
همونطور که دیدید فقط کافی بود با دقت بیشتری به مسئله نگاه میکردیم و به جای تعریف متد fly در سوپر کلاس، متد دیگه ای تحت عنوان move در نظر میگرفتیم و تعریف متد fly رو به کلاس دیگه ای که مختص پرندگان پروازی بود اختصاص می دادیم.
این اصل هم میگه که یه کلاس نباید اینترفیس هایی (Interface) رو پیاده سازی کنه که خودش بهشون نیازی نداره! به عبارت دیگه همون طور که قبلا هم گفتم هر کلاس باید کار مختص به خودش رو انجام بده. نمونه کد زیر و یه نگاه بندازیم تا موضوع شفاف تر بشه:
// Without ISP class Worker { work() { // Work logic } eat() { // Eat logic } }
خوب همونطوری که در بالا میبینیم به طور نمونه دو تا کار متفاوت به کلاس ورکر داده شده در صورتی که میشه اون رو به شکل زیر نوشت تا خوانایی کدمون بالاتر بره:
// With ISP class Workable { work() { } } class Eatable { eat() { } } class Worker implements Workable, Eatable { work() { // Work logic } eat() { // Eat logic } }
که در بالا اومدیم دو تا کلاس تعریف کردیم و هر کدوم تسک مربوط به خودش رو داره انجام میده. در نهایت در کلاس Worker کافیه دو کلاس Workable و Eatable رو implement کنیم و به راحتی ازشون استفاده کنیم.
فقط نکته ای که باید توجه کنیم اینه که با توجه به این که این قوانین که داریم میگیم مربوط به زبان های شی گرا هستن، از همین جهت تو جاوا اسکریپت عملا دستور implements کار نمیکنه و این نحوه پیاده سازی توی Typescript که بر مبنای شی گرایی کار میکنه معتبر هست.
و اما قانون آخرمون که خیلی هم مهم هست اینه که وابستگی مستقیم کلاس های سطح بالا رو به کلاس های سطح پایین منع میکنه. دلیل این کارم اینه که اگه کلاس های سطح بالا مستقیما به سطح پایین متصل بشن، بعد بابت هر تغییری که روی کلاس های سطح پایین صورت بگیره، باید کلاس های سطح بالامون رو هم اصلاح کنیم که خوب نیست!
حالا مثال زیر و بررسی کنیم با هم تا عملا توی کد ببینیم منظور از این قانون چی هست و داره بهمون چی میگه؟
// Without DIP class LightBulb { turnOn() { // Turn on logic } turnOff() { // Turn off logic } } class Switch { constructor(bulb) { this.bulb = bulb; } operate() { // Operate the bulb if (/* some condition */) { this.bulb.turnOn(); } else { this.bulb.turnOff(); } } }
خوب در مثال بالا کلاس LightBulb، کلاس سطح بالاست و Switch کلاس سطح پایین محسوب میشه. همونطوری که میبینیم، اینجا دو کلاس مستقیما به هم وصل شدن و در نتیجه در آینده هر تغییری که در Switch اعمال شه باید مستقیما روی کلاس LightBulb هم ایجاد بشه اما بریم ببینیم چطوری میشه این مشکل رو حل کرد؟!
// With DIP class Switchable { turnOn() {} turnOff() {} } class LightBulb implements Switchable { turnOn() { // Turn on logic } turnOff() { // Turn off logic } } class Switch { constructor(device) { this.device = device; } operate() { // Operate the device if (/* some condition */) { this.device.turnOn(); } else { this.device.turnOff(); } } }
همون طور که میبینیم اینجا یه کلاس Switchable ایجاد کردیم که به واسطه اون بتونیم وابستگی کلاس سطح بالامون رو از کلاس سطح پایین جدا کنیم که اونم از طریق همین کلاس Switchable انجام شد.
خوب اینم از بحث اصول SOLID در جاوا اسکریپت، قطعا برای تسلط بر این موضوعات، نیاز به تمرین و تکرار بیشتری خواهید داشت که شاید اول کمی اذیت شید اما ارزشش رو داره و کمک میکنه سطح برنامه نویسی تون چند لول بالاتر بیاد.