آشنایی با قوانین SOLID - به همراه مثال

شاید کم تر برنامه نویسی باشه که تو آگهی های استخدام عبارت "آشنایی با قوانین SOLID" رو ندیده باشه. یادگیری این قوانین کد نویسی تبدیل به جزء جداناپذیر برنامه نویسی شده و توصیه میشه تا حد امکان هنگام برنامه نویسی رعایت بشوند. تو این مقاله سعی کردم این اصول رو تا حد امکان ساده توضیح بدم. پس تا انتهای این مقاله با من باشید..‏SOLID چیست؟قوانین SOLID شامل 5 قانون طراحی شی گرا میشه که توسط آقای Robert C. Martin یا همون عمو باب معرفی شدند. پیروی از این قوانین باعث میشه برنامه ما قابل فهم تر بشه، تغییر دادنش راحت تر بشه و همینطور نگهداری از برنامه رو هم آسون تر میکنه(و کلی فواید دیگه?). هر حرف از این عبارت بیانگر یک قانون هست.Single-Responsibility PrincipleOpen–Closed PrincipleLiskov Substitution PrincipleInterface Segregation PrincipleDependency Inversion Principleدو تا نکته ریز: مفاهیمی مانند کلاس یا ماژول استفاده شده تو این مقاله تو بیشتر موارد میتونند جای همدیگه استفاده بشوند. یعنی اگر تو تعریف یک قانون از کلاس استفاده شده، همون قانون ممکنه برای ماژول و گاها تابع یا متد هم صادق باشه.مثال هایی که زده شده فقط برای درک بهتر یک قانون هست. پس ممکنه تو یه مثال قانون های دیگه و اصول دیگه کلا نقض بشوند.‏1. Single-Responsibility Principle(SRP)این قانون میگه که هر کلاس فقط باید یک کار مشخص رو انجام بده یا به عبارت دیگه تنها یک دلیل برای تغییر اون کلاس باید وجود داشته باشه.اگر یک کلاس مسئول کار های مختلفی بشه، در صورتی که یک قسمت خاص از اون کلاس رو تغییر بدیم، ممکنه روی کارکرد قسمت های دیگه از اون کلاس تاثیر بذاره(حتی بدون اینکه شما متوجه بشید). پس این قانون احتمال وجود باگ های ناخواسته رو به شدت کم می کنه.برای درک بهتر به تصویر پایین توجه کنید:تو کد بالا ما یک کلاس به نام Service داریم که دو تا کار گرفتن اطلاعات از سرور و ذخیره اون تو دیتابیس رو انجام میده، بنابراین قانون SRP رو نقض میکنه. برا حل این مشکل این کلاس رو باید به دو کلاس مجزا تقسیم کنیم که یکی کار گرفتن اطلاعات از سرور و دیگری ذخیره اطلاعات تو دیتابیس رو بر عهده داشته باشه.‏2. Open–Closed Principle(OCP)این قانون میگه که کلاس های ما باید به گونه ای نوشته بشوند که اگر خواستیم امکان جدیدی به اون اضافه کنیم، مجبور نباشیم اون کلاس رو تغییر بدیم، بلکه با استفاده از روش هایی مثل ارث بری(یا استفاده از اینترفیس ها و ...) بتونیم قابلیت های اون رو گسترش بدیم.در صورتی که با تغییر یک کلاس بخوایم امکانات جدیدی به اون اضافه کنیم، تغییرات ما ممکنه تو کارکرد بخش های دیگه ای که دارند از اون کلاس استفاده می کنند، مشکل ایجاد کنه. با استفاده از این قانون از این گونه مشکلات جلوگیری میشه.نکته: یک استثنا برای این قانون موقعی هست که میخواید یک باگی رو از کلاس برطرف کنید. یعنی اون موقع باید کلاس رو تغییر بدید.برای درک بهتر به تصویر پایین توجه کنید:تو کد بالا ما دو کلاس با نام های House و Car داریم که دارایی یک شخص رو نشون می دهند(برای همین از اینترفیس Property استفاده می کنند). هر کدوم از این دارایی ها یک ارزش مشخصی دارند. کلاسی با نام TotalPropertyValueCalculator هم داریم که مسئول محاسبه جمع دارایی های یک فرده. در صورتی که یک دارایی دیگه تعریف بکنیم، مجبور هستیم کلاس TotalPropertyValueCalculator رو هم تغییر بدیم که مخالف OCP هست. برای حل این مشکل میتونیم از کد زیر استفاده کنیم. تو کد تصحیح شده زیر تابعی با نام calculateValue رو تو اینترفیس Property تعریف کردیم و هر دارایی بر اساس منطق و متغیر های خودش میتونه اون رو override کنه. در صورت اضافه شدن دارایی جدید هم تنها همون کلاس جدید تغییر می کنه. این مثال تنها یکی از روش های نقض این قانون بود و روش های مختلفی برا نقض اش وجود داره.‏3. Liskov Substitution Principle(LSP)این قانون میگه که فرزندان(Child) یک کلاس نباید کارکرد پدر(Parent) خودشون رو نقض کنند و باید امکان استفاده به جای همدیگه ازشون، بدون مشکل فراهم باشه.احتمالا براتون پیش اومده که یک شی از جنس کلاس فرزند رو به عنوان کلاس پدر به توابع دیگه پاس دادید. در صورتی که به این قانون عمل نکنید، ممکنه کارکرد اون تابع با مشکل مواجه بشه. پس موقعی که دارید توابع کلاس پدر رو توسط فرزند اصطلاحا override می کنید، باید حواستون باشه که کارکرد اصلی تابع پدر نقض نشه.برای درک بهتر به تصویر پایین توجه کنید:تو تصویر بالا ما یک کلاس StorageLogger داریم که کارش ذخیره لاگ بصورت یک فایل تو حافظه هست. کلاس دیگه ای به نام PrinterLogger داریم که از StorageLogger ارث بری کرده و متد saveLogInStorage رو هم override کرده. همون طور که تو تصویر مشخصه این متد به گونه ای override شده که کارکرد پدر خودش رو کاملا نقض کرده و کلا ذخیره کردن لاگ تو حافظه رو نادیده گرفته. حالا فرض کنید کلاسی از شما یک شی از جنس StorageLogger میخواد و شما شی از جنس PrinterLogger رو بهش پاس میدید(چون ارث بری اینجا اتفاق افتاده، پس این کار ممکنه). حالا فرض کنید اون کلاس بعد از صدا زدن متد saveLogInStoreage، فایلی که از لاگ ذخیره شده رو به سرور میفرسته. از اونجایی که موقع override کردن ما هیچ فایلی رو ذخیره نکردیم، ممکنه برنامه با مشکل مواجه بشه. برا حل این مشکل میتونیم ارث بری رو به صورت زیر تغییر بدیم:‏4. Interface Segregation Principle(ISP)این قانون میگه کلاسی که داره از یک interface استفاده میکنه، نباید به توابعی از اون interface وابسته باشه که نیازی بهشون نداره.فرض کنید یک interface ده تا تابع داره. اگر کلاسی که داره از اون Interface استفاده می کنه، فقط به دو تاش نیاز داشته باشه، مجبوره هشت تا تابع دیگه رو به اجبار پیاده سازی کنه(و یا حتی در صورت پیاده سازی اشتباه و یا خالی گذاشتن بدنه، ممکنه باعث باگ های ناخواسته بشه). پس interface ها رو برای کارکرد های مختلف در حد معقول کوچیک در نظر بگیرید.برای درک بهتر به تصویر پایین توجه کنید:این مثال تا حدودی شبیه api های داخلی جاواست. دو تا کلاس با نام FileReader(مسئول خوندن اطلاعات از یک فایل) و FileWriter(مسئول نوشتن اطلاعات تو فایل) داریم. اینترفیس ای به نام Stream هم داریم که نشون دهنده عملیات های خوندن و نوشتن و آزاد کردن منابع هست. از اون جایی که FileReader مسئول خوندن اطلاعات از فایل هست، به متد write نیازی نداره و پیاده سازی ای براش تعریف نشده. از اون طرف کلاس FileWriter هم به متد read نیازی نداره. بنابراین اصل ISP نقض شده. برا حل این مشکل میتونیم از کد زیر استفاده بکنیم که اومدیم به ازای کارکرد های مختلف اینترفیس های کوچکتر و واضح تری تعریف کردیم.‏5. Dependency Inversion Principle(DIP)این قانون به طور خلاصه میگه که باید به abstraction ها(مثلا Interface ها) وابسته باشید نه پیاده سازی ها. یعنی ارتباط شما با کلاس ها یا ماژول های دیگه باید از طریق abstraction ها باشه نه پیاده سازی های خاص.در اصل این قانون 2 تا اصل رو عنوان میکنه که سعی کردم خلاصه اش رو براتون بنویسم. وابسته بودن به abstraction ها به جای جزئیات، باعث میشه به راحتی بتونیم رفتار یک ماژول رو بدون تغییر دادن کد اون بخش عوض کنیم. مثلا به جای اینکه به یک پرینتر با مدل خاص وابسته باشید، به هر چیزی که میتونه عمل پرینت گرفتن رو انجام بده، وابسته باشید. این شیوه وضعیت وابستگی رو تو برنامه شما بهبود میده.برای درک بهتر به تصویر پایین توجه کنید:تو تصویر بالا دو کلاس با نام های ReleaseLogger(مسئول ذخیره و ارسال لاگ به سرور در نسخه ریلیز) و DebugLogger(مسئول چاپ کردن لاگ رو صفحه تو نسخه دیباگ ) داریم. اینترفیس ای هم با نام Logger داریم که این دو کلاس اون رو پیاده کردند. بر اساس قانون DIP، اگر میخواید وابستگی داشته باشید، باید به اینترفیس Logger وابسته باشید نه یکی از کلاس هایی که اون رو پیاده سازی کردن مثل DebugLogger یا ReleaseLogger. این باعث میشه به راحتی بتونید رفتار کلاس ها رو بدون تغییر دادن اون ها عوض کنید.سخن پایانیتو این مقاله سعی کردم تا حد امکان مطالب رو بصورت واضح و اون طور که درک کردم، توضیح بدم. منتها هیچ چیزی رو نمیشه بهش مسلط شد مگر با تمرین زیاد و البته گذشت زمان. پس پیشنهاد می کنم دست به کد شید و مثال های خودتون از این قوانین رو برا خودتون بنویسید و به مزایا و معایب اش فکر کنید. اگر نظر یا انتقادی داشتید یا ایرادی تو مقاله بود، خیلی خوشحال میشم باهام تو کامنت ها در میون بذارید.ارادتمند شما، هادی درتاج