Правила наследования типов. Принцип «Liskov Substitution Principle».



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

Формулировка №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, то как пользователи этого базового класса знаем только его пред- и пост-условия. Нарушая принцип проектирования по контракту мы меняем поведение унаследованного объекта.

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

 


15. C#. Приведение типов. Операторы as и is.

 

Динамическая идентификация типов (runtime type identification — RTTI) позволяет определить тип объекта во время выполнения программы, что необходимо во многих ситуациях. Например, можно совершенно точно узнать, на объект какого типа в дей-ствительности указывает ссылка на базовый класс. Еще одно применение RTTI — за-ранее проверить, удачно ли будет выполнена операция приведения типа, не допустив возникновения исключения, связанного с некорректно заданной операцией приведе-ния типа. Динамическая идентификация типов также является ключевым компонен-

том средства отражения (информации о типе).

В С# предусмотрено три ключевых слова, которые поддерживают динамическую идентификацию типов: i s , as и typeof. Рассмотрим назначение каждого из них в отдельности.


Дата добавления: 2018-05-13; просмотров: 406; Мы поможем в написании вашей работы!

Поделиться с друзьями:






Мы поможем в написании ваших работ!