4. SOLID: Liskov Substitution
Design Patterns. Урок: SOLID — Liskov Substitution Principle
Заголовок раздела «Design Patterns. Урок: SOLID — Liskov Substitution Principle»Объекты дочернего класса должны быть способны заменить объекты родительского класса без нарушения корректности программы.
Сформулировала принцип Барбара Лисков в 1987 году. Грубо говоря: если ты ожидаешь Animal, а тебе подсунули Dog — всё должно работать одинаково.
Классический пример нарушения: прямоугольник и квадрат
Заголовок раздела «Классический пример нарушения: прямоугольник и квадрат»Математически квадрат — это частный случай прямоугольника. Логично сделать Square extends Rectangle? Нет!
// ❌ Нарушение LSPclass Rectangle { constructor( protected width: number, protected height: number, ) {}
setWidth(width: number): void { this.width = width; } setHeight(height: number): void { this.height = height; } getArea(): number { return this.width * this.height; }}
class Square extends Rectangle { // Квадрат должен сохранять равенство сторон setWidth(width: number): void { this.width = width; this.height = width; // ← Нарушаем контракт родителя! }
setHeight(height: number): void { this.width = height; // ← И здесь тоже! this.height = height; }}
// Функция работает с Rectanglefunction increaseWidth(rect: Rectangle): void { const originalHeight = rect.getArea() / rect.width; rect.setWidth(rect.width + 10); // Ожидаем: площадь изменится, высота — нет console.assert( rect.getArea() === (rect.width) * originalHeight, 'Area should change proportionally' );}
const square = new Square(10, 10);increaseWidth(square); // Assertion fails! Square нарушает контрактПравильное решение: общий интерфейс
Заголовок раздела «Правильное решение: общий интерфейс»// ✅ Общий интерфейс без нарушения контрактаinterface Shape { getArea(): number;}
class Rectangle implements Shape { constructor( private width: number, private height: number, ) {}
setWidth(width: number): void { this.width = width; } setHeight(height: number): void { this.height = height; } getArea(): number { return this.width * this.height; }}
class Square implements Shape { constructor(private side: number) {}
setSide(side: number): void { this.side = side; } getArea(): number { return this.side * this.side; }}Как проверить соблюдение LSP
Заголовок раздела «Как проверить соблюдение LSP»Три вопроса:
- Предусловия: дочерний класс не должен требовать более строгих условий, чем родитель
- Постусловия: дочерний класс должен гарантировать хотя бы то, что гарантирует родитель
- Инварианты: свойства, верные для родителя, должны быть верны для дочернего класса
class Bird { fly(): void { console.log('Flying...'); }
move(distance: number): void { this.fly(); console.log(`Moved ${distance}m`); }}
// ❌ Нарушение — пингвин не может летатьclass Penguin extends Bird { fly(): void { throw new Error('Penguins cannot fly!'); // Нарушаем контракт Bird }}
// ✅ Правильная иерархияinterface CanFly { fly(): void;}
class Animal { move(distance: number): void { console.log(`Moved ${distance}m`); }}
class FlyingBird extends Animal implements CanFly { fly(): void { console.log('Flying...'); } move(distance: number): void { this.fly(); super.move(distance); }}
class Penguin extends Animal { swim(): void { console.log('Swimming...'); } move(distance: number): void { this.swim(); super.move(distance); }}LSP и интерфейсы
Заголовок раздела «LSP и интерфейсы»LSP особенно важен при работе с интерфейсами в TypeScript:
interface Logger { log(message: string): void; error(message: string): void;}
class ConsoleLogger implements Logger { log(message: string): void { console.log(message); } error(message: string): void { console.error(message); }}
class SilentLogger implements Logger { log(_message: string): void { /* Intentionally silent */ } error(_message: string): void { /* Intentionally silent */ }}
// ✅ SilentLogger полностью заменяет ConsoleLoggerfunction processData(data: any[], logger: Logger): void { logger.log('Processing started'); // ... обработка ... logger.log('Processing finished');}
// Оба работают корректноprocessData(data, new ConsoleLogger());processData(data, new SilentLogger()); // В тестах!Практические задания
Заголовок раздела «Практические задания»- Найди нарушения LSP в следующем коде и исправь:
class Storage { save(key: string, value: string): void { localStorage.setItem(key, value); } load(key: string): string { return localStorage.getItem(key)!; }}
class ReadOnlyStorage extends Storage { save(_key: string, _value: string): void { throw new Error('This storage is read-only!'); }}-
Создай иерархию
Vehicle → Car, Truck, ElectricCarбез нарушения LSP. Учти что у электромобиля нетrefuel(), а уTruckестьloadCargo(). -
Напиши тест который проверяет соответствие LSP для пары родитель/потомок.
-
Как LSP связан с принципом «Composition over Inheritance»?
-
Посмотри на React: как
React.ComponentиReact.PureComponentсоблюдают LSP?