Формулировка №1: eсли для каждого объекта o1 типа S существует объект o2 типа T, который для всех программ P определен в терминах T, то поведение P не измениться, если o1 заменить на o2 при условии, что S является подтипом T.
Формулировка №2: подтипы должны быть заменяемы базовыми типами.
Примеры
Проверка абстракции на тип
Я уже приводил код проверки абстракции на тип на примере нарушения принципа открытости/закрытости. Теперь мы видим, что класс Repository нарушает еще и принцип замещения Лисков. Дело в том, что внутри класса Repository мы оперируем не только абстрактной сущностью AbstractEntity, но и унаследованными типами. А это значит, что в данном случае подтипы AccountEntity и RoleEntity не могут быть заменены типом, от которого они унаследованы. По определению имеем нарушение.
Надо заметить, что принципы проектирования взаимосвязаны. Нарушение одного из принципов скорее всего приведет к нарушению одного или нескольких других принципов.
Ошибочное наследование
Проблема
Мы хотим реализовать свой список с интерфейсом IList. Его особенностью будет то, что все записи в нем дублируются.
1: public class DoubleList<T> : IList<T>
2: {
3: private readonly IList<T> innerList = new List<T>();
4:
5: public void Add(T item)
6: {
7: innerList.Add(item);
8: innerList.Add(item);
9: }
10:
11: ...
Данная реализация не представляет никакой опасности, если рассматривать ее изолированно. Взглянем на использование этого класса с точки зрения клиента. Клиент, абстрагируясь от реализаций, пытается работать со всеми объектами типа IList одинаково:
1: [Fact]
2: public void CheckBehaviourForRegularList()
3: {
4: IList<int> list = new List<int>();
5:
6: list.Add(1);
7:
8: Assert.Equal(1, list.Count);
9: }
10:
11: [Fact]
12: public void CheckBehaviourForDoubleList()
13: {
14: IList<int> list = new DoubleList<int>();
15:
16: list.Add(1);
17:
18: Assert.Equal(1, list.Count); // fail
19: }
Поведение списка DoubleList отличается от типичных реализаций IList. Получается, что наш DoubleList не может быть заменен базовым типом. Это и есть нарушение принципа замещения Лисков.
Проблема заключается в том, что теперь клиенту необходимо знать о конкретном типе объекта, реализующем интерфейс IList. В качестве такого объекта могут передать и DoubleList, а для него придется выполнять дополнительную логику и проверки.
Решение
Правильным решением будет использовать свой собственный интерфейс, например, IDoubleList. Этот интерфейс будет объявлять для пользователей поведение, при котором добавляемые элементы удваиваются.
Проектирование по контракту
Есть формальный способ понять, что наследование является ошибочным. Это можно сделать с помощью проектирования по контракту. Бернард Мейер, его автор, сформулировал следующий принцип:
Наследуемый объект может заменить родительское пред-условие на такое же или более слабое и родительское пост-условие на такое же или более сильное. (перефразировано)
Рассмотрим пред- и пост-условия для интерфейса 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
22 коммент.: