← Все вопросы

В чём суть принципов открытости/закрытости (Open/Closed) и подстановки Лисков (Liskov)?

Задан 16 месяцев назад1к просмотров2 ответа
9

Разбираю SOLID и застрял на двух средних принципах: OCP (Open/Closed) и LSP (Liskov Substitution). Что значит «открыт для расширения, но закрыт для модификации»? И что именно требует принцип подстановки Лисков — ведь наследование и так позволяет подставлять наследников? Объясните на примерах C#, в чём подвох.

2 ответа

17
✓ Принятый ответ — помог автору

Разберём оба принципа.

Open/Closed Principle (OCP)

«Программные сущности должны быть открыты для расширения, но закрыты для модификации». То есть добавлять новое поведение нужно, дописывая код, а не переписывая уже работающий и протестированный.

Нарушение — switch по типу, который придётся править при каждом новом случае:

// Плохо: при добавлении фигуры лезем в этот метод
public class AreaCalculator
{
    public double Area(object shape)
    {
        if (shape is Circle c) return Math.PI * c.Radius * c.Radius;
        if (shape is Square s) return s.Side * s.Side;
        throw new NotSupportedException();
    }
}

Исправляем через абстракцию:

public abstract class Shape { public abstract double Area(); }

public class Circle : Shape
{
    public double Radius { get; set; }
    public override double Area() => Math.PI * Radius * Radius;
}

public class Square : Shape
{
    public double Side { get; set; }
    public override double Area() => Side * Side;
}

// Этот класс больше не нужно менять при добавлении фигуры
public class AreaCalculator
{
    public double Area(Shape shape) => shape.Area();
}

Новую фигуру добавляем новым классом — старый код не трогаем.

Liskov Substitution Principle (LSP)

«Объекты базового типа можно заменить объектами подтипа без нарушения корректности программы». Само наследование это не гарантирует — компилятор пропустит, а логика сломается.

Классический контрпример — Квадрат и Прямоугольник:

public class Rectangle
{
    public virtual int Width { get; set; }
    public virtual int Height { get; set; }
    public int Area() => Width * Height;
}

public class Square : Rectangle
{
    // Квадрат держит стороны равными — и ломает ожидания
    public override int Width { set { base.Width = base.Height = value; } }
    public override int Height { set { base.Width = base.Height = value; } }
}

// Код, который верен для Rectangle, ломается для Square:
void Test(Rectangle r)
{
    r.Width = 5;
    r.Height = 4;
    // ожидаем 20, но для Square получим 16
    Console.WriteLine(r.Area());
}

Квадрат является прямоугольником математически, но не является заменяемым подтипом по поведению. LSP нарушен. Признаки нарушения: наследник усиливает предусловия, ослабляет постусловия, кидает новые исключения или возвращает NotImplementedException в переопределённом методе.

Связь принципов: LSP — это, по сути, условие корректной работы OCP. Полиморфная подстановка (на которой держится OCP) работает правильно только тогда, когда наследники честно соблюдают контракт базового типа.

7

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

public override void Fly()
{
    throw new NotSupportedException("Пингвин не летает");
}

Если Penguin : Bird, а Bird.Fly() обещает полёт — подстановка пингвина вместо птицы сломает любой код, ожидающий, что птица полетит. Решение — пересмотреть иерархию: вынести летающих в отдельный интерфейс IFlyingBird.

По OCP мой главный совет: не делайте абстракции заранее «на всякий случай». Применяйте OCP к тем точкам, которые реально часто меняются. Преждевременная абстракция тоже зло.

Ваш ответ

Войдите, чтобы ответить на вопрос.
Поддержать проект