SOLID принципы в JavaScript. Принцип подстановки Барбары Лисков
Принципы хорошего кода

SOLID принципы в JavaScript. Принцип подстановки Барбары Лисков

В третьей статье, посвященной SOLID-принципам проектирования приложений мы поговорим о принципе подстановки Барбары Лисков. Этот принцип был сформулирован Барбарой Лисков в 1988 году и звучит так:

Подтипы должны быть заменяемы их исходными типами.

Принцип подстановки чаще всего используется в контексте наследования, однако суть принципа больше связана не с наследованием, а с поведенческой совместимостью объектов. Это уточнение особенно актуально для JavaScript, как для динамически типизированного языка.

В объектно-ориентированном проектировании наследование предоставляет механизм повторного использования кода в схожих классах. Это достигается за счет инкапсуляции общего поведения в базовом классе и переопределения/расширения поведения в наследниках. Согласно принципу подстановки Барбары Лисков поведение наследника должно быть эквивалентно поведению базового класса.

Пример нарушения принципа подстановки Барбары Лисков

Рассмотрим следующий пример:


function Vehicle() {
    var that = {
        speedValue: 0,
        running: false
    };

    that.speed = function() {
        return that.speedValue;
    };
    that.start = function() {
        that.running = true;
    };
    that.stop = function() {
        that.running = false;
    };
    that.accelerate = function() {
        that.speedValue++;
    };
    that.decelerate = function() {
        that.speedValue--;
    };
    that.state = function() {
        if (!that.running) {
            return 'parked';
        }
        else if (that.running && that.speedValue) {
            return 'moving';
        }
        else if (that.running) {
            return 'idle';
        }
    };
  
    return that;
}

В примере представлен объект Vehicle, описывающий поведение транспортного средства. Представим, что данный объект включен в некую библиотеку, которая используется в сторонних приложениях. Добавим новый объект, описывающий быстрое транспортное средство:


function FastVehicle() {
    var that = new Vehicle();
    that.accelerate = function() {
        that.speedValue += 3;
    };
  
    return that;
}

Объект FastVehicle наследуется от Vehicle, наследует все методы базового объекта, ускоряется в три раза быстрее. Мы публикуем новую версию библиотеки. Однако объекты FastVehicle могут использоватся не во всех приложениях, использующих нашу библиотеку. Рассмотрим пример:


var maneuver = function(vehicle) {
    console.log(vehicle.state());
    vehicle.start();
    console.log(vehicle.state());
    vehicle.accelerate();
    console.log(vehicle.state());
    console.log(vehicle.speed());
    vehicle.decelerate();
    console.log(vehicle.speed());
    if (vehicle.state() != 'idle') {
        throw 'The vehicle is still moving!';
    }
    vehicle.stop();
    console.log(vehicle.state());
};

Если функции maneuver() передать объект FastVehicle, мы получим исключение «The vehicle is still moving!». Это произошло потому, что разработчик данной функции исходил из того, что транспортные средства ускоряются и замедляются одинаково. Объекты Vehicle и FastVehicle не полностью взаимозаменяемы, и это нарушает принцип подстановки Лисков.

Классический пример с прямоугольником

Классический пример, который приводится при описании принципа подстановки — пример с прямоугольником и квадратом, описанный в книге Роберта Мартина.

Представим, что мы имеем объект прямоугольник:


var rectangle = {
    length: 0,
    width: 0
};

Позже нам понадобился объект квадрат. Основываясь на том, что квадрат является прямоугольником, стороны которого равны, мы решили создать объект квадрат и использовать его вместо прямоугольника. Свойства length и width определили таким образом, чтобы они изменялись синхронно и свойство квадрата не нарушалось:


var square = {};
(function() {
    var length = 0, width = 0;
    Object.defineProperty(square, 'length', {
        get: function() { return length; },
        set: function(value) { length = width = value; }
    });
    Object.defineProperty(square, 'width', {
        get: function() { return width; },
        set: function(value) { length = width = value; }
    });
})();

Проблема в том, что объект квадрат не может использоваться в любом коде вместо прямоугольника. Например, функция, создающая прямоугольник и вычисляющая площадь:


var g = function(rectangle) {
    rectangle.length = 3;
    rectangle.width = 4;
    console.log(rectangle.length);
    console.log(rectangle.width);
    console.log(rectangle.length * rectangle.width);
};

Если этой функции передать объект квадрат, мы получим площадь, равную 16, вместо ожидаемой 12. Здесь мы тоже видим нарушение принципа подстановки, но оно уже связано не с наследованием, а с заменой одного объекта другим.

Снижение вероятности нарушения принципа подстановки

Как же выявить и устранить нарушения принципа подстановки Барбары Лисков? К сожалению, это очень трудная и не всегда решаемая задача. Чтобы выявить такое нарушения, нужно представлять все возможные варианты использования библиотеки в клиентских приложениях. Тем не менее, существует несколько стратегий снижения вероятности таких нарушений.

Проектирование по контракту

Одна из стратегий снижения вероятности нарушения принципа подстановки — проектирование по контракту. Эта стратегия была предложена Бертраном Мейером. Согласно этой методике, объект должен иметь так называемый «контракт» — набор спецификаций, описывающий поведение объекта: входные параметры и ожидаемый результат.

Такие спецификации могут быть описаны в виде набора модульных тестов. Достаточное покрытие кода тестами не только делает поведение объекта предсказуемым, но и помогает разработчикам клиентских приложений понимать ограничения при использовании библиотеки.

Избегание наследования

Другая стратегия снижения вероятности нарушения принципа подстановки — сведение к минимуму использования наследования. Вместо наследования, там где это возможно, рекомендуется использовать композицию.

Заключение

Мы рассмотрели еще один SOLID принцип проектирования приложения — принцип подстановки Барбары Лисков. Этот принцип важно учитывать при разработке библиотек, которые могут использоваться другими разработчиками в клиентских приложениях. Выполнение этого принципа гарантирует ожидаемое поведение ваших объектов при определенных корректных входных данных.

К сожалению, выявить нарушение принципа подстановки не так просто. Однако значительно помочь в этом может написание модульных тестов: для разработчиков как самой библиотеки, так и клиентского кода, модульные тесты могут быть свое рода гарантией определенного результата при определенных входных данных.

С другими принципами SOLID вы можете познакомиться в статьях: SOLID принципы в JavaScript. Принцип единственной ответственности и SOLID принципы в JavaScript. Принцип открытости-закрытости.

Рассылка
Подпишитесь на рассылку и получайте дайджест новостей и статей.
Никакого спама!
Подписаться