Ответ
Принцип подстановки Лисков (Liskov Substitution Principle, LSP): Объекты производного класса должны быть заменяемы на объекты базового класса без нарушения корректности программы. Клиентский код, работающий с базовым типом, должен продолжать работать корректно с любым из его подтипов.
Классический пример нарушения 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
{
get => base.Width;
set { base.Width = value; base.Height = value; } // Нарушение!
}
public override int Height
{
get => base.Height;
set { base.Height = value; base.Width = value; } // Нарушение!
}
}
// Клиентский код, который сломается
public void TestRectangleArea(Rectangle rect)
{
rect.Width = 5;
rect.Height = 4;
Console.WriteLine("Expected area: 20, Actual area: " + rect.Area);
}
// Вызов
var sq = new Square();
TestRectangleArea(sq); // Выведет: Expected area: 20, Actual area: 16
// Квадрат не может быть заменой прямоугольника в этом контексте!
Как исправить (соблюсти LSP): Нужно пересмотреть иерархию наследования. Квадрат — это частный случай прямоугольника с математической точки зрения, но не с точки зрения поведения объекта, изменяющего свои стороны независимо.
// Решение 1: Не связывать наследованием, использовать композицию или общий интерфейс.
public interface IShape
{
int GetArea();
}
public class Rectangle : IShape { /* ... */ }
public class Square : IShape { /* ... */ }
// Решение 2: Сделать классы неизменяемыми (immutable).
public class Rectangle
{
public Rectangle(int width, int height)
{
Width = width;
Height = height;
}
public int Width { get; } // Только getter
public int Height { get; }
public int Area => Width * Height;
}
public class Square : Rectangle
{
// Квадрат создается с одним параметром, что логично.
public Square(int side) : base(side, side) { }
}
// Теперь подстановка безопасна, так как состояние объектов нельзя изменить после создания.
Вывод: LSP заставляет проектировать иерархии так, чтобы подтипы не ужесточали предусловия и не ослабляли постусловия базового типа.
Ответ 18+ 🔞
А, принцип подстановки Лисков, ёпта! Это ж та самая история, когда ты думаешь, что у тебя в программе один объект, а на самом деле подсунули другого, и всё летит в тартарары, как твой код после пятничного деплоя.
Смотри, в чём суть, блядь. Представь, есть у тебя базовый класс, скажем, Утка. И ты пишешь код, который с этими утками работает: заставляет крякать, плавать, летать. А потом ты подсовываешь ему подтип Утка-приманка (резиновую). И если эта резиновая утка вдруг начнёт в твоём коде пытаться летать и падать, потому что у неё крылья не для того — это и есть нарушение, блядь. Клиентский код ожидает утку, а не пиздёж.
Вот тебе классический пиздец, который все приводят — прямоугольник и квадрат.
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
{
get => base.Width;
set { base.Width = value; base.Height = value; } // На, блядь, получай!
}
public override int Height
{
get => base.Height;
set { base.Height = value; base.Width = value; } // И тебе того же!
}
}
И вроде бы логично, математически-то верно. Но с точки зрения поведения — полный пиздец. Смотри, что происходит.
Есть у тебя метод, который работает с прямоугольником:
public void TestRectangleArea(Rectangle rect)
{
rect.Width = 5;
rect.Height = 4;
Console.WriteLine("Expected area: 20, Actual area: " + rect.Area);
}
Ты вызываешь его, передавая прямоугольник — всё ок. Ожидаешь 20, получаешь 20. Ура.
А теперь подсовываешь квадрат:
var sq = new Square();
TestRectangleArea(sq); // Выведет: Expected area: 20, Actual area: 16
Вот и всё, приехали. Клиентский код сломался, потому что квадрат не может быть адекватной заменой прямоугольнику в этом контексте. Он нарушил контракт: у прямоугольника стороны независимы, а у квадрата — нет. После установки ширины в 5, высота автоматом тоже стала 5, и когда код попытался поставить высоту в 4, он и ширину заодно поменял на 4. Итог — площадь 16, а не 20. Охуенно, да?
Так как же это исправить, чтобы не было мучительно больно?
Вариант первый, самый правильный — перестать ебаться с наследованием там, где оно не нужно. Квадрат с математической точки зрения — да, прямоугольник. А с точки зрения поведения объекта в программе — нихуя не прямоугольник, если мы меняем стороны.
Поэтому делаем так: либо общий интерфейс, либо композиция.
public interface IShape
{
int GetArea();
}
public class Rectangle : IShape { /* ... свою логику ... */ }
public class Square : IShape { /* ... свою логику ... */ }
Теперь клиентский код работает с IShape, и ему похуй, квадрат там или прямоугольник — главное, чтобы площадь умел возвращать. Подстановка безопасна.
Вариант второй — сделать объекты неизменяемыми, как скала. Если состояние менять нельзя, то и проблем нет.
public class Rectangle
{
public Rectangle(int width, int height)
{
Width = width;
Height = height;
}
public int Width { get; } // Только геттер, сеттера нет
public int Height { get; }
public int Area => Width * Height;
}
public class Square : Rectangle
{
// Квадрат создаётся с одной стороной, и это логично
public Square(int side) : base(side, side) { }
}
Вот теперь подставляй что хочешь. Объекты создал — и забыл. Менять их после создания нельзя, поэтому и подмена безопасна. Хоть квадрат, хоть прямоугольник — площадь посчитается корректно на момент создания, и никто не сможет потом испортить состояние.
Вывод, блядь: LSP — это про то, чтобы подтипы не выёбывались и не меняли правила игры, заданные базовым классом. Нельзя ужесточать предусловия (требовать больше, чем база) и нельзя ослаблять постусловия (обещать меньше, чем база). Иначе клиентский код, который полагается на контракт базового типа, получит неожиданный пиздец вместо работы. Проектируй иерархии так, чтобы подтипы были настоящими заменами, а не вот этим вот всем.