۱. مقدمه:

در سال ۱۹۹۸ خانم Barbara Liskov برای زیرنوعهای یک نوع، تعریف زیر را ارئه داد:
چیزی که ما میخواهیم خاصیت جایگزینی به این شکل است که: به ازای هر شی O1 از نوع S اگر یک شی O2 از T در برنامه P داشته باشیم که با جایگزنی O2 با O1 عملکرد P تغییر پیدا نکند، آنگاه میگوییم S زیرنوع T است.
برای یادگیری دقیق تر این تعریف که به Liskov Substitution Principle یا LSP مشهور است در ادامه این مطلب با من همراه باشید.
۲. راهنمای استفاده از وراثت:
فرض کنید مطابق با تصویر زیر کلاسی با نام License داریم که متدی با نام CalcFee دارد که توسط سیستم Billing مورد استفاده قرار میگیرد. این کلاس دو زیر کلاس به نام های PersonalLicence و BusinessLicense دارد که روشهای متفاوتی را برای پیاده سازی CalcFee مورد استفاده قرار میدهند.

با توجه به اینکه عملکرد Billing به هیچ طریقی به دو زیر نوع کلاس License وابسته نیست طراحی این کلاس مطابق با LSP است. هر دو زیر نوع کلاس License قابلیت جایگزینی آن را دارا هستند.
۳. مشکلی به نام مربع/مستطیل:
معمولا برای توضیح اصل Liskov از مثال مربع مستطیل استفاده میشود که با توجه به دانشی که از شرایط داریم برای این کار بسیار مناسب است. به تصویر زیر نگاه کنید:
در این مثال مربع زیرنوع مناسبی برای مستطیل نیست. به خاطر این که طول و عرض مستطیل بدون وابستگی به یکدیگر قابلیت تغییر دارند. اما در مورد مربع این موضوع صدق نمیکند. طول و عرض مربع باید با هم تغییر کنند. از آنجایی که کاربر فرض میکند با مستطیل در حال کار کردن است، عملکرد سیستم با جایگزینی مربع به جای مستطیل کاملا گیج کننده میشود. به تکه کد زیر دقت کنید.

در صورتی که تابع GetRectangleInstance به جای مستطیل یک مربع را بازگشت دهد، شرط ما هرگز برقرار نخواهد شد و این با توجه به مشاهدات برنامه کاملا اشتباه است. تنها راهی که بتوانیم تکه کد بالا را اصلاح کنیم این است که شرطی به برنامه اضافه کنیم و قبل از انجام کار اصلی بررسی کنیم که داخل متغیر ما واقعا مستطیل قرار دارد یا مربع که با این اوضاع کلاس استفاده کننده ما به نوع کلاس پیاده سازی کننده وابسته میشود. در حقیقت این زیر نوع قابلیت جایگزینی نوع بالادستی خود را ندارد.
۴. کاربرد LSP در معماری نرمافزار:
همانطور که در مثال بالا مشاهده کردید، در سالهای ابتدایی انقلاب شی گرایی LSP به عنوان راهنمایی برای تعیین ارث بریها مورد استفاده قرار میگرفت. اما با گذشت زمان کم کم ارزشهای بیشتر این اصل معلوم شد و کاربرد آن گسترده تر از قبل گردید و در طراحی رابطها و پیاده سازیها کاربرد فراوانی یافت.
رابطهایی که از آنها صحبت کردیم از هر نوعی میتوانند باشند، شاید یک رابط سی شارپ یا جاوا داشته باشیم که توسط چندین کلاس پیاده سازی شده است. یا شاید چندین کلاس روبی داشته باشیم که توابعی با امضای شبیه به هم را به اشتراک گذاشته اند.
در تمامی این مثالها و حتی بیشتر از آن هم LSP کاربرد دارد به خاطر اینکه کاربرانی وجود دارند که از این رابطها استفاده میکنند و در زمان اجرا ممکن است از پیادهسازیهای متفاوتی از این رابطها استفاده کنند.
بهترین راه برای اینکه LSP را از منظر معماری نرمافزار درک کنیم این است که ببینیم در صورت عدم رعایت این اصل چه اتفاقی برای سیستم میافتد.
۵. مثالی از عدم رعایت LSP:
فرض کنید در حال توسعه سیستمی برای تجمیع چندین سرویس تاکسیرانی هستیم. مسافر از وبسایت ما برای انتخاب مناسبترین تاکسی استفاده میکند و هیچ دغدغه و توجهی بابت شرکت تاکسیرانی ندارد. هنگامی که مسافر تاکسی را انتخاب میکند، سیستم ما به کمک سرویسهایی که از شرکتهای تاکسیرانی دریافت کرده است، تاکسی را برای مسافر اعزام میکند.
حال فرض کنید که URI وب سرویس اعزام بخشی از اطلاعات راننده باشد که دیتابیس رانندهها نگهداری میشود. هنگامی که سیستم راننده مناسب را انتخاب کرد، URI مربوط به سرویس را از ردیف مربوط به راننده در دیتابیس میخواند و از آن آدرس برای انجام عملیات اعزام استفاده میکند.
فرض کنید راننده ای به نام Bob به آدرس purplecab.com/driver/Bob داریم. حال سیستم ما آدرس مسافر را به آدرس راننده اضافه میکند و درخواست را مطابق آدرس زیر برای سرویس ارسال میکند.
purplecab.com/driver/Bob/pickupAddress/24 Maple St./pickupTime/153/destination/ORD
به طور واضحی مشخص است با این شرایط همه سرویسهای تاکسی رانی باید شرایط ارائه یکسانی برای وبسرویسهای خود در نظر بگیرند و مطابق با یکدیگر وب سرویسها را ارائه کنند تا برنامه ما بتواند به درستی کار کند. همه آنها باید pickupAddress, pickupTime و destination را دقیقا مطابق با همین سرویس داشته باشند.
حال فرض کنید یک شرکت تاکسی رانی بدون توجه به این نیازمندی سیستم ما برای تجمیع سرویسهای تاکسی رانی، برای خود سرویسی توسعه میدهد که در آن به جای Destination از فرمت خلاصه Dest استفاده شده است. حال فرض کنید که شرکت تاکسی رانی مذکور یکی از بزرگترین شرکتهای تاکسی رانی باشد و با این نوع ارائه سرویس امکان استفاده از آن و تجمیع دادههایش در نرمافزار ما وجود ندارد. تصور کنید در این شرایط چه اتفاقی برای معماری سیستم میافتد!
در این شرایط احتمالا باید سورس کد برنامه را تغییر دهیم و شرطی اضافه کنیم که اگر تاکسیرانی منتخب مسافر شرکتی بود که در باره آن صحبت کردیم، برای اعزام تاکسی به نحوه دیگری اقدام شود که با بقیه سیستم متفاوت است.
احتمالا تا همینجای کار متوجه فاجعهای که اتفاق میافتد شدهاید. اضافه شدن شرط جدید مربوط به شرکت جدید ارائه خدمات تاکسی رانی و وابستگی به نوع شرکت کافی است تا در آینده با اشکالات و خطاهای وحشتناکی مواجه شویم.
فرض کنید در آینده شرکتی دیگری اضافه میشود که باز هم قواعد ما را رعایت نمیکند! در این صورت تکلیف کار ما چیست؟ باز هم شرط جدید یا کنار گذاشتن تاکسیرانیهای جدید و متفاوت؟ چاره چیست؟
معماری ما به گونهای باید باشد که وابستگی به خدمات دهنده نداشته باشیم. در این مثال احتمالا باید یک دیتابیس برای نگهداری تنظیمات آدرس داشت باشیم و هنگام اعزام تاکسی از دیتابیس تنظیمات دقیق هر سیستم را بخوانیم و عملیات اعزام را متناسب با تنظیمات هر شرکت انجام دهیم. مطابق با تصویر زیر

احتمالا باید تنظیمات و استراتژیهای متفاوتی را در سیستم داشته باشیم تا بدون وابستگی به ارائه دهنده خدمات بتوانیم کار تجمیع و اعزام تاکسی را به درستی انجام دهیم.
۶. جمع بندی:
ما میتوانیم و باید LSP را هنگام معماری نرمافزار مدنظر قرار دهیم. در صورت عدم رعایت این اصل خیلی زود کدها و معماری ما با شرطها و بخشهای غیرکاربردی و شلوغ کننده پر میشود.