Архив за месяц: Август 2017

LSP

Явное грубое нарушение LSP (простой пример)

function f(Shape p) {
    if (p isinstance Circle) {
       drowCirle(p);
    }
    if (p instance Rectangle) {
        drowRectangle(p);
    }
}

Заключается в том, что мы вынуждены работать с объектом одного супертипа по-разному, кроме этого при добавлении новых видов фигур придется модифицировать этот код.
Этот пример показывает что когда мы говорим про LSP совсем не обязательно речь будет идти про входящие параметры для функции (про сигнатуру, хотя часто когда кто-то рассказывает об этом, то речь идет именно о наследовании и о переопределении методов и неизменности сигнатуры), а речь идет о поведении программы при использовании пришедшего типа данных. LSP призывает нас писать код так, чтобы вызывающий его код не заботился о возможных side-effect, а работал одинаково для любого подтипа супертипа. Если программа при этом сломается, то принцип считается нарушенным, если программа будет корректно работать, можно сказать что принцип соблюдается.
Разберем более сложный пример.

class Rectangle() {
    property Width;
    property Height;
    virtual function setWidth(int Value);
    virtual function setHeight(int Value);
    function getWidth() {return self.Width;}
    function getHeight() {return self.Height;}
}

class Square() ext Rectangle {
    function setWidth(inv Value) {
        self.Width = Value;
        self.Height = Value;
    }
    function setHeight(inv Value) {
        self.Width = Value;
        self.Height = Value;
    }
}
Есть функция f которая работает с этим кодом
function f(Rectangle t) {
    t.setWidth(5);
    t.setHeight(4);
    assert(t.getWidth() * t.getHeight(), 20)
}

Очевидно, что для случая с квадратом тут произойдет ошибка, ведь программист функции f делает предположение что если у прямоугольника установить стороны в определенные значения, то площадь будет равной произведению сторон. Эта функция показывает нарушение принципа LSP допущенного в коде класса Square.
В статье http://www.webcitation.org/6AJYJLSEa есть продолжение, но я бы хотел остановится пока на этом месте. Вчера всплыл вопрос: может ли переопределение конструктора дочернего класса быть нарушением LSP? В данном примере, наверное, нет. Мы видим что тут вообще не идет речь о инстанцировании этих классов, еще очевидно что их конструкторы могут быть разными, прямоугольнику нужны значения сторон, квадрату — только одной стороны. Нарушение создается в методах класса Square и проявляется в функции f при использовании супертипа Rectangle. Но это только в данном примере именно так, а что если нам нужно написать функцию которая будет с помощью порождающего шаблона создавать класс? Сразу кажется что было бы удобно иметь единый набор и порядок параметров для инстанцирования объектов фабрикой. Предположим что функция будет работать именно с нашими классами Прямоугольник и Квадрат. Теперь вспомним что нарушение LSP проявится именно тогда, когда функция оборачивающая фабрику сломается при получении некоторого подходящего типа данных и оперировании им. Что вообще может получить такая функция на вход? Допустим, название класса и массив параметров, это довольно типичный набор, получается тут не идет речь о каком-то сложном типе данных (как в академических примерах) на входе и вся работа будет заключатся в том чтобы написать команду инстанцирования класса с массивом параметров на входе, причем даже не понадобится большого switch case. По-моему тут вообще не идет речи о нарушении принципа подстановки, ведь смысл принципа в том, чтобы писать новые подклассы так, чтобы те кто используют базовый класс при получении подкласса работали как и прежде, если же они упадут, значит в своем подклассе вы нарушили принцип подстановки.
Можно придумывать еще примеры: хотим работать с ХранилищеИнтерфейс, при этом сами хранилища бывают РедисХранилище, МемкешХранилище, ПостгресХранилище, очевидно что они должны быть сконфигурированы разными наборами параметров и использующему коду должно быть все равно как они создавались, важно что вызывающий код не должен заметить никакой разницы между ними при подстановке любого из них.