Формулировка №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.):
- Принцип единственности ответственности (The Single Responsibility Principle)
- Принцип открытости/закрытости (The Open Closed Principle)
- Принцип замещения Лисков (The Liskov Substitution Principle)
- Принцип разделения интерфейса (The Interface Segregation Principle)
- Принцип инверсии зависимости (The Dependency Inversion Principle)
Ссылки
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