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

ارور دسترسی روی gitlab

gitlab رو توی داکر بالا آوردم ولی بعضی از کاربران برای لاگین ارور 422 میگرفتن. بعد از بررسی متوجه شدم که باید از مسیر زیر کانفیگ رو تیک بزنیم که resource های خارجی هم بتونن دسترسی داشته باشن. چون gitlab رو روی داکر بالا میارید این وسط لودبالانسر قرار میگیره

Admin → Settings → Network → Outbound Requests 

Structured vs Functional vs Object Oriented

هر کدوم از این پارادایم های برنامه نویسی شرایط خاصی دارن.

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

  1. Encapsulation
  2. Inheritance
  3. Polymorphism

وقتی میگیم یک برنامه Structured هست که از توابع قابل تست استفاده شده باشه. عملا الان هر کسی داره کد میزنه structured کد میزنه. این مفهوم زمانی شکل گرفت که با زبانهایی مثل Assembly برنامه نوشته میشد. برنامه نویسی Functional روی انتقال کنترل برنامه به صورت مستقیم تاکید داره.

Functional Programming وقتی میتونیم ادعا کنیم که برنامه مون کاملا Functional هست که تمامی متغیرها غیرقابل تغییر باشن Immutable و بنابراین مجبور میشیم از یک زبان Functional هم استفاده کنیم که ابزارهای کار با این متغیرها رو به ما میده. Functional Programming بر روی انتصاب به متغیرها محدودیت میزاره.

یعنی عملا هر کدام از این پارادایم ها یک محدودیت خاص بر روی برنامه های ما میزاره و هیچ کدوم چیزی اضافه نمیکنه.

Structured Programming

کسایی که قبلا Assembly کد زدن میدونن برنامه ها چطور بود. شما یک سری دستور پشت هم میچیدید و هر جایی لازم بود از GOTO و JUMP استفاده میکردید که برنامه بره به جای جدید و اون تیکه رو اجرا کنه. مثلا از خط 20 میگفتیم برگرد به خط 10. با این تکنیک عملا حلقه هم پیاده سازی میشد. ولی بعدا این روش منسوخ شد و توی برنامه توابع تعریف شدن. مفاهیم حلقه با کلید های جدید شکل گرفتن. به این روش جدید میگن structured programming.

Structured programming یعنی برنامه رو به بخش های مختلف و توابع مختلف تقسیم کنیم و بتونیم هر کدوم از اون توابع یا بخش ها رو تست کنیم. تست کنیم که در چه شرایطی غلط کار میکنن و بنابراین اطمینانی از برنامه داشته باشیم.

Functional programming

جمله زیر یه تعریف ریشه ای از Functional Programming هست.

variables in functional programming do not vary

یعنی اگر برنامه ای بنویسیم که تمامی متغیرهای immutable یا غیرقابل تغییر باشن میتونیم ادعا کنیم که برنامه ما از پارادایم functional استفاده میکنه. ربان هایی مثل clojure از این پاردایم استفاده میکنن.

برای اینکه متغیرها تغییر نکنن نیاز هست که برنامه شامل اجرای توابع پشت سر هم باشن. یعنی توی یک زبان functional چیزی که میبینیم یه رشته از توابع هستن که پشت سر هم اجرا میشن

بهترین روش برای حل مشکل Race condition

تمامی مشکلات مربوط به race condition و همزمانی تغییر یک متغیر توسط thread ها و پروسه های موازی و شرایط deadlock از این واقعیت سرچشمه میگیره که چند فرایند به اشتباه یک متغیر رو با هم تغییر بدن. و اگر یک متغیر داشته باشیم که دیگه تغییر نکنه تمامی این مشکلات از ریشه حل میشه. یعنی اگر متغیرهای ما همگی immutable یا غیر قابل تغییر باشن دیگه این مشکلات رو نمیبینیم.

تنها در زبانهای functional مثل clojure این امکان وجود داره که تمامی متغیرها immutable باشن.

در زبانهای دیگه که functional نیستن باید این مشکل رو جوری دیگه حل کرد. یکی از روش هایی که این مشکل رو حل میکنن اینه که یک بخشی از برنامه که قراره همزمانی توش داشته باشیم رو با متغیرهای immutable مینویسیم و بقیه بخش های برنامه رو mutable.

و از بخش mutable میتونیم از transactional memory استفاده کنیم که از این متغیرها نگهداری بشه.

پس بهترین معماری اینه که برنامه به دو بخش تقسیم بشه. یک بخش که متغیرها رو توشون تغییر میدیم و یک بخش که متغیرها تغییر نمیکنن و immutable هستن. حالا از سیاست هایی از اون بخشی که متغیرها رو عوض میکنه محافظت میکنیم.

Event Sroucing چیه؟

فرض کنیم یه اپلیکیشن بانکی میخایم بنویسیم که حسابهای افراد رو مدیریت کنه. برای این کار یک روش اینه که اگه طرف مثلا 1 میلیون بریزه توی حسابش اپلیکیشن بره و مقداره متغیر مانده حساب رو با 1 میلیون اضافه کنه. و هر وقت x تومن خرج میکنه این متغیر رو x واحد کم کنه. این چه مشکلی میتونه داشته باشه:

1.ممکنه race condition پیش بیاد و چند تا thread یا پروسه مقدار رو با هم تغییر بدن و در این حالت عملا حساب بانکی مشتری رو خراب کردیم

2.تاریخچه ای از تراکنش ها وجود نداره که بتونیم پرینت حساب بگیریم

بهترین روش اینه که مقدار حساب مشتری یه متغیر غیرقابل تغییر باشه 🙂 “فکر نکنم دیگه بشه بهش گفت متغیر” و تراکنش ها رو ذخیره کنیم مثلا مقدار 1 میلیون هست و وقتی x تومن خرج میکنه دیگه از اون مقدار کم نکنیم و فقط یه تراکنش -x ثبت کنیم. در این حالت دیگه حساب مشتری خراب نمیشه. تاریخچه حساب معلومه و race condition به وجود نمیاد. این روشی هست که توی اپلیکیشن های بانکی ازش استفاده میشه و بهش میگن Event Sourcing.

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

مشکل دوم اینه که دیسک زیادی نیاز هست چون باید تمامی تراکنش های تمامی کاربران نگهداری شه.

اگر منابعی مثل CPU و Disk به اندازه کافی داشته باشیم میتونیم تمامی متغیرها رو immutable کنیم و بنابراین برنامه کاملا Functional باشه. در این حالت البته توسعه سخت تر هم هست.