اصل جایگزینی لیسکوف در اصول SOLID
اصل Liskov Substitution Principle در SOLID چیست؟
Liskov Substitution Principle یا به اختصار LSP که در فارسی به آن «اصل جایگزینی لیسکوف» یا «اصل جانشینی لیسکوف» گفته میشود، یک اصل مهندسی نرمافزار و اصل سوم از اصول طراحی SOLID است.
این اصل بیان میکند که اشیاء یک سوپرکلاس باید بدون تغییر در صحت برنامه با اشیاء یک زیرکلاس جایگزین شوند. به عبارت دیگر، اگر شئِ کلاسِ پدر قادر به انجام کاری باشد، پس همه اشیاء کلاسِ فرزند نیز باید قادر به انجام آن کار باشند. اگر کلاس فرزند قادر به انجام کاری نباشد که کلاس پدر میتواند آن را انجام دهد، اصل جایگزینی Loskov را نقض کردهایم.
LSP مبتنی بر مفهوم زیرگروه سازیِ رفتاری است، که در آن زیرگروهها با رفتارهایشان (behaviors) تعریف می شوند، نه با پیادهسازی (implementation).
به طور خلاصه، اصل جایگزینی Liskov بیان میکند که اشیاء یک کلاس مشتق شده باید بتوانند بدون تأثیر بر صحت برنامه، جایگزین اشیاء کلاس پایه شوند. یعنی تا زمانی که رفتار یک شی با رفتار سوپرکلاس یکسان باشد، میتوان آن را جایگزین سوپرکلاس کرد. این میتواند منجر به کد انعطافپذیرتر و قابل استفاده مجدد و همچنین کاهش مشکلات ناخواسته شود.
یک مثال از اصل جایگزینی لیسکوف در زبان برنامه نویسی
در اینجا یک کلاس «وسیله نقلیه» به نام Vehicle داریم که دارای سه متد «روشن کردن موتور» startEngine() ، «حرکت کردن» doMovement() و «پرواز کردن» fly() است.
حالا این کلاس سه فرزند با عناوین «ماشین» Car ، «دوچرخه» Bicycle و «هواپیما» Airplane دارد.

حالا کلاس وسیله نقلیه با نام Vehicle را به صورت زیر داریم:
class Vehicle {
public function startEngine() {
return "Start the engine";
}
public function doMovement() {
return "move";
}
public function doMovement() {
return "fly";
}
}
این کلاس دارای سه متد برای «روشن کردن موتور» ، «حرکت کردن» و «پرواز کردن است». پس اگر هواپیما فرزند این کلاس باشد، میتواند به صورت زیر نوشته شود:
class Airplane extends Vehicle {
}
در کد بالا هواپیما فرزند وسیله نقلیه است. پس هر سه متد آن را به ارث میبرد و میتواند استفاده کند.
حتی میتواند برای خودش یک متد اضافه داشته باشد که در کلاس پدر وجود ندارد. مانند کد زیر:
class Airplane extends Vehicle {
public function beep() {
return "Beep beep!";
}
}
همانطور که در کد بالا مشاهده میکنید، هواپیما علاوه بر اینکه سه متد از کلاس پدر به ارث برده است، حالا یک متد اضافه به نام beep هم دارد که میتواند بوق بزند.
حالا فرض کنید دوچرخه میخواهد فرزند کلاس Vehicle باشد. اما دوچرخه موتور ندارد و همچنین نمیتواند پرواز کند. کد زیر را در نظر بگیرید:
class Bicycle extends Vehicle {
// The bicycle has no engine
// leave it without implementation or throw exception
}
در اینجا دوچرخه نمیتواند دو متد از کلاسِ پدر را به ارث ببرد، پس اصل جایگزینی لیسکوف را نقض میکند. پس در کلاس دوچرخه یک Exception را برمیگرداند که اعلام کند دوچرخه موتور ندارد و پرواز کردن دوچرخه هم ممکن نیست.
در اینجا چه اتفاقی رخ خواهد داد؟
کاربری که در حال استفاده از کلاس Bicycle میباشد، از این تغییرات اطلاعی ندارد و ناگهان یک چیز غیرمنتظره یا یک Exception در برنامه او رخ خواهد داد که مجبور است برای حل آن مشکل، برنامه خود را تغییر بدهد.
اینجا اصل LSP نقض میشود. زیرا کلاس Bicycle، رفتار و ویژگیهای کلاسِ پدر را تغییر داده است و برنامه نویس مجبور شده است برای حل این مشکل، کد خود را ویرایش کند و در برنامه خود تغییراتی اعمال کند.
راه حل بهتر برای حل مسئله با اصل جایگزینی لیسکوف
برای اینکه اصل لیسکوف را نقض نکنیم، حالا کد خود را دوباره بازنویسی میکنیم.
class Vehicle {
public function doMovement() {
return "move";
}
}
class DeviceWithoutEngine extends Vehicle {
public function startEngine() {
return "Basic functionality of moving for device without engine";
}
}
class DeviceWithEngine extends Vehicle {
public function startEngine() {
return "Basic engine start functionality";
}
}
در کد بالا، کلاس Vehicle دارای متد doMovement() است. همان آپشن حرکت کردن.
کلاس DeviceWithoutEngine مخصوص وسایل نقلیهای است که بدون موتور حرکت میکنند، همچنین فرزند کلاس Vehicle است و متد doMovement() را هم به ارث میبرد.
کلاس DeviceWithEngine مخصوص وسایل نقلیهای است که موتور دارند و برای حرکت باید موتور خود را روشن کنند، همچنین فرزند کلاس Vehicle است و متد doMovement() را هم به ارث میبرد.
پس کلاس های «ماشین»، «دوچرخه» و «هواپیما» را به صورت زیر مینویسیم:
class Car extends DeviceWithEngine {
public function openRoof() {
return "Roof opened.";
}
}
class Bicycle extends DeviceWithoutEngine {
public function beep() {
return "Beep beep!";
}
}
class Airplane extends DeviceWithEngine {
public function fly() {
return "Fly.";
}
}
در کد بالا سه عنصر را داریم که هر کدام را به صورت جداگانه تحلیل میکنیم:
کلاس ماشین: این کلاس فرزند کلاس DeviceWithEngine است. یعنی علاوه بر اینکه متد openRoof() را در خود دارد، متد startEngine() برای وسایل نقلیهای که موتور دارند و متدdoMovement() را هم به ارث میبرد.
کلاس دوچرخه: این کلاس فرزند کلاس DeviceWithoutEngine است. یعنی علاوه بر اینکه متد beep() را در خود دارد، متد startEngine() برای وسایل نقلیهای که موتور ندارند و متد doMovement() را هم به ارث میبرد.
کلاس هواپیما: این کلاس فرزند کلاس DeviceWithEngine است. یعنی علاوه بر اینکه متد fly() را در خود دارد، متد startEngine() برای وسایل نقلیهای که موتور دارند و متدdoMovement() را هم به ارث میبرد.
حالا میتوانیم خروجی را با کد زیر چاپ کنیم:
$car = new Car();
$bicycle = new Bicycle();
$airplane = new Airplane();
echo $car->doMovement() . "\n";
echo $car->startEngine() . "\n";
echo $car->openRoof() . "\n";
echo "\n";
echo $bicycle->doMovement() . "\n";
echo $bicycle->startEngine() . "\n";
echo $bicycle->beep() . "\n";
echo "\n";
echo $airplane->doMovement() . "\n";
echo $airplane->startEngine() . "\n";
echo $airplane->fly() . "\n";
خروجی کد به شکل زیر خواهد بود:
move
Basic engine start functionality
Roof opened.
move
Basic functionality of moving for device without engine
Beep beep!
move
Basic engine start functionality
Fly.