По сути, мы хотим, чтобы все было разумно.
Рассмотрим следующую проблему:
Мне дали группу прямоугольников, и я хочу увеличить их площадь на 10%. Поэтому я устанавливаю длину прямоугольника в 1,1 раза больше, чем была раньше.
public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
foreach(var rectangle in rectangles)
{
rectangle.Length = rectangle.Length * 1.1;
}
}
Теперь в этом случае длина всех моих прямоугольников теперь увеличена на 10%, что увеличит их площадь на 10%. К сожалению, кто-то фактически передал мне смесь квадратов и прямоугольников, и когда длина прямоугольника была изменена, изменилась и ширина.
Мои модульные тесты пройдены, потому что я написал все свои модульные тесты, чтобы использовать набор прямоугольников. Теперь я внес небольшую ошибку в свое приложение, которая может оставаться незамеченной в течение нескольких месяцев.
Хуже того, Джим из бухгалтерии видит мой метод и пишет какой-то другой код, который использует тот факт, что если он передаст квадраты моему методу, он получит очень хорошее увеличение размера на 21%. Джим счастлив, и никто не мудрее.
Джим получил повышение за отличную работу в другом отделе. Альфред присоединяется к компании как младший. В своем первом сообщении об ошибке Джилл из рекламного агентства сообщил, что пропуск квадратов для этого метода приводит к увеличению на 21% и требует исправления ошибки. Альфред видит, что квадраты и прямоугольники используются повсюду в коде, и понимает, что разрыв цепочки наследования невозможен. Он также не имеет доступа к исходному коду учета. Таким образом, Альфред исправляет ошибку следующим образом:
public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
foreach(var rectangle in rectangles)
{
if (typeof(rectangle) == Rectangle)
{
rectangle.Length = rectangle.Length * 1.1;
}
if (typeof(rectangle) == Square)
{
rectangle.Length = rectangle.Length * 1.04880884817;
}
}
}
Альфред доволен своими навыками хакерского убера, и Джилл подписывает, что ошибка исправлена.
В следующем месяце никому не платят, потому что бухгалтерия зависела от того, можно ли передать квадраты IncreaseRectangleSizeByTenPercent
методу и получить увеличение на 21%. Вся компания переходит в режим «исправление приоритета 1», чтобы отследить источник проблемы. Они прослеживают проблему до решения Альфреда. Они знают, что им нужно поддерживать и бухгалтерию, и рекламу. Таким образом, они решают проблему, идентифицируя пользователя с помощью вызова метода следующим образом:
public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
IncreaseRectangleSizeByTenPercent(
rectangles,
new User() { Department = Department.Accounting });
}
public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles, User user)
{
foreach(var rectangle in rectangles)
{
if (typeof(rectangle) == Rectangle || user.Department == Department.Accounting)
{
rectangle.Length = rectangle.Length * 1.1;
}
else if (typeof(rectangle) == Square)
{
rectangle.Length = rectangle.Length * 1.04880884817;
}
}
}
И так далее.
Этот анекдот основан на реальных ситуациях, с которыми ежедневно сталкиваются программисты. Нарушение принципа подстановки Лискова может привести к очень тонким ошибкам, которые обнаруживаются только спустя годы после того, как они написаны, и к тому времени, когда исправление нарушения нарушит кучу вещей, а не исправление его разозлит вашего крупнейшего клиента.
Есть два реалистичных способа решения этой проблемы.
Первый способ - сделать Rectangle неизменным. Если пользователь Rectangle не может изменить свойства длины и ширины, эта проблема исчезнет. Если вы хотите прямоугольник с другой длиной и шириной, вы создаете новый. Квадраты могут наследовать от прямоугольников счастливо.
Второй способ - разорвать цепочку наследования между квадратами и прямоугольниками. Если квадрат определяются как имеющие единственной SideLength
собственностью и прямоугольники имеют Length
и Width
имущество , и нет наследства, то невозможно случайно сломать вещи, ожидая прямоугольник и получить квадрат. В терминах C # вы можете использовать seal
класс прямоугольников, который гарантирует, что все прямоугольники, которые вы когда-либо получали, на самом деле являются прямоугольниками.
В этом случае мне нравится способ решения проблемы «неизменяемыми объектами». Идентичность прямоугольника - это его длина и ширина. Имеет смысл, что когда вы хотите изменить идентичность объекта, то, что вы действительно хотите, это новый объект. Если вы потеряете старого клиента и получите нового, вы не меняете Customer.Id
поле со старого клиента на нового, вы создаете нового Customer
.
Нарушения принципа подстановки Лискова часто встречаются в реальном мире, в основном потому, что много кода написано людьми, которые некомпетентны / испытывают нехватку времени / не заботятся / делают ошибки. Это может и действительно приводит к некоторым очень неприятным проблемам. В большинстве случаев вы предпочитаете композицию вместо наследования .