Для меня это 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