Примечание. Это третья статья в серии статей, посвященных каждому из пяти принципов SOLID объектно-ориентированного программирования.

В мире разработки программного обеспечения принципы SOLID играют ключевую роль в создании удобных в сопровождении и масштабируемых систем. Среди этих принципов принцип замещения Лискова (LSP), пожалуй, один из самых неправильно понятых. Названный в честь Барбары Лисков, которая представила его на конференции 1987 года, LSP является неотъемлемой частью обеспечения простоты управления и контроля системы.

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

Принцип подстановки Лисков гласит: «Если S является подтипом T, то объекты типа T могут быть заменены объектами типа S без изменения каких-либо желаемых свойств программы (корректность, выполняемая задача и т. д.)».

Проще говоря, любая реализация абстракции (интерфейса) должна быть заменяемой везде, где принимается эта абстракция.

Почему LSP важен?

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

Принцип подстановки Лисков в действии

Давайте углубимся в практический пример, чтобы лучше понять LSP. Мы будем использовать популярный пример с фигурами.

class Rectangle {
    protected int width, height;
    public void setWidth(int width) {
        this.width = width;
    }
    public void setHeight(int height) {
        this.height = height;
    }
    public int getWidth() {
        return width;
    }
    public int getHeight() {
        return height;
    }
    public int getArea() {
        return width * height;
    }
}
class Square extends Rectangle {
    @Override
    public void setWidth(int width) {
        super.setWidth(width);
        super.setHeight(width);
    }
    @Override
    public void setHeight(int height) {
        super.setWidth(height);
        super.setHeight(height);
    }
}

В этом примере Square является подтипом Rectangle и переопределяет методы setWidth и setHeight. На первый взгляд это кажется нормальным, потому что квадрат — это прямоугольник. Но давайте посмотрим, что произойдет, когда мы напишем функцию, использующую эти классы.

void printArea(Rectangle r) {
    r.setWidth(5);
    r.setHeight(4);
    System.out.println("Expected area of 20, got " + r.getArea());
}
void main() {
    Rectangle r = new Rectangle();
    printArea(r); // Expected area of 20, got 20
    Square s = new Square();
    printArea(s); // Expected area of 20, got 16
}

Этот пример нарушает LSP, поскольку Square нельзя заменить на Rectangle без изменения поведения функции printArea. Когда мы заменили Rectangle на Square в методе printArea, результат отличался от того, что мы ожидали.

Практический пример: рефакторинг кода для соблюдения LSP

Итак, как мы можем реорганизовать приведенный выше код, чтобы он соответствовал LSP?

Одним из возможных решений может быть полный отказ от подкласса Square и создание отдельного интерфейса для фигур с областями. Это решение будет соответствовать LSP, поскольку между различными типами фигур не будет отношения наследования. Вот как это может выглядеть:

interface ShapeWithArea {
    int getArea();
}
class Rectangle implements ShapeWithArea {
    protected int width, height;
    // ... existing Rectangle methods ...
    @Override
    public int getArea() {
        return width * height;
    }
}
class Square implements ShapeWithArea {
    private int side;
    public Square(int side) {
        this.side = side;
    }
    @Override
    public int getArea() {
        return side * side;
    }
}

В этом рефакторинге кода Rectangle и Square являются типами ShapeWithArea, и их можно использовать взаимозаменяемо без изменения поведения программы.

Заключение

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