اصل تعویض لیسکوف: وراثتی که درست به نظر می رسد اما همه چیز را می شکند

“اگر شما نیاز به نوشتن اگر (شیء زیر کلاس) در زمان اجرا باشد ، میراث شما ممکن است از قبل اشتباه باشد.”
اصل جایگزینی Liskov (LSP) اغلب به عنوان دانشگاهی دیده می شود – تا زمانی که بی سر و صدا سیستم شما را در تولید بشکند.
شما یک کلاس را گسترش می دهید که فکر می کنید از کد استفاده مجدد می کنید. اما بدون درک ، شما یک زیر کلاس ایجاد می کنید که رفتار مورد انتظار کلاس پایه را نقض می کند.
کامپایل می کند اجرا می شود اما رفتار نمی کند.
این همان چیزی است که اصل تعویض لیسکوف ، “L” در جامد ، به ما کمک می کند تا از آن جلوگیری کنیم.
LSP در واقع چه می گوید؟
“اگر S زیرگروه T باشد ، ممکن است اشیاء نوع T بدون تغییر صحت برنامه با اشیاء نوع S جایگزین شوند.”
– باربارا لیسکوف (1987)
به زبان ساده:
یک زیر کلاس باید رفتار مورد انتظار از کلاس پایه را حفظ کند
شما باید بتوانید از آن استفاده کنید بدون اینکه بدانید یک زیر کلاس است
اگر نیاز به تنظیم منطق خود دارید تا زیر کلاس حساب کنید … این شکسته است
یک قیاس ساده: فنجان پلاستیکی در مقابل فنجان شیشه ای
دستگاهی را تصور کنید که فنجان ها را با آب پر می کند.
اگر یک فنجان پلاستیکی را با یک لیوان جایگزین کنید ، همه چیز هنوز هم باید یکسان باشد.
اگر شیشه به دلیل اینکه سیستم انتظار چیزی را شکننده نمی کند ، فرو می رود … این یک نقض جایگزینی است.
همین کار را برای کد انجام می دهد: تغییر رفتار داخلی سکوت اعتماد به انتزاع را می شکند.
یک مثال کلاسیک: مستطیل در مقابل مربع
بی ضرر به نظر می رسد
از این گذشته ، یک مربع مستطیل با طرفین مساوی است ، درست است؟
مستطیل کلاس عمومی {
عمومی دوتایی مجازی عمومی {دریافت کنید ؛ ست }
عمومی دو برابر مجازی عمومی {دریافت کنید ؛ ست }
public double Area() => Width * Height;
}
اکنون ما آن را گسترش می دهیم:
مربع کلاس عمومی: مستطیل {
عمومی از عرض دو برابر {
تنظیم {
Base.width = مقدار ؛
base.height = مقدار ؛
}
}
public override double Height {
set {
base.Width = value;
base.Height = value;
}
}
}
منطقی به نظر می رسد – اما پس از آن:
مستطیل r = مربع جدید () ؛
r.width = 5 ؛
R.height = 10 ؛
Console.Writeline (R.Area ()) ؛ // مورد انتظار: 50 – واقعی: 100
رونق این رفتار شکسته است زیرا مربع از خواص به شکلی که فرضیات کلاس پایه را می شکند ، غلبه می کند.
چگونه می توان گفت LSP را نقض می کنید
شما باید در منطق خود بررسی کنید که آیا (شیء زیرگروه است)
زیر کلاس شما استثنائاتی را در روش هایی که انتظار می رود ایمن باشد ، پرتاب می کند
زیر کلاس شما تأثیر روشهای کلاس پایه را تغییر می دهد
با اطمینان نمی توانید پایه را با زیر کلاس جایگزین کنید
اگر نمی توانید بدون بررسی داخلی آن به شیء اعتماد کنید ، LSP را نقض کرده اید.
رفع: ترکیب را بیش از وراثت ترجیح می دهد
وراثت قراردادها را اعمال می کند. ترکیب انعطاف پذیری را امکان پذیر می کند.
بیایید مستطیل و مربع را با استفاده از رابط ها بازنویسی کنیم:
رابط عمومی ishape {
منطقه دو برابر () ؛
}
مستطیل کلاس عمومی: ishape {
عرض دو برابر عمومی {دریافت کنید ؛ ست }
عمومی دو قد بلند {دریافت کنید. ست }
public double Area() => Width * Height;
}
مربع کلاس عمومی: ishape {
عمومی دو طرف عمومی {دریافت کنید. ست }
public double Area() => Side * Side;
}
اکنون ، هر شکل منطق خاص خود را تعریف می کند.
بدون رفتار اجباری بدون تخلف جای تعجب نیست
**
نکات عملی برای اعمال LSP در کد شما **
برای تعریف انتظارات واضح از رابط ها استفاده کنید
از ترکیب مطلوب هنگامی که رفتار به طور قابل توجهی متفاوت است
تست هایی بنویسید که زیرگروه ها را برای انواع پایه مبادله می کند
از خود بپرسید: “اگر این شیء را با همان رابط کاربری دیگری جایگزین کنم ، آیا هنوز هم همانطور که انتظار می رفت کار می کند؟”
یک مورد در دنیای واقعی: اعتبارسنجی پرداخت
پرداخت کلاس عمومی {
اعتبارسنجی خالی مجازی عمومی () {
// چک های اساسی
}
}
کلاس عمومی بین المللی پرداخت: پرداخت {
اعتبار عمومی Void Veriation () {
Base.Validate () ؛
CheckexChangerates () ؛ // ممکن است پرتاب کند
}
}
اگر سیستم انتظار پرداخت داشته باشد ، و شما از بین المللی عبور می کنید ، ممکن است عوارض جانبی غیر منتظره ای رخ دهد – رفتاری که باید سازگار باشد.
-Better: اعتبار سنجی جداگانه با استفاده از رابط ها:
رابط عمومی ipaymentalidator {
اعتبارسنجی باطل () ؛
}
هر کلاس منطق خاص خود را بر عهده دارد – بدون اینکه یک زنجیره وراثت مشترک را خراب کند.
نتیجه گیری: LSP در مورد اعتماد به معماری شماست
اصل تعویض لیسکوف از سیستم شما در برابر وراثت جعلی محافظت می کند – وقتی همه چیز به نظر می رسد اما کاملاً متفاوت رفتار می کند.
نادیده گرفتن LSP خطاهای کامپایل را پرتاب نمی کند.
در زمان اجرا به شما هشدار نمی دهد.
این فقط سکوت معماری شما را از بین می برد تا اینکه دوباره تغییر شکل کابوس شود.
✔ اگر یک زیر کلاس نمی تواند با خیال راحت کلاس پایه را جایگزین کند ، نباید از آن به ارث ببرد.