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

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

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

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

Ответ 1

Для меня эта цитата 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, использующие поведение переключения типов, также обычно нарушают принцип Open и Closed, поскольку для размещения новых подклассов потребуется постоянное изменение функции.

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

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

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 миль.

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

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

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

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 (т. 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" в ТВЕРДОМ), например

  • на новом интерфейсе IVehicleWithDoors, который может наследовать IVehicle
  • или лучше IMO, на отдельном интерфейсе IDoor, и тогда такие Car как Car и Truck будут реализовывать интерфейсы IVehicle и IDoor, а Scooter и Motorcycle нет.
  • или даже 3 интерфейса, IVehicle (Drive()), IDoor (Open()) и IVehicleWithDoors которые наследуют оба из них.

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

Ответ 2

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

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."); 
    }
}

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

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

Ответ 3

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

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

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

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

Ответ 4

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

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

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

Таким образом, автомобиль является транспортным средством, но транспортное средство не является автомобилем.

Ответ 5

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