LSP: مفهوم، اصل جانشینی باربارا لیسکوف در توسعه

نویسنده: IT Sectr منتشر شده: 2026-05-12 زمان مطالعه: 9 دقیقه

LSP (Liskov Substitution Principle) — سومین اصل SOLID است که شرایط وراثت صحیح در برنامه‌نویسی شیءگرا را تعیین می‌کند. این اصل توسط باربارا لیسکوف در سال ۱۹۸۷ فرموله شد: اگر S زیرنوع T باشد، اشیاء T می‌توانند با اشیاء S بدون تغییر ویژگی‌های برنامه جایگزین شوند. همانطور که در کتاب رابرت مارتین Clean Architecture (2017) اشاره شده، اصل جانشینی ایجاب می‌کند که زیرکلاس قرارداد کلاس پایه را تضعیف نکند.

نکات مهم

  • LSP — اصل جانشینی لیسکوف، سومین اصل SOLID درباره وراثت صحیح
  • زیرکلاس باید قرارداد کلاس پایه را حفظ کند — پیش‌شرط‌ها و پس‌شرط‌ها
  • نقض LSP در مسئله مربع و مستطیل و استثناهای پرتاب شده ظاهر می‌شود
  • ترکیب اغلب برای رعایت LSP از وراثت ارجح‌تر است
  • طراحی قراردادی (Design by Contract) — روش رسمی بررسی LSP

LSP (Liskov Substitution Principle) چیست؟

LSP (Liskov Substitution Principle) — اصل جانشینی فرموله شده توسط باربارا لیسکوف در کنفرانس OOPSLA در سال ۱۹۸۷. تعریف رسمی: فرض کنید q(x) یک ویژگی قابل اثبات برای اشیاء x از نوع T باشد. آنگاه q(y) باید برای اشیاء y از نوع S قابل اثبات باشد، که در آن S زیرنوع T است. به زبان ساده: اشیاء زیرکلاس باید چنان رفتار کنند که کد کار با کلاس پایه با زیرکلاس نیز به درستی کار کند.

در عمل LSP به این معناست که زیرکلاس نباید قرارداد کلاس پایه را نقض کند. قرارداد شامل پیش‌شرط‌ها (آنچه برای فراخوانی متد لازم است)، پس‌شرط‌ها (آنچه پس از فراخوانی تضمین می‌شود) و نامغیرها (شرایطی که در طول عمر شیء حفظ می‌شوند) است. زیرکلاس می‌تواند پیش‌شرط‌ها را تقویت یا پس‌شرط‌ها را تضعیف کند — این دقیقاً نقض LSP است.

مثال کلاسیک نقض LSP — مربعی که از مستطیل ارث می‌برد. متد setWidth در مستطیل عرض را تنظیم می‌کند، در مربع — هم عرض و هم ارتفاع. کلاینی که منتظر رفتار مستطیل است (تغییر یک ضلع بر دیگری تأثیر نمی‌گذارد) نتیجه غیرمنتظره دریافت می‌کند. مربع یک زیرنوع صحیح از مستطیل نیست.

شرایط رسمی LSP

LSP سه شرط برای وراثت صحیح تعیین می‌کند: پیش‌شرط‌های زیرکلاس نمی‌توانند قوی‌تر از پیش‌شرط‌های کلاس پایه باشند (زیرکلاس بیشتر مطالبه نمی‌کند)، پس‌شرط‌های زیرکلاس نمی‌توانند ضعیف‌تر از پس‌شرط‌های کلاس پایه باشند (زیرکلاس کمتر تضمین نمی‌کند)، نامغیرهای کلاس پایه باید در زیرکلاس حفظ شوند. این شرایط به عنوان قاعده طراحی قراردادی اثر برتراند میر شناخته می‌شوند.

اگر حداقل یک شرط نقض شود — کدی که از چندریختی استفاده می‌کند ممکن است دچار مشکل شود. کامپایلر قراردادهای معنایی را بررسی نمی‌کند، فقط نحوی را بررسی می‌کند. بنابراین LSP مسئله انضباط معماری است، نه تایپ‌دهی ایستا.

