Страницы

Поиск по вопросам

среда, 19 декабря 2018 г.

Может ли нарушаться принцип подстановки Лисков при использовании интерфейса/абстрактного класса?

На размышления меня натолкнула вот эта статья: http://blog.byndyu.ru/2009/10/blog-post_29.html Приведу немного переработанный пример из нее:
public interface IList {
public void add(int e);
}
public class List implements IList{
@Override public void add(int e) { // добавляет элемент }
}
public class DoubleList implements IList{
@Override public void add(int e) { // добавляет элемент // добавляет элемент }
}
Метод add в классе DoubleList добавляет элемент дважды. В клиентском коде можно написать примерно так:
public void LSPTest(IList list){ int oldLen = list.getLength();
list.add(1);
int newLen = list.getLength();
if(newLen - oldLen == 1){ // делать что то полезное } }
Очевидно, что поведение программы будет разным, в зависимости от того, получит функция LSPTest объект класса List или DoubleList. Но разве это нарушает LSP? Ведь DoubleList наследует не класс List, а интерфейс IList. Интерфейс не может задавать никаких предусловий и постусловий (в данном случае точно не задает). И интерфейсы же для того и написаны, что бы иметь разные реализации, иногда имеющие совсем мало общего. Я всегда считал, что если бы, например, DoubleList был наследован от List, то выделение интерфейса и опускание классов на один уровень - это как раз решение проблемы при нарушении LSP. По-моему это называется факторизация. И к тому же, LSP говорит о том, что прогрмма не должна меняться, если вместо объекта базового класса подставить объект производного. Но как можно подставить что то вместо объекта базового класса, если базовый класс является абстрактным? Или тем более интерфейсом? Вместо него нельзя ничего подставить, потому что его просто нельзя создать.


Ответ

Интерфейс не может задавать никаких предусловий и постусловий
Формально вы правы. Объявление интерфейса само по себе не задает "материального" (назовем это так) контракта -- т.е. контракта, который может быть проверен на этапе компиляции или выполнения. Т.е. нет никаких средств, гарантирующих, что все классы, реализующие интерфейс, будут реализовывать его одинаково с т.з. LSP.
Хитрость заключается в том, что при использовании интерфейсов мы всегда говорим о "нематериальном" контракте. Он выражается в названии методов и в комментариях. Это такой неформальный уговор среди разработчиков. В случае с IList из .NET это выглядит так*:
// Adds an item to the list. The exact position in the list is // implementation-dependent, so while ArrayList may always insert // in the last available location, a SortedList most likely would not. // The return value is the position the new element was inserted in. int Add(Object value);
Как видно из комментария, если класс-наследник будет добавлять в список сразу два элемента, он нарушит два пункта из комментария: что добавлять должен один элемент и что метод возвращает позицию добавленного элемента (а что возвращать в случае двух элементов?).
В то же время, комментарии к методу ICollection.Add() ничего не говорят о том, что этот метод предполагает делать. И, как мы видим, это согласуется с тем, что, например, HashSet (реализующий ICollection) после двух вызовов метода Add() с одинаковыми аргументами будет содержать лишь один элемент.
Это тонкий лед, т.к., повторюсь, нет средств для обеспечения выполнения обозначенного контракта -- это лежит целиком на совести программиста. Более того, интерфейс может и не продоставлять никаких комментариев и о его контракте придется как-то догадываться -- из документации, интернета, от коллег.
Но тем не менее 99.9% разработчиков, увидев название интерфейса IList, будут ожидать, что метод Add() добавит в список один элемент. Если же вдруг этот метод будет добавлять два элемента, не добавлять ничего, или даже удалять, это очень удивит эти 99.9%. В этом, собственно, и заключается нарушение LSP.
Если вы пока не очень понимаете LSP, то забудьте на время про интерфейсы и посмотрите на примеры использования наследования классов. Например, на классическую проблему прямоугольника и квадрата

*желающим попудрить себе мозг читать ниже.
Метод IList.Add(), как мы уже видели, явно в комментарии обозначает свой контракт. А что насчет IList? Как видно, он наследуются от ICollection, а это значит, что на метод Add() не налагается ограничений. Т.о. класс DoubleList : IList формально уже не будет нарушать LSP. Но бьюсь об заклад, ваши коллеги не будут рады такой реализации :).

Комментариев нет:

Отправить комментарий