Liskov substitution principal یا قاعده جانشانی لیسکوف

LSP یک اصل از SOLID هست که میگه:

اگر S یک زیرنوع(Subtype) از T باشد، آنگاه در یک برنامه باید بتوان یک شیء از نوع T را با یک شیء از نوع S جایگزین کرد بدون اینکه این برنامه هیچ تغییری نیاز داشته باشد. به بیان ساده تر آبجکت کلاسهایی که والد و فرزند هستن باید بدون هیچ مشکلی در یک برنامه جای هم استفاده شوند.

مثال معروف بالا یک نمونه LSP رو نشون میده یه برنامه حسابداری هست که قیمت License رو حساب میکنه. حالا این محصول ما دو نوع license داره شخصی و شرکتی PersonalLicense و Business License. نکته اینجاست اگر به برنامه Billing ها یک شیء از هرکدوم این کلاسها رو پاس بدیم هیچ تغییری در رفتار Billing وجود نداره و نیازی هم به تغییر کد نیست بنابراین یک LSP هست.

مثال بعدی اینه که میدونیم مربع یک نوع مستطیل هست که اضلاعش با هم برابره. اما نمیتونیم بگیم توی یک برنامه شیء مربع قابل جایگزین با مستطیل نیست. چراکه مستطیل دو تابع داره setWeight و setHeight که طول و عرضش رو ست میکنه. حالا اگه توی یک مربع هر کدوم از این دوتا رو فراخوانی کنیم اونیکی هم تغیییر میکنه چون طول و عرض باید برابر باشن. بنابراین اگر به یک برنامه که یک شیء از مربع رو به جای مستطیل پاس بدیم نیاز هست که کد این برنامه رو هم تغییر بدیم که (مثلا با یک if که چک کنه مربعه یا مستطیل). بنابراین این مثال یک LSP نیست.

OCP در SOLID

OCP یا Open Closed Principal یکی از اصول SOLID هست. که میگه یک نرم افزار باید قابل توسعه و گسترش باشه و نسبت به تغییر بسته باشه.

A software artifact should be open for extention and closed for change.

یعنی توسعه پذیر باشه. به این معنی که اگر نیازمندی جدید بیاد با همین معماری بتونیم اون قابلیت رو اضافه کنیم ولی نه با تغییر اصول قبلی.

توی کتاب Clean Architecture یه مثال اومده که خیلی توضیحات داره و پیچیده است و جالب اینه که توی یکی از پست های همین بلاگ من یه مثال دارم که مثال خوبی برای OCP هست.

توی مثال ها گفته شده شما باید معماری رو جوری بچینید که اون بخش هایی که بنظرتون نباید تغییر کنن محافظت بشن. توی مثال این کتاب گفته: اگر کامپوننت A قراره از تغییر توی کامپوننت B محافظت بشه، کامپوننت B به وابستگی به کامپوننت A داشته باشه. مثل رابطه مدل ها توی دیتابیس.

توی این پست ما با یه چالش روبه رو بودیم.

چالش این بود که یه مدل Company داشتیم و میخواستیم یه فیلد عکس logo بهش اضافه کنیم و فکر کردم چون از این مدل در جاهای مختلف استفاده شده احتمالا این تغییر در جاهای مختلف Impact داره و باید بریم اونجاها رو هم تغییر بدیم. از طرفی نخواستم این مدل رو تغییر بدم و مجبور شم این مدل رو با migration توی دیتابیس تغییر بدم بنا براین به این نتیجه رسیدم که برای لوگو یک مدل جدید بسازم و یک رابطه یک-به-یک داشته باشن.

با این کار در واقع از مدل Company محافظت کردم. از این به بعد اگر بخوایم از یک مدل یا کلاس یا به صورت کلی یک کامپوننت محافظت کنیم باید بقیه به اون وابستگی داشته باشن و اون به کامپوننتی وابسته نباشه.

پس OCP یک اصل در معماری نرم افزاره که هدفش اینه که سیستم رو توسعه پذیرتر کنه و نیاز به تغییر در کامپوننت های قبلی در توسعه ها رو به حداقل برسونه. این هدف با پارتیشن بندی بخش های نرم افزار محقق میشه. با رده بندی کامپوننت ها با استفاده از وابستگی اونها به صورتی که یه سلسله مراتب ایجاد شه که از کامپوننتهای سطح بالا محافظت بشه که توی کامپوننت های سطح-پایینتر تغییر نکنن.

SRP یا Single Responsibility Principal

این اصل SOLID به این معنی نیست که هر ماژول از کد باید یک وظیفه داشته باشه (در نگاه اول اینطور به نظر میرسه).

این اصل میگه که:

A module should have one and only one reason to change

هر ماژول فقط یک دلیل برای تغییر باید داشته باشه. این یعنی چی؟ در واقع هر برنامه برای این ساخته شده که کار یک مشتری یا یک stakeholder رو راه بندازه. در واقع مشتری دلیل تغییر کد میتونه باشه. بنابراین میتونیم این جمله رو اینجوری بنویسیم

“هر ماژول فقط باید مسئول یک مشتری یا کاربر باشه”

