← Назад к вопросам
Что такое принцип подстановки Барбары Лисков?
1.3 Junior🔥 111 комментариев
#Архитектура и паттерны
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Принцип подстановки Барбары Лисков (LSP)
Принцип подстановки Барбары Лисков (Liskov Substitution Principle, LSP) — третий принцип SOLID, сформулированный Барбарой Лисков в 1987 году. Он гласит: объекты подклассов должны корректно заменять объекты базовых классов без нарушения работы программы.
Другими словами: если класс S является подтипом класса T, то объекты типа T в программе могут быть заменены объектами типа S без каких-либо нежелательных последствий.
Простой пример — нарушение LSP
// ❌ Нарушает LSP
class Rectangle {
width: number = 0;
height: number = 0;
setWidth(w: number) {
this.width = w;
}
setHeight(h: number) {
this.height = h;
}
getArea() {
return this.width * this.height;
}
}
class Square extends Rectangle {
setWidth(w: number) {
this.width = w;
this.height = w; // ❌ изменяет оба параметра
}
setHeight(h: number) {
this.width = h;
this.height = h; // ❌ изменяет оба параметра
}
}
// Проблема в использовании
function testRectangle(rect: Rectangle) {
rect.setWidth(5);
rect.setHeight(10);
console.log(rect.getArea()); // ожидаем 50
}
const square = new Square();
testRectangle(square); // выведет 100, а не 50 ❌
Правильное решение — разделение типов
// ✅ Соблюдает LSP
interface Shape {
getArea(): number;
}
class Rectangle implements Shape {
constructor(private width: number, private height: number) {}
getArea() {
return this.width * this.height;
}
}
class Square implements Shape {
constructor(private side: number) {}
getArea() {
return this.side * this.side;
}
}
function calculateArea(shape: Shape) {
return shape.getArea();
}
const rect = new Rectangle(5, 10);
const square = new Square(5);
calculateArea(rect); // 50 ✓
calculateArea(square); // 25 ✓
Пример в React
// ❌ Нарушает LSP
interface ButtonProps {
onClick: () => void;
}
interface SubmitButtonProps extends ButtonProps {
// SubmitButton не поддерживает onClick как обычная кнопка
// вместо этого требует onSubmit
onSubmit: (formData: FormData) => void;
onClick?: never; // запрещаем onClick
}
// ❌ Код ломается при подстановке
function renderButton(props: ButtonProps) {
return <button onClick={props.onClick}>Click</button>;
}
const submitBtn: SubmitButtonProps = {
onSubmit: (data) => console.log(data),
};
renderButton(submitBtn); // ❌ ошибка: onClick не определен
// ✅ Соблюдает LSP
interface Button {
render(): React.ReactNode;
}
interface BasicButtonProps {
label: string;
onClick: () => void;
}
interface SubmitButtonProps {
label: string;
onSubmit: (data: FormData) => void;
}
class BasicButton implements Button {
constructor(private props: BasicButtonProps) {}
render() {
return <button onClick={this.props.onClick}>{this.props.label}</button>;
}
}
class SubmitButton implements Button {
constructor(private props: SubmitButtonProps) {}
render() {
return (
<button onClick={() => this.props.onSubmit(new FormData())}>
{this.props.label}
</button>
);
}
}
function renderButton(btn: Button) {
return btn.render(); // ✓ работает для обоих типов
}
Пример с утилитами
// ❌ Нарушает LSP
function processUsers(users: User[]) {
users.forEach(user => {
const email = user.getEmail(); // ожидаем email
});
}
class AdminUser extends User {
getEmail() {
return null; // ❌ нарушает контракт
}
}
const admins = [new AdminUser()];
processUsers(admins); // ломается ❌
// ✅ Соблюдает LSP
interface EmailProvider {
getEmail(): string;
}
class User implements EmailProvider {
constructor(private email: string) {}
getEmail() {
return this.email;
}
}
class Admin implements EmailProvider {
constructor(private contactEmail: string) {}
getEmail() {
return this.contactEmail; // всегда возвращает строку ✓
}
}
function processEmailProviders(providers: EmailProvider[]) {
providers.forEach(p => {
console.log(p.getEmail()); // ✓ безопасно
});
}
Ключевые признаки нарушения LSP
- Подкласс отключает функционал базового класса
- Подкласс изменяет контракт методов (returns null вместо объекта)
- Необходимо проверять тип перед использованием (instanceof)
- Нарушается замещаемость объектов
- Возникают неожиданные исключения при подстановке
Как соблюдать LSP
- Используй interface вместо наследования
// Плохо: наследование
class Bird { fly() { } }
class Penguin extends Bird { fly() { throw new Error(); } }
// Хорошо: разные интерфейсы
interface Flyer { fly(): void; }
interface Walker { walk(): void; }
class Eagle implements Flyer { fly() { } }
class Penguin implements Walker { walk() { } }
- Не меняй контракт в подклассе
// Плохо
class Service { getData(): Promise<User | null> { } }
class ExtendedService extends Service { getData(): Promise<User> { } } // ❌
// Хорошо
class Service { getData(): Promise<User> { } }
class ExtendedService extends Service { getData(): Promise<User> { } } // ✓
- Композиция вместо наследования
// Лучше использовать composition
class AdvancedButton {
constructor(private button: BasicButton, private features: Feature[]) {}
}
Важность для фронтенда
В React и TypeScript LSP критичен для:
- Правильной типизации компонентов
- Предсказуемости поведения
- Переиспользуемости кода
- Тестируемости
Принцип Лисков обеспечивает, что подклассы безопасно заменяют своих родителей, делая код более надежным и предсказуемым.