اصل جانشینی لیسکوف چگونه کار می‌کند

مکانیزم LSP مبتنی بر سازگاری رفتاری انواع است. اگر کلاس S از کلاس T ارث ببرد، کد کلاینت باید بتواند از S در هر جایی که T انتظار می‌رود استفاده کند بدون تغییر در رفتار خود. این نه تنها امضای متدها، بلکه معنای آنها را نیز شامل می‌شود.

LSP زیرکلاس را از افزودن رفتار جدید منع نمی‌کند. نقض انتظارات کد نوشته شده برای کلاس پایه ممنوع است. اگر کلاس پایه تضمین می‌کند که متد save استثنا پرتاب نمی‌کند، زیرکلاس نباید آنها را پرتاب کند. اگر کلاس پایه مقدار نامنفی برمی‌گرداند، زیرکلاس نباید مقدار منفی برگرداند.

در پروژه‌های واقعی LSP اغلب با افزودن منطق شرطی در متدهای زیرکلاس نقض می‌شود: «اگر شرط — استثنا پرتاب کن»، «اگر شرط — null برگردان». هر چنین «غافلگیری» چندریختی را تضعیف می‌کند و کد کلاینت را مجبور به بررسی نوع شیء قبل از فراخوانی می‌کند — که با ایده طراحی شیءگرا در تضاد است.

در پروژه‌های موبایل نقض معمول LSP هنگام ایجاد ViewModel پایه رخ می‌دهد. اگر BaseViewModel تضمین کند که متد onCleared همه منابع را آزاد می‌کند، و زیرکلاس این متد را خالی بازنویسی کند — هر کدی که به آزادسازی منابع از طریق فراخوانی چندریختی onCleared متکی است، نادرست کار خواهد کرد. LSP ایجاب می‌کند که زیرکلاس یا super.onCleared() را فراخوانی کند یا خود همان کار را انجام دهد. ترکیب از طریق LifecycleObserver — جایگزینی که نقض LSP را در مدیریت چرخه حیات حذف می‌کند.

نشانه‌های نقض LSP در کد

شاخص‌های اصلی نقض LSP عبارتند از: بررسی نوع شیء با instanceof یا is قبل از فراخوانی متد، پیاده‌سازی خالی متدها (stub)، پرتاب استثنای NotImplementedError یا UnsupportedOperationException، بازگرداندن null به جای مقدار. هر یک از این الگوها نشان می‌دهد که زیرکلاس یک زیرنوع صحیح نیست.

نشانه رایج دیگر — وراثت به منظور استفاده مجدد از کد، نه برای مدل‌سازی رابطه «است» (is-a). کلاس Bird متد fly() دارد. کلاس Penguin از Bird ارث می‌برد و fly() را خالی یا استثناکننده بازنویسی می‌کند. این نقض LSP است: پنگوئن یک زیرنوع صحیح از پرنده نیست.

در توسعه موبایل LSP هنگام ایجاد ViewHolder، Fragment یا ViewController پایه با متدهای stub نقض می‌شود. اگر زیرکلاس از نیمی از متدهای کلاس پایه استفاده نکند — وراثت نادرست انتخاب شده است. ترکیب یا تفکیک رابط مسئله را درست‌تر حل می‌کند.

تست LSP

تست ساده برای بررسی LSP: یک تست واحد برای کلاس پایه بنویسید که قرارداد آن را بررسی می‌کند (مقادیر بازگشتی، استثناها، اثرات جانبی). این تست را برای هر زیرکلاس اجرا کنید. اگر تست ناموفق بود — LSP نقض شده است. این رویکرد «تست از طریق قرارداد کلاس پایه» نام دارد.

در پروژه‌های Android چنین تستی برای ViewModel و Repository مفید است. اگر BaseViewModel وضعیت Loading را قبل از خطا تضمین کند، و زیرکلاس بدون Loading خطا بزند — تست نقض LSP را در مرحله CI ثبت می‌کند.

مثال‌های LSP در توسعه موبایل

بیایید مثال Android را با پردازش ClickListener بررسی کنیم. نقض LSP زمانی رخ می‌دهد که پیاده‌سازی پایه چیزی را تضمین کند و زیرکلاس آن را نقض کند.

kotlin
// کلاس پایه با تضمین: 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 به جای داده نشان می‌دهد:

swift
// پروتکل با قرارداد: داده یا خطا برمی‌گرداند
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
    }
}

قاعده عملی: اگر زیرکلاس نمی‌تواند قرارداد کلاس پایه را انجام دهد — نباید زیرکلاس باشد. جایگزین — استخراج رابطی با حداقل قرارداد و پیاده‌سازی آن در هر نوع به روش خود.

LSP و وراثت: چه زمانی ترکیب را انتخاب کنیم

ترکیب در شرایطی که رابطه «است» (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 سازگاری رفتار (معنا) را ایجاب می‌کند. وراثت بدون LSP چندریختی‌ای می‌دهد که در زمان اجرا خراب می‌شود.

آیا null در زیرکلاس همیشه LSP را نقض می‌کند؟

اگر کلاس پایه بازگشت non-null را تضمین کند — بله. اگر قرارداد null (مقدار اختیاری) را مجاز بداند — خیر. LSP null را منع نمی‌کند، تضعیف قرارداد را منع می‌کند. مستندات کلاس پایه را مطالعه کنید و بررسی کنید که آیا قرارداد زیرکلاس سازگار است.

چگونه LSP در Swift به پروتکل‌ها اعمال می‌شود؟

به پروتکل‌ها LSP همانند کلاس‌ها اعمال می‌شود. پیاده‌سازی پروتکل باید قرارداد معنایی را رعایت کند: اگر پروتکل متدی را به عنوان non-throwing تعریف می‌کند، پیاده‌سازی نباید خطا پرتاب کند. Swift این را در سطح کامپایل بررسی نمی‌کند — مسئولیت بر عهده توسعه‌دهنده است.

آیا LSP می‌تواند با استفاده از sealed class نقض شود؟

در Kotlin Sealed class — مورد خاصی است، زیرا سلسله‌مراتب بسته و برای کامپایلر شناخته شده است. LSP به sealed class به میزان کمتری اعمال می‌شود، زیرا همه زیرنوع‌ها به صراحت در عبارت when ذکر شده‌اند. خطای زیرکلاس sealed محلی خواهد بود، نه یک خطای چندریختی پنهان.

چگونه رعایت LSP را در پروژه تست کنیم؟

یک تست پارامتری برای کلاس پایه بنویسید که برای همه زیرکلاس‌های آن اجرا می‌شود. تست قراردادهای رفتاری کلیدی را بررسی می‌کند: مقادیر بازگشتی، استثناها، حالت‌ها. اگر تست در یکی از زیرکلاس‌ها ناموفق باشد — LSP نقض شده است. در CI چنین تستی از پسرفت کد چندریختی جلوگیری می‌کند.

خلاصه

  • LSP (Liskov Substitution Principle) — اصل جانشینی، سومین در SOLID، درباره سازگاری معنایی وراثت
  • زیرکلاس باید قرارداد کلاس پایه را حفظ کند: پیش‌شرط‌ها، پس‌شرط‌ها و نامغیرها
  • بررسی instanceof و بازنویسی خالی متدها — نشانه‌های اصلی نقض LSP
  • ترکیب و رابط‌ها مسئله LSP را در جایی که وراثت نادرست است حل می‌کنند
  • مسئله مربع و مستطیل — مثال کلاسیک ناسازگاری زیرنوع‌ها
  • تست قرارداد برای کلاس پایه، که برای همه زیرکلاس‌ها اجرا می‌شود، نقض LSP را در CI شناسایی می‌کند
  • در Kotlin Sealed class خطرات LSP را به دلیل سلسله‌مراتب بسته و شناخته شده برای کامپایلر کاهش می‌دهد

ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد

IT Sectr از سال 2017 برنامه‌های iOS و Android را برای استارتاپ‌ها و کسب‌وکارها ایجاد می‌کند. ما به شما مشاوره می‌دهیم و بهترین راه‌حل را پیشنهاد خواهیم کرد.

بحث درباره پروژه

همچنین بخوانید