مشتری منظور شخص نیست، چرا که توی یک تیم 20 نفر ممکنه یک نقش داشته باشن پس بهتره به جای کاربر یا مشتری بنویسیم Actor.

A module should be responsible to one and only one actor

هر ماژول فقط باید به یک Actor مرتبط باشه.

با یک مثال میتونیم بهتر بفهمیم:

فرض کنید یه ماژول داریم به اسم Employee که سه تا قسمت داره: CalculatePay و ReportHours و Save

CalculatePay برای محاسبه پرداختی استفاده میشه و مورد استفاده مدیر مالی. ReportHours برای محاسبه ساعات کارکرد نیازه برای استفاده مدیر منابع انسانی و Save با الگوریتمی بهینه داده ها رو توی دیتابیس ذخیره میکنه و مورد استفاده مدیر IT.

همینطور که میبینید این ماژول سه تا actor داره. فرض کنید که CalculatePay و ReportHours هر دو از یک الگوریتم استفاده میکنن (چون توی یک ماژول هستن برای جلوگیری از duplication برنامه نویس فکر کرده که هردو باید از یک الگوریتم استفاده کنن) فرض کنید مدیر مالی تصمیم میگیره که الگوریم محاسبه پرداختی تیمش رو تغییر بده و بعد از یک ماه مدیر منابع انسانی متوجه میشه محاسبه پرداختی تیمش تغییر کرده بدون اینکه خبر داشته باشه.

بنابراین:

seperate the code that different actors depend on

بهتره هر ماژول فقط یک actor داشته باشه.

مثال دوم اینه که:

اگر به حالتی برخوردید که یک ماژول توسط برنامه نویسان دو واحد همزمان توسعه داده میشه و توی Merge کردن کد به conflict میخورید یعنی این ماژول برای دو actor توسعه داده شده و بهتره اصلاح شه.

راه حل:

بهترین راه حل اینه که دیتا و توابع رو از هم جدا کنیم ینی یه کلاس داشته باشیم به اسم EmployeeData که سه تا کلاس PayCalculator و HourReporter و EmployeeSaver از اون به ارث میبرن و از همدیگه هییچ اطلاعی ندارن. این جوری هر کدوم رو تغییر بدیم فقط یه actor تحت تاثیر قرار میگیره.

مشکلش اینه که الان برنامه نویسا 3 تا کلاس دارن که توسعه بدن که برای حل این مشکل میشه از Facade Pattern استفاده کرد و یک EmployeeFacade ساخت که از سه تا کلاس به ارث می بره.

EmployeeFacade کد خیلی کمی داره. وظیفه اش صرفا ساختن نمونه و مقدار دهی اولیه هر کلاس و دادن اجرای برنامه به اون کلاسه نه چیزی بیشتر instantiating and delegating

بعضی برنامه نویسها هم ترجیح میدن که قسمتهای خیلی مهم کد رو نزدیک کلاس Data بزارن.

پس اصل SRP به توابع و کلاسها (در واقع component level) کار داره. اما تاثیرش رو توی معماری میزاره

اصول SOLID یا SOLID Principals

در زمینه توسعه محصول در هر بخش اصولی وجود داره که برای توسعه نرم افزاری استاندارد باید رعایت بشه. یک سری اصول در لایه های پایینی و جزئی نرم افزار وجود دارن. مثلا کد داخل توابع رو چطور بنویسیم، چطور به دیتابیس query بزنیم و اصلا چطور متغیرها رو تعریف کنیم و … اینها برای خودشون اصولی دارن ولی SOLID به این بخش مرتبط نیست.

توی لایه های بالاتر میگیم حالا این توابع و متغیرها و داده ها وقتی دسته بندی میشن با چه اصولی کنار هم قرار بگیرن. اگر بخایم زبان های OOP رو در نظر بگیریم در واقع مرتبط به داخل کلاس class میشه. اصول SOLID مربوط به این دسته بندی هاست یعنی توی لایه میانی mid-level.

لایه بالاتر میشه معماری که SOLID به اینجا هم مربوط نیست.

SOLID میگه این لایه میانی باید جوری نوشته بشه که:

  1. قابل تغییر باشه
  2. قابل فهم باشه
  3. ساختن بخش هایی از نرم افزار که خودشون بخش هایی از ماژولهای بزرگتر خواهند بود

اگر بخایم یه مثال قابل فهم بزنیم. فرض کنیم میخایم یه ساختمون تولید کنیم. بخشی از افراد دارن آجر درست میکنن. طبیعتا اگر آجرها مقاوم نباشن دیگه اتاقها و پارکینگ خوبی هم نمیشه ساخت. اما فرض کنیم که آجرها و مواد اولیه خوب ساخته شده حالا بازم اگر از اصولی پیروی کنیم ممکنه پارکینگ یا اتاق خوبی نسازیم پس از یک اصولی استفاده میکنیم تا بخش های ساختمون درست ساخته بشه SOILD با ساختن آجر کاری نداره با ساختن پارکینگ و لایه میانی کار داره.

اصول SOLID شامل موارد زیره:

SRP یا Single Responsility Principal

OCP یا Open Closed Principal

LSP یا Liscov Substitution Principal

ISP یا Interface Segregation Principal

DIP یا Dependency Inversion Principal