← Назад к вопросам

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

1.8 Middle🔥 191 комментариев
#ООП и паттерны проектирования

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI6 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Принцип подстановки Лисков (LSP)

Принцип подстановки Лисков (Liskov Substitution Principle, LSP) — это третий из SOLID принципов объектно-ориентированного программирования, сформулированный Барбарой Лисков. Он гласит: объекты должны быть заменяемыми экземплярами своих базовых типов без нарушения корректности программы. Проще говоря, если класс S является подклассом класса T, то объекты типа T должны без проблем заменяться объектами типа S, не ломая логику программы.

Ключевые аспекты принципа

  • Контракты предков и потомков: Подкласс не должен ужесточать предусловия (требования к входным данным) или ослаблять постусловия (гарантии на результат).
  • Инварианты: Подкласс должен сохранять инварианты (внутренние условия целостности) базового класса.
  • Семантическая совместимость: Поведение подкласса должно соответствовать ожиданиям, заданным базовым классом, а не только синтаксически (через сигнатуры методов).

Пример нарушения и соблюдения LSP в C#

Рассмотрим классический пример с геометрическими фигурами, который часто иллюстрирует нарушение LSP.

❌ Нарушение принципа подстановки

public class Rectangle
{
    public virtual int Width { get; set; }
    public virtual int Height { get; set; }
    
    public int CalculateArea()
    {
        return Width * Height;
    }
}

public class Square : Rectangle
{
    public override int Width
    {
        get => base.Width;
        set
        {
            base.Width = value;
            base.Height = value; // Нарушение: квадрат меняет и высоту при изменении ширины
        }
    }
    
    public override int Height
    {
        get => base.Height;
        set
        {
            base.Height = value;
            base.Width = value; // Нарушение: аналогично
        }
    }
}

Проблема: Клиентский код, работающий с Rectangle, ожидает, что ширина и высота изменяются независимо. Например:

public void TestRectangleArea(Rectangle rect)
{
    rect.Width = 5;
    rect.Height = 4;
    Console.WriteLine($"Ожидаемая площадь: 20, фактическая: {rect.CalculateArea()}");
    // Для Rectangle: 5 * 4 = 20 (корректно)
    // Для Square: 4 * 4 = 16 (нарушение ожиданий!)
}

Если передать в TestRectangleArea объект Square, результат будет неожиданным (16 вместо 20). Это нарушает LSP, так как Square не может быть заменой Rectangle без изменения поведения.

✅ Соблюдение принципа подстановки

Исправим пример, сделав Rectangle и Square независимыми классами, унаследованными от общего абстрактного базового класса:

public abstract class Shape
{
    public abstract int CalculateArea();
}

public class Rectangle : Shape
{
    public int Width { get; set; }
    public int Height { get; set; }
    
    public override int CalculateArea()
    {
        return Width * Height;
    }
}

public class Square : Shape
{
    public int SideLength { get; set; }
    
    public override int CalculateArea()
    {
        return SideLength * SideLength;
    }
}

Преимущества подхода:

  • Классы Rectangle и Square не связаны наследованием друг с другом, что устраняет проблему смены поведения.
  • Клиентский код работает с абстракцией Shape, позволяя корректно заменять любые фигуры:
public void PrintArea(Shape shape)
{
    Console.WriteLine($"Площадь: {shape.CalculateArea()}");
}

// Использование
var shapes = new List<Shape>
{
    new Rectangle { Width = 5, Height = 4 },
    new Square { SideLength = 4 }
};

foreach (var shape in shapes)
{
    PrintArea(shape); // Работает корректно для всех фигур
}

Практическое применение LSP в Backend-разработке

В контексте C# Backend принцип Лисков критически важен для проектирования устойчивых архитектур:

  1. Интерфейсы и абстракции: Используйте интерфейсы для определения контрактов, которые гарантируют семантическую совместимость.
public interface IRepository<T>
{
    T GetById(int id);
    void Save(T entity);
}

public class UserRepository : IRepository<User>
{
    public User GetById(int id) { /* реализация */ }
    public void Save(User entity) { /* реализация */ }
}

public class CachedUserRepository : IRepository<User>
{
    private readonly IRepository<User> _decoratedRepository;
    
    public CachedUserRepository(IRepository<User> decoratedRepository)
    {
        _decoratedRepository = decoratedRepository;
    }
    
    public User GetById(int id)
    {
        // Проверка кэша, затем вызов _decoratedRepository.GetById()
        // Поведение соответствует контракту IRepository
    }
    
    public void Save(User entity)
    {
        // Инвалидация кэша + вызов _decoratedRepository.Save()
    }
}
  1. Полиморфизм без сюрпризов: LSP обеспечивает корректную работу полиморфных коллекций и методов.

  2. Тестируемость: Классы, соблюдающие LSP, легко тестировать через базовые интерфейсы.

Вывод

Принцип подстановки Лисков — это не просто правило наследования, а требование к семантической совместимости в иерархии классов. В Backend-разработке его соблюдение приводит к:

  • Более стабильным API и контрактам
  • Упрощению модульного тестирования
  • Гибкости в внедрении зависимостей (DI)
  • Устойчивости к изменениям через декораторы и стратегии

Нарушение LSP часто проявляется через проверки типа (is, as, GetType()) в клиентском коде — это явный "запах", указывающий на проблему в иерархии наследования.

Приведи пример принципа подстановки | PrepBro