Принцип замещения Лисков

29 октября 2009 г.

Формулировка №1: eсли для каждого объекта o1 типа S существует объект o2 типа T, который для всех программ P определен в терминах T, то поведение P не изменится, если o2 заменить на o1 при условии, что S является подтипом T.

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

Короткая версия: Derived classes must be substitutable for their base classes

Примеры

Проверка абстракции на тип

Я уже приводил код проверки абстракции на тип на примере нарушения принципа открытости/закрытости. Теперь мы видим, что класс Repository нарушает еще и принцип замещения Лисков. Дело в том, что внутри класса Repository мы оперируем не только абстрактной сущностью AbstractEntity, но и унаследованными типами. А это значит, что в данном случае подтипы AccountEntity и RoleEntity не могут быть заменены типом, от которого они унаследованы. По определению имеем нарушение.

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

Ошибочное наследование

Проблема

Мы хотим реализовать свой список с интерфейсом IList. Его особенностью будет то, что все записи в нем дублируются.

public class DoubleList<T> : IList<T>
{
    private readonly IList<T> innerList = new List<T>();
 
    public void Add(T item)
    {
        innerList.Add(item);
        innerList.Add(item);
    }
 
    ...  

Данная реализация не представляет никакой опасности, если рассматривать ее изолированно. Взглянем на использование этого класса с точки зрения клиента. Клиент, абстрагируясь от реализаций, пытается работать со всеми объектами типа IList одинаково:

[Fact]
public void CheckBehaviourForRegularList()
{
    IList<int> list = new List<int>();
 
    list.Add(1);
 
    Assert.Equal(1, list.Count);
}
 
[Fact]
public void CheckBehaviourForDoubleList()
{
    IList<int> list = new DoubleList<int>();
 
    list.Add(1);
 
    Assert.Equal(1, list.Count); // fail
}

Поведение списка DoubleList отличается от типичных реализаций IList. Получается, что наш DoubleList не может быть заменен базовым типом. Это и есть нарушение принципа замещения Лисков.

Проблема заключается в том, что теперь клиенту необходимо знать о конкретном типе объекта, реализующем интерфейс IList. В качестве такого объекта могут передать и DoubleList, а для него придется выполнять дополнительную логику и проверки.

Решение

Правильным решением будет использовать свой собственный интерфейс, например, IDoubleList. Этот интерфейс будет объявлять для пользователей поведение, при котором добавляемые элементы удваиваются. Более подробно об этом можно прочитать в дополнении к LSP.

Проектирование по контракту

Есть формальный способ понять, что наследование является ошибочным. Это можно сделать с помощью проектирования по контракту. Бернард Мейер, его автор, сформулировал следующий принцип:

Наследуемый объект может заменить родительское пред-условие на такое же или более слабое и родительское пост-условие на такое же или более сильное. (перефразировано)

Рассмотрим пред- и пост-условия для интерфейса IList. Для функции Add:

  • пред-условие: item != null
  • пост-условие: count = oldCount + 1

Для нашего DoubleList и его функции Add:

  • пред-условие: item != null
  • пост-условие: count = oldCount + 2

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

Другими словами, когда мы используем интерфейс IList, то как пользователи этого базового класса знаем только его пред- и пост-условия. Нарушая принцип проектирования по контракту мы меняем поведение унаследованного объекта.

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

Этот пост входит в серию Принципы проектирования классов (S.O.L.I.D.):


Ссылки

R. Martin: The Liskov Substitution Principle

LosTechies: The Liskov Substitution Principle

Wikipedia: Liskov substitution principle

CodingEfficiency: SOLID – L: Liskov Substitution Principle

DimeCasts: # 92 - Creating SOLID Code: Liskov Substitution Principle

Моя книга «Антихрупкость в IT»

Как достигать результатов в IT-проектах в условиях неопределённости. Подробнее...