Дополнение к LSP

26 февраля 2012 г.

Прежде, чем прочитать дополнение LSP, изучите и попробуйте применить Принцип замещения Лисков (Liskov Substitution Principle).


Недавно у меня состоялся разговор с опытным программистом, который разбирался, как программирование по контракту связано с LSP. В примере статьи я использовал интерфейс IList, создал объект DoubleList и унаследовал DoubleList от IList. Дальше, при каждом использовании DoubleList в проекте будет происходить нарушение LSP. Это можно сразу понять, если обратить внимание на контракт интерфейса IList.

Вопрос

Сейчас я более детально напишу, какие будут возникать проблемы, если не исправить ошибочное наследование. Это будет ответ на вопрос из нашего с Мурадом чата: «Не понимаю, почему тебе IDoubleList не нравится? DoubleList ведь наследуется от IList».

Пример

У нас в проекте есть интерфейс IList и два наследника: List и DoubleList. DoubleList своеобразно реализует метод Add - он добавляет переданный ему объект дважды. Это значит, когда мы добавим один элемент в DoubleList, свойство Count вернет цифру 2, а по индексу 0 и 1 вернутся два одинаковых элемента.

Рассмотрим пример, где в публичный метод SomeMethod передается абстракция IList:

public void SomeMethod(IList<string> list)
{
    List<string> urls = urlService.GetUrls();

    foreach(string url in urls)
    {
        if (SomeBoolLogic(url))
            list.Add(url);
    }

    if (urls.Count > list.Count)
        throw Exception();
}

Результаты сравнения свойств Count (11 строка) будут отличаться в зависимости от того, какой из наследников интерфейса IList будет передан в метод SomeMethod. Если вы передадите экземпляр класса List, то элементов будет меньше, а если DoubleList, то больше (функция Add добавляет по два объекта за раз). Т.е. результат этой функции будет зависеть от конкретной реализации IList.

Программист, который привык, что эта функция работает с любым наследником IList, посидев однажды в дебагере, поймет, что иногда (когда приходит DoubleList) эта функция не работает. Программист добавит код-костыль, чтобы поправить последствия неправильного наследования DoubleList от IList:

public void SomeMethod(IList<string> list)
{
    List<string> urls = urlService.GetUrls();
    foreach(string url in urls)
    {
        if (SomeBoolLogic(url))
            list.Add(url);
    }

    // начало костыля
    int realCount;

    if (list is DoubleList)
        realCount = list.Count / 2;
    else
        realCount = list.Count;
    // конец костыля

    if (urls.Count > realCount)
        throw Exception();
}

Нарушение контракта интерфейса IList привело к использованию оператора is (или as). Если какой-то разработчик создаст TripleList, унаследует его от IList, то еще больше усложнит жизнь своего проекта.

Корни проблемы лежат в непонимании принципов ООП. Вы должны свободно использовать абстракцию у себя в коде, без лишних проверок на тип. Использование операторов is и as должно быть очень веско обосновано, т.к. чаще всего они являются первыми признаками нарушения LSP.

Пример №2

Или давайте еще проще. Когда вы берете элементы по индексу, то ожидаете, что индекс будет считаться с 0. Возьмите наследника IList, сделайте индекс не с 0, а с 1 и вас возненавидит вся команда. Причем желательно, чтобы в системе был другой IList, который считает с 0.

Ваши примеры

Пример с IList очень простой. Возьмите какой-нибудь интерфейс/абстрактный класс из своей системы, опиши его контракт, сделайте наследника, в котором контракт будет нарушен. После этого передайте неправильного наследника в функции, где использовалась абстракция. Вы сразу поймете, что нарушение контракта будет подталкивать вас к нарушению LSP.

Я рад, что теории вызывают сомнения и требуют дополнительных доказательств. Давайте углубляться в эти темы, чтобы всплывали интересные грани.

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

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