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

Что такое принцип подстановки Барбары Лисков?

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

  1. Используй 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() { } }
  1. Не меняй контракт в подклассе
// Плохо
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> { } } // ✓
  1. Композиция вместо наследования
// Лучше использовать composition
class AdvancedButton {
  constructor(private button: BasicButton, private features: Feature[]) {}
}

Важность для фронтенда

В React и TypeScript LSP критичен для:

  • Правильной типизации компонентов
  • Предсказуемости поведения
  • Переиспользуемости кода
  • Тестируемости

Принцип Лисков обеспечивает, что подклассы безопасно заменяют своих родителей, делая код более надежным и предсказуемым.

Что такое принцип подстановки Барбары Лисков? | PrepBro