LSP (Liskov Substitution Principle) — سومین اصل SOLID است که شرایط وراثت صحیح در برنامهنویسی شیءگرا را تعیین میکند. این اصل توسط باربارا لیسکوف در سال ۱۹۸۷ فرموله شد: اگر S زیرنوع T باشد، اشیاء T میتوانند با اشیاء S بدون تغییر ویژگیهای برنامه جایگزین شوند. همانطور که در کتاب رابرت مارتین Clean Architecture (2017) اشاره شده، اصل جانشینی ایجاب میکند که زیرکلاس قرارداد کلاس پایه را تضعیف نکند.
نکات مهم
LSP (Liskov Substitution Principle) — اصل جانشینی فرموله شده توسط باربارا لیسکوف در کنفرانس OOPSLA در سال ۱۹۸۷. تعریف رسمی: فرض کنید q(x) یک ویژگی قابل اثبات برای اشیاء x از نوع T باشد. آنگاه q(y) باید برای اشیاء y از نوع S قابل اثبات باشد، که در آن S زیرنوع T است. به زبان ساده: اشیاء زیرکلاس باید چنان رفتار کنند که کد کار با کلاس پایه با زیرکلاس نیز به درستی کار کند.
در عمل LSP به این معناست که زیرکلاس نباید قرارداد کلاس پایه را نقض کند. قرارداد شامل پیششرطها (آنچه برای فراخوانی متد لازم است)، پسشرطها (آنچه پس از فراخوانی تضمین میشود) و نامغیرها (شرایطی که در طول عمر شیء حفظ میشوند) است. زیرکلاس میتواند پیششرطها را تقویت یا پسشرطها را تضعیف کند — این دقیقاً نقض LSP است.
مثال کلاسیک نقض LSP — مربعی که از مستطیل ارث میبرد. متد setWidth در مستطیل عرض را تنظیم میکند، در مربع — هم عرض و هم ارتفاع. کلاینی که منتظر رفتار مستطیل است (تغییر یک ضلع بر دیگری تأثیر نمیگذارد) نتیجه غیرمنتظره دریافت میکند. مربع یک زیرنوع صحیح از مستطیل نیست.
LSP سه شرط برای وراثت صحیح تعیین میکند: پیششرطهای زیرکلاس نمیتوانند قویتر از پیششرطهای کلاس پایه باشند (زیرکلاس بیشتر مطالبه نمیکند)، پسشرطهای زیرکلاس نمیتوانند ضعیفتر از پسشرطهای کلاس پایه باشند (زیرکلاس کمتر تضمین نمیکند)، نامغیرهای کلاس پایه باید در زیرکلاس حفظ شوند. این شرایط به عنوان قاعده طراحی قراردادی اثر برتراند میر شناخته میشوند.
اگر حداقل یک شرط نقض شود — کدی که از چندریختی استفاده میکند ممکن است دچار مشکل شود. کامپایلر قراردادهای معنایی را بررسی نمیکند، فقط نحوی را بررسی میکند. بنابراین LSP مسئله انضباط معماری است، نه تایپدهی ایستا.
مکانیزم LSP مبتنی بر سازگاری رفتاری انواع است. اگر کلاس S از کلاس T ارث ببرد، کد کلاینت باید بتواند از S در هر جایی که T انتظار میرود استفاده کند بدون تغییر در رفتار خود. این نه تنها امضای متدها، بلکه معنای آنها را نیز شامل میشود.
LSP زیرکلاس را از افزودن رفتار جدید منع نمیکند. نقض انتظارات کد نوشته شده برای کلاس پایه ممنوع است. اگر کلاس پایه تضمین میکند که متد save استثنا پرتاب نمیکند، زیرکلاس نباید آنها را پرتاب کند. اگر کلاس پایه مقدار نامنفی برمیگرداند، زیرکلاس نباید مقدار منفی برگرداند.
در پروژههای واقعی LSP اغلب با افزودن منطق شرطی در متدهای زیرکلاس نقض میشود: «اگر شرط — استثنا پرتاب کن»، «اگر شرط — null برگردان». هر چنین «غافلگیری» چندریختی را تضعیف میکند و کد کلاینت را مجبور به بررسی نوع شیء قبل از فراخوانی میکند — که با ایده طراحی شیءگرا در تضاد است.
در پروژههای موبایل نقض معمول LSP هنگام ایجاد ViewModel پایه رخ میدهد. اگر BaseViewModel تضمین کند که متد onCleared همه منابع را آزاد میکند، و زیرکلاس این متد را خالی بازنویسی کند — هر کدی که به آزادسازی منابع از طریق فراخوانی چندریختی onCleared متکی است، نادرست کار خواهد کرد. LSP ایجاب میکند که زیرکلاس یا super.onCleared() را فراخوانی کند یا خود همان کار را انجام دهد. ترکیب از طریق LifecycleObserver — جایگزینی که نقض LSP را در مدیریت چرخه حیات حذف میکند.
شاخصهای اصلی نقض LSP عبارتند از: بررسی نوع شیء با instanceof یا is قبل از فراخوانی متد، پیادهسازی خالی متدها (stub)، پرتاب استثنای NotImplementedError یا UnsupportedOperationException، بازگرداندن null به جای مقدار. هر یک از این الگوها نشان میدهد که زیرکلاس یک زیرنوع صحیح نیست.
نشانه رایج دیگر — وراثت به منظور استفاده مجدد از کد، نه برای مدلسازی رابطه «است» (is-a). کلاس Bird متد fly() دارد. کلاس Penguin از Bird ارث میبرد و fly() را خالی یا استثناکننده بازنویسی میکند. این نقض LSP است: پنگوئن یک زیرنوع صحیح از پرنده نیست.
در توسعه موبایل LSP هنگام ایجاد ViewHolder، Fragment یا ViewController پایه با متدهای stub نقض میشود. اگر زیرکلاس از نیمی از متدهای کلاس پایه استفاده نکند — وراثت نادرست انتخاب شده است. ترکیب یا تفکیک رابط مسئله را درستتر حل میکند.
تست ساده برای بررسی LSP: یک تست واحد برای کلاس پایه بنویسید که قرارداد آن را بررسی میکند (مقادیر بازگشتی، استثناها، اثرات جانبی). این تست را برای هر زیرکلاس اجرا کنید. اگر تست ناموفق بود — LSP نقض شده است. این رویکرد «تست از طریق قرارداد کلاس پایه» نام دارد.
در پروژههای Android چنین تستی برای ViewModel و Repository مفید است. اگر BaseViewModel وضعیت Loading را قبل از خطا تضمین کند، و زیرکلاس بدون Loading خطا بزند — تست نقض LSP را در مرحله CI ثبت میکند.
بیایید مثال Android را با پردازش ClickListener بررسی کنیم. نقض LSP زمانی رخ میدهد که پیادهسازی پایه چیزی را تضمین کند و زیرکلاس آن را نقض کند.
// کلاس پایه با تضمین: onClick فراخوانی خواهد شد
open class BaseClickListener {
open fun onClick(view: View) {
// پردازش پایه
}
}
// نقض LSP: زیرکلاس شرطی اضافه میکند که استثنا پرتاب میکند
class RestrictedClickListener : BaseClickListener() {
override fun onClick(view: View) {
if (!isLoggedIn) {
throw IllegalStateException("Not logged in")
}
super.onClick(view)
}
}
// راهحل صحیح: قرارداد نقض نشده است
class ConditionalClickListener : BaseClickListener() {
override fun onClick(view: View) {
if (isLoggedIn) {
super.onClick(view)
}
}
}
مثال iOS با پروتکل DataSource نقض LSP را از طریق بازگرداندن nil به جای داده نشان میدهد:
// پروتکل با قرارداد: داده یا خطا برمیگرداند
protocol DataProvider {
func fetchData() async throws -> [String]
}
// نقض LSP: nil بدون خطا برمیگرداند
class SilentFailProvider: DataProvider {
func fetchData() async throws -> [String] {
return [] // آرایه خالی به جای خطا
}
}
// رعایت صحیح LSP
class NetworkProvider: DataProvider {
func fetchData() async throws -> [String] {
throw NetworkError.timeout
}
}
قاعده عملی: اگر زیرکلاس نمیتواند قرارداد کلاس پایه را انجام دهد — نباید زیرکلاس باشد. جایگزین — استخراج رابطی با حداقل قرارداد و پیادهسازی آن در هر نوع به روش خود.
ترکیب در شرایطی که رابطه «است» (is-a) مبهم یا مشروط است، از وراثت ارجحتر است. مثال کلاسیک: Manager Employee است؟ بله. اما آیا Square یک Rectangle صحیح است؟ LSP میگوید «خیر». اگر در صحت وراثت تردید دارید — ترکیب را انتخاب کنید.
در توسعه موبایل ترکیب اغلب از طریق تزریق وابستگی استفاده میشود: به جای ارث بردن رفتار از کلاس پایه، کلاس آن را از طریق سازنده دریافت میکند. ViewModel از Repository ارث نمیبرد، بلکه آن را به عنوان وابستگی میپذیرد. این نقض LSP را طبق تعریف حذف میکند — وراثتی نیست، نقض قراردادی هم نیست.
نشانههایی که وراثت باید با ترکیب جایگزین شود: زیرکلاس از بخشی از متدهای کلاس پایه استفاده نمیکند، زیرکلاس متدها را با stub خالی بازنویسی میکند، کد کلاینت نوع شیء را با instanceof بررسی میکند. در این موارد وراثت نادرست انتخاب شده و LSP نقض شده است.
رابطها مسئله LSP را بدون وراثت حل میکنند: هر نوع دقیقاً همان متدهایی را که نیاز دارد پیادهسازی میکند. به جای کلاس پایه مشترک Bird با متد fly (جایی که Penguin پرواز نمیکند) — رابط Flyable که فقط پرندگان پرنده آن را پیادهسازی میکنند. Penguin Bird را بدون متد fly پیادهسازی میکند — LSP نقض نشده است.
در معماری Android این رویکرد از طریق رابطهای تفکیک شده UseCase اعمال میشود: به جای یک UseCase بزرگ با متدهای getAll، getById، save، delete — رابطهای جداگانه GetItemsUseCase، SaveItemUseCase. کلاینت فقط به رابط مورد نیاز وابسته است و هر کلاسی که این رابط را پیادهسازی کند از نظر LSP صحیح است.
سوالات متداول
وراثت — مکانیزم زبان، LSP — قاعده استفاده صحیح از این مکانیزم. وراثت سازگاری امضاها (نحو) را تضمین میکند، LSP سازگاری رفتار (معنا) را ایجاب میکند. وراثت بدون LSP چندریختیای میدهد که در زمان اجرا خراب میشود.
اگر کلاس پایه بازگشت non-null را تضمین کند — بله. اگر قرارداد null (مقدار اختیاری) را مجاز بداند — خیر. LSP null را منع نمیکند، تضعیف قرارداد را منع میکند. مستندات کلاس پایه را مطالعه کنید و بررسی کنید که آیا قرارداد زیرکلاس سازگار است.
به پروتکلها LSP همانند کلاسها اعمال میشود. پیادهسازی پروتکل باید قرارداد معنایی را رعایت کند: اگر پروتکل متدی را به عنوان non-throwing تعریف میکند، پیادهسازی نباید خطا پرتاب کند. Swift این را در سطح کامپایل بررسی نمیکند — مسئولیت بر عهده توسعهدهنده است.
در Kotlin Sealed class — مورد خاصی است، زیرا سلسلهمراتب بسته و برای کامپایلر شناخته شده است. LSP به sealed class به میزان کمتری اعمال میشود، زیرا همه زیرنوعها به صراحت در عبارت when ذکر شدهاند. خطای زیرکلاس sealed محلی خواهد بود، نه یک خطای چندریختی پنهان.
یک تست پارامتری برای کلاس پایه بنویسید که برای همه زیرکلاسهای آن اجرا میشود. تست قراردادهای رفتاری کلیدی را بررسی میکند: مقادیر بازگشتی، استثناها، حالتها. اگر تست در یکی از زیرکلاسها ناموفق باشد — LSP نقض شده است. در CI چنین تستی از پسرفت کد چندریختی جلوگیری میکند.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید