Может ли кто-нибудь привести пример принципа замещения Лискова (LSP) с использованием транспортных средств?

Принцип замещения Лискова гласит, что подтип должен быть заменяемым для этого типа (без изменения правильности программы).

  • Может кто-нибудь привести пример этого принципа в области транспортных средств (автомобилей)?
  • Кто-нибудь может привести пример нарушения этого принципа в области транспортных средств?

Я читал о примере квадрата / прямоугольника, но думаю, что пример с транспортными средствами поможет мне лучше понять концепцию.


person random512    schedule 31.12.2013    source источник


Ответы (5)


Для меня это Quoteferrer, 1996 г. (Роберт К. Мартин) обобщает лучший LSP:

Функции, использующие указатели или ссылки на базовые классы, должны иметь возможность использовать объекты производных классов, не зная об этом.

В последнее время в качестве альтернативы абстракциям наследования, основанным на подклассе от (обычно абстрактного) базового / суперкласса, мы также часто используем интерфейсы для полиморфной абстракции. LSP имеет значение как для потребителя, так и для реализации абстракции:

  • Любой код, использующий абстракцию класса или интерфейса, не должен предполагать ничего другого о классе, кроме определенной абстракции;
  • Любое создание подкласса суперкласса или реализация абстракции должны соответствовать требованиям и соглашениям интерфейса к абстракции.

Соответствие LSP

Вот пример использования интерфейса IVehicle, который может иметь несколько реализаций (альтернативно, вы можете заменить интерфейс абстрактным базовым классом с несколькими подклассами - тот же эффект).

interface IVehicle
{
   void Drive(int miles);
   void FillUpWithFuel();
   int FuelRemaining {get; } // C# syntax for a readable property
}

Эта реализация потребителя IVehicle остается в пределах LSP:

void MethodWhichUsesIVehicle(IVehicle aVehicle)
{
   ...
   // Knows only about the interface. Any IVehicle is supported
   aVehicle.Drive(50);
 }

Серьезное нарушение - переключение типа среды выполнения

Вот пример нарушения LSP с использованием RTTI, а затем Downcasting - дядя Боб называет это «вопиющим нарушением»:

void MethodWhichViolatesLSP(IVehicle aVehicle)
{
   if (aVehicle is Car)
   {
      var car = aVehicle as Car;
      // Do something special for car - this method is not on the IVehicle interface
      car.ChangeGear();
    }
    // etc.
 }

Нарушающий метод выходит за рамки ограниченного IVehicle интерфейса и взламывает определенный путь для известной реализации интерфейса (или подкласса, если вместо интерфейсов используется наследование). Дядя Боб также объясняет, что нарушения LSP с использованием поведения переключения типов обычно также нарушают принцип , поскольку для включения новых подклассов потребуется постоянная модификация функции.

Нарушение - предварительное условие усиливается подтипом

Другой пример нарушения - это когда предварительное условие усиливается подтипом:

public abstract class Vehicle
{
    public virtual void Drive(int miles)
    {
        Assert(miles > 0 && miles < 300); // Consumers see this as the contract
    }
 }

 public class Scooter : Vehicle
 {
     public override void Drive(int miles)
     {
         Assert(miles > 0 && miles < 50); // ** Violation
         base.Drive(miles);
     }
 }

Здесь подкласс Scooter пытается нарушить LSP, поскольку он пытается усилить (дополнительно ограничить) предварительное условие для метода Drive базового класса, который miles < 300, до максимального значения менее 50 миль. Это недействительно, поскольку по определению контракта Vehicle разрешает 300 миль.

Точно так же условия публикации не могут быть ослаблены (т.е. ослаблены) подтипом.

