Ответ 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, код, который требует объектов этих интерфейсов, не должен снижать интерфейс для доступа к дополнительным функциям. Код должен выбрать соответствующий минимальный интерфейс/(супер) класс, который ему нужен, и придерживаться только контрактной функциональности этого интерфейса.