Принцип замещения Лискова (LSP) может быть описан как
Экземпляры производного класса должны использоваться через интерфейс его базового класса без того, чтобы клиенты базового класса могли заметить разницу.
Другими словами, подтипы могут работать в качестве замены своих базовых типов.
Подтипирование – это концепция, широко используемая в языках программирования. Подтип – это специализация базового типа. В принципе, он должен обладать всеми атрибутами базового типа и должен быть способен выполнять все задачи, которые может выполнять его базовый тип. Он также может иметь некоторые новые атрибуты и выполнять свои собственные новые задачи.
Что происходит, когда нарушается LSP?
Вместо того, чтобы смотреть какой-то код, как обычно, давайте воспользуемся другим подходом и проиллюстрируем это на примере из реальной жизни.
Джон, бизнесмен, приезжает в город на встречу. Он решает, что, пока он там, он хотел бы осмотреть достопримечательности города.
Он понимает, что для этого ему понадобится транспортное средство . Он звонит своей подруге Джейн, которая живет в городе, и спрашивает ее, сможет ли он найти транспортное средство , когда приземлится в аэропорту. Джейн сообщает ему, что он может найти его прямо через дорогу от выхода из аэропорта.
Джон доволен и с нетерпением ждет идеального путешествия. Однако, когда Джон приземляется в городе и выходит из аэропорта, он находит железнодорожную станцию через дорогу. Его возбуждение сменяется хмурым взглядом.
Так что же пошло не так? Джон предполагал, что любое транспортное средство выполнит эту работу за него. Он не ожидал, что некоторые транспортные средства не подходят для его целей.
Вы можете назвать Джона немного небрежным, но теперь давайте посмотрим, как это переводится в код.
Джон является |/клиентом или |/вызывающим классом . Это ссылка на список доступных транспортных средств . План Джона – это задача, которую необходимо выполнить. Транспортное средство – это класс, который обещал выполнить задание для Джона.
class JohnsJourney { private Location currentLocation; private VehicleProvider vehicleProvider; ... private void travel(Location start, Location end) { Vehicle availableVehicle = vehicleProvider.getAvailableVehicle(); currentLocation = availableVehicle.move(start, end); } } class Vehicle { String name; double maxSpeed; /** * Move between two locations * @param start * @param end * return final location */ public Location move(Location start, Location end) { Location currentLocation = start; while(currentLocation != end) { keepMoving(...); } return currentLocation; } /** * Move on a pre-defined route * @param route * return final location */ public abstract Location moveOnARoute(Route route); ... }
Джону все равно, каким будет Транспортное средство, пока оно ему не понадобится. Все, что он знает, – это то, что это решило бы его проблему. Вы можете рассмотреть это динамическая привязка . К сожалению, одно из транспортных средств, Поезд, является самозванцем. Он не следует поведению своего предшественника. Он перемещается только по указанным маршрутам и ничего не делает, если его просят переместиться между любыми двумя местоположениями.
class RentalCar extends Vehicle { //doesn't override move @Override public Location moveOnARoute(Route route) { move(route.getStart(), route.getEnd()); } ... } class Train extends Vehicle { @Override public Location move(Location start, Location end) { //do nothing return start; } @Override public Location moveOnARoute(Route route) { Location currentLocation = route.getStart(); while(currentLocation != route.getEnd()) { keepMoving(...); } return currentLocation; } ... }
Если бы транспортное средство было Арендованным автомобилем, все было бы хорошо. Но это Поезд, и он нарушает контракт, который класс транспортных средств представил Джону.
LSP стремится избежать таких неожиданных ситуаций. Это подводит нас к более низкоуровневому объяснению LSP – когда мы пытаемся вызвать метод, реализованный дочерним классом, используя ссылку на базовый класс, результат не должен нарушать контракт, представленный базовым классом. Если нет, дочерний класс нарушает LSP, и базовый класс больше не подходит для динамической привязки.
Следование этому принципу не означает, что все дочерние классы будут возвращать один и тот же результат или что вы не должны переопределять методы. Важно понимать, что контракт является основой этого принципа. Животное будет двигаться, но Собака будет двигаться иначе, чем Кенгуру. Тем не менее, гарантируется, что они будут двигаться, и это делает их хорошими подтипами.
Как мы можем улучшить ситуацию?
Добавьте еще один уровень абстракции – Супертип должен предоставлять только контракты, которые смогут выполнить все его подтипы. Если некоторые из этих подтипов работают определенным образом, но некоторые из них работают по-другому, вполне вероятно, что контракт раскрывается на длинном уровне. Давайте посмотрим, как мы это исправим.
В нашем примере все транспортные средства не могут использовать метод move(). Следовательно, неправильно использовать этот метод на этом уровне. Мы скорее можем перевести его в более специализированный класс, называемый Личным транспортным средством.
class Vehicle { String name; double maxSpeed; /** * Move on a pre-defined route * @param route * return final location */ public abstract Location moveOnARoute(Route route); ... } class PersonalVehicle extends Vehicle { /** * Move between two locations * @param start * @param end * return final location */ public Location move(Location start, Location end) { Location currentLocation = start; while(currentLocation != end) { keepMoving(...); } return currentLocation; } @Override public Location moveOnARoute(Route route) { move(route.getStart(), route.getEnd()); } ... }
Оригинал: “https://dev.to/abh1navv/how-solid-is-your-code-liskov-s-substitution-principle-d55”