(Пользователи Code Contracts в C # отметит, что предварительные условия и постусловия ДОЛЖНЫ быть помещены в интерфейс через _ 11_ класс, и не может быть помещен в классы реализации, что позволяет избежать нарушения)

Невидимое нарушение - злоупотребление реализацией интерфейса подклассом

more subtle нарушение (также терминология дяди Боба) может быть показано с помощью сомнительного производного класса, реализующего интерфейс:

class ToyCar : IVehicle
{
    public void Drive(int miles) { /* Show flashy lights, make random sounds */ }
    public void FillUpWithFuel() {/* Again, more silly lights and noises*/}
    public int FuelRemaining {get {return 0;}}
}

Здесь, независимо от того, как далеко проехал ToyCar, оставшееся топливо всегда будет равно нулю, что удивит пользователей интерфейса IVehicle (т. Е. Бесконечное потребление MPG - вечное движение?). В этом случае проблема в том, что, несмотря на ToyCar выполнение всех требований интерфейса, ToyCar просто по своей сути не является реальным IVehicle и просто штампует интерфейс.

Один из способов предотвратить такое злоупотребление вашими интерфейсами или абстрактными базовыми классами - обеспечить доступность хорошего набора модульных тестов в интерфейсе / абстрактном базовом классе для проверки того, что все реализации соответствуют ожиданиям (и любым предположениям). Модульные тесты также отлично подходят для документирования типичного использования. например этот NUnit Theory отклонит ToyCar от включения его в вашу производственную базу кода:

[Theory]
void EnsureThatIVehicleConsumesFuelWhenDriven(IVehicle vehicle)
{
    vehicle.FillUpWithFuel();
    Assert.IsTrue(vehicle.FuelRemaining > 0);
    int fuelBeforeDrive = vehicle.FuelRemaining;
    vehicle.Drive(20); // Fuel consumption is expected.
    Assert.IsTrue(vehicle.FuelRemaining < fuelBeforeDrive);
}

Редактировать, Re: OpenDoor

Открытие дверей звучит как совершенно другая проблема, поэтому их необходимо разделить соответствующим образом (т.е. S и I в SOLID), например

Добавьте отдельный интерфейс IDoor, и тогда транспортные средства, подобные Car и Truck, будут реализовывать оба интерфейса IVehicle и IDoor, но Scooter и Motorcycle будут реализовывать только IVehicle.

Во всех случаях, чтобы избежать нарушения LSP, код, требующий объектов этих интерфейсов, не должен понижать уровень интерфейса для доступа к дополнительным функциям. Код должен выбрать соответствующий минимальный интерфейс / (суперкласс), который ему нужен, и придерживаться только ограниченной функциональности этого интерфейса.

person StuartLC    schedule 31.12.2013
comment
Спасибо, Стюарт. Учитывая интерфейс IVehicle, у вас может быть автомобиль, грузовик и мотоцикл, каждый из которых реализует этот интерфейс. Автомобиль и грузовик должны иметь метод OpenDoor, тогда как мотоцикл не должен иметь метода OpenDoor. Как бы вы справились с этим сценарием, чтобы соответствовать LSP? - person random512; 31.12.2013
comment
@ random512 Может быть, создать функцию с теми же аргументами и типом возвращаемого значения ... и ничего не делать. Проверьте этот ответ: stackoverflow.com/ a / 38923313/5233122 - person George Geschwend; 10.06.2017
comment
@GeorgeGeschwend - Я обновил ответ некоторое время назад, чтобы включить второй вопрос OP о OpenDoors. Но методы штамповки резины - явный признак злоупотребления дизайном - мы пытаемся сделать что-то подходящим или согласованным с интерфейсом, что в данном случае просто неприменимо. @anotherdave дает еще один хороший пример резиновой штамповки. - person StuartLC; 11.06.2017
comment
@StuartLC Извините, я немного озадачился насчет директора Лискова. После просмотра действительно хорошего видео, в котором Боб Мартин обсуждает SOLID, у меня возникло ощущение, что этот принцип можно использовать в качестве теста. Как базовый класс Vehical, у которого есть функция (поведение) openDoor (), и вы пытаетесь позволить мотоциклу, у которого нет двери, наследовать его ... ну, это нарушает принцип подстановки Лискова. Хотя в реальном мире принято думать, что мотоцикл - это транспортное средство, в объектно-ориентированном моделировании это может быть не так. Так что делать? Не позволяйте Motorcycle наследовать от Vehicle. - person George Geschwend; 13.06.2017
comment
@ random512, поскольку OpenDoor (который) требует двери, это должна быть специальная реализация для конкретной реализации. Например, IVehicleWithDoors (который наследуется от IVehicle) может иметь этот метод, а IVehicle - нет. На автомобилях IV должны быть только компоненты, общие для всех транспортных средств. - person gimlichael; 15.03.2018
comment
@StuartLC, не могли бы вы прояснить это лучше для меня. Это означает, что если у меня не может быть общедоступных свойств / методов в подклассах? Я имею в виду, что если у меня есть общедоступные свойства в подклассе, обязательно знать его реализацию, верно? - person Henrique; 26.11.2020
comment
@Henrique, все дело в том, чтобы не «обмануть» полиморфный дизайн. Разработайте интерфейс и используйте его так, как было задумано. Вы не можете переопределить виртуальное или абстрактное защищенное свойство в подклассе и изменить область видимости на общедоступную, если вы это имели в виду. - person StuartLC; 27.11.2020

Image Я хочу арендовать машину на время переезда. Я звоню в компанию по прокату и спрашиваю, какие у них модели. Но они говорят мне, что мне просто дадут следующую машину, которая появится в наличии:

public class CarHireService {
    public Car hireCar() {
        return availableCarPool.getNextCar();
    }
}

Но они дали мне брошюру, в которой говорится, что все их модели имеют следующие особенности:

public interface Car {
    public void drive();
    public void playRadio();
    public void addLuggage();
}

Звучит как раз то, что я ищу, поэтому я заказываю машину и уезжаю счастливой. В день переезда у моего дома появляется машина Формулы-1:

public class FormulaOneCar implements Car {
    public void drive() {
        //Code to make it go super fast
    }

    public void addLuggage() {
        throw new NotSupportedException("No room to carry luggage, sorry."); 
    }

    public void playRadio() {
        throw new NotSupportedException("Too heavy, none included."); 
    }
}

Я недоволен, потому что их брошюра обманула меня - не имеет значения, есть ли у болида Формулы 1 поддельный багажник, который выглядит, в нем можно хранить багаж, но не открывается, это бесполезно для переезда!

Если мне говорят, что «это то, что делают все наши машины», то любой автомобиль, который мне дают, должен вести себя таким же образом. Если я не могу доверять деталям в их брошюре, это бесполезно. В этом суть принципа замещения Лискова.

person anotherdave    schedule 09.01.2014
comment
На самом деле это скорее пример принципа разделения интерфейсов, не так ли? - person trnelson; 17.03.2014
comment
Что ж, принцип разделения интерфейса решит проблему с другой стороны (поскольку интерфейс тогда не будет давать обещаний, которые конкретный класс не может выполнить) - person anotherdave; 18.03.2014
comment
Что ж, анализируя пример, могу сделать вывод: методы нужно реализовать в контракте (интерфейсе), т.е. исключение недопустимо. - person karlihnos; 13.07.2017
comment
@karlihnos Я думаю, что это на более высоком уровне, чем то, если вы не генерируете исключение - как клиент вы должны иметь возможность зависеть от дочернего класса как от экземпляра его родительского. Дочерний элемент должен предлагать дополнительные функциональные возможности или другую реализацию, но не должен удалять вещи, которые, как мы знаем, верны в отношении родительского класса. Тем не менее, NotSupportedException и подобные исключения определенно являются запахом нарушения LSP. - person anotherdave; 13.07.2017
comment
Я не чувствую его LSP, потому что согласно дяде Бобу. жирный шрифт. Функции, использующие указатели или ссылки на базовые классы, должны иметь возможность использовать объекты производных классов, не зная об этом ». Мы нигде не используем эту концепцию (в этом примере). - person spandey; 24.01.2018
comment
@ spandey15 конечно, вы должны читать между строк, я думаю, тогда :) я хотел сказать, что функция, имеющая ссылку на Car, не могла бы использовать экземпляры своего производного класса, FormulaOneCar, не зная об этом - поэтому нарушение LSP - person anotherdave; 24.01.2018

Принцип замены Лискова гласит, что объект с определенным интерфейсом может быть заменен другим объектом, который реализует тот же интерфейс, сохраняя при этом всю правильность исходной программы. Это означает, что не только интерфейс должен иметь точно такие же типы, но и поведение также должно оставаться правильным.

В транспортном средстве у вас должна быть возможность заменить деталь на другую, и машина продолжит работать. Допустим, у вашего старого радио нет цифрового тюнера, но вы хотите слушать HD-радио, поэтому вы покупаете новое радио с HD-приемником. У вас должна быть возможность вынуть старое радио и подключить новое, если у него такой же интерфейс. На первый взгляд, это означает, что электрическая вилка, соединяющая магнитолу с автомобилем, должна быть такой же формы на новом радиоприемнике, как и на старом. Если автомобильная вилка прямоугольная и имеет 15 контактов, то разъем нового магнитолы должен быть прямоугольным и иметь 15 контактов.

Но есть и другие соображения, помимо механической посадки: электрические характеристики вилки также должны быть такими же. Если контакт 1 разъема старого радио имеет +12 В, то контакт 1 разъема нового радио также должен быть +12 В. Если контакт 1 на новом радио был контактом "левый динамик", в радио могло произойти короткое замыкание или перегорел предохранитель. Это было бы явным нарушением LSP.

Вы также можете рассмотреть ситуацию перехода на более раннюю версию: допустим, ваше дорогое радио умирает, и вы можете позволить себе только AM-радио. У него нет стереовыхода, но он имеет тот же разъем, что и ваш существующий радиоприемник. Скажем, в спецификации вывод 3 является выходом для левого динамика, а контакт 4 - для правого динамика. Если ваше AM-радио воспроизводит монофонический сигнал с обоих контактов 3 и 4, вы можете сказать, что его поведение согласовано, и это будет приемлемой заменой. Но если ваше новое AM-радио воспроизводит звук только на контакте 3 и ничего не на контакте 4, звук будет несбалансированным, и это, вероятно, не будет приемлемой заменой. Эта ситуация также нарушит LSP, потому что, хотя вы можете слышать звуки и не перегорать предохранители, радио не соответствует полной спецификации интерфейса.

person John Deters    schedule 03.01.2014

Во-первых, вам нужно определить, что такое транспортное средство и автомобиль. Согласно Google (не очень полные определения):

Транспортное средство: вещь, используемая для перевозки людей или товаров, особенно. на суше, например в автомобиле, грузовике или тележке.

Автомобиль:
дорожное транспортное средство, обычно с четырьмя колесами, приводимое в действие двигателем внутреннего сгорания или
электродвигателем и способное перевозить небольшое количество людей

Итак, автомобиль - это средство передвижения, но транспортное средство - это не автомобиль.

person user2810910    schedule 31.12.2013

На мой взгляд, для архивирования LSP подтипы никогда не могут добавлять новые общедоступные методы. Только частные методы и поля. И, конечно, подтипы могут переопределять методы базового класса. Если у подтипа есть единственный общедоступный метод, которого нет у базового типа, вы просто не можете заменить подтип базовым типом. Если вы передаете экземпляр клиентскому методу, при этом вы получаете экземпляр подтипа, но тип параметра является базовым типом, или если у вас есть коллекция типа базового типа, частью которой также являются подтипы, то как вы вообще можете вызвать метод класса подтипа не запрашивая его тип, используя статус if, и если тип совпадает, выполните приведение к этому подтипу, чтобы вызвать на нем метод.

person brighty    schedule 10.02.2015
comment
не могли бы вы перепроверить ваши ответы? - person spandey; 09.05.2018
comment
Взгляните на публикацию StuartLC от 31 декабря 2013 года в 17:40, и вы заметите то же мнение, что и я. Стюарт упомянул, что следует избегать понижения интерфейса для доступа к дополнительным функциям, доступным только в подклассе. Почему? Потому что это нарушает LSP. - person brighty; 10.05.2018
comment
LSP относится к классам, а не к типам, и добавление нового общедоступного метода в подкласс вполне нормально. - person ComDubh; 09.01.2020