Введение: Почему SOLID важен в TypeScript
При разработке на TypeScript, особенно в крупных проектах, поддержка чистоты и гибкости кода становится критически важной. SOLID — это пять фундаментальных принципов объектно-ориентированного программирования, которые помогают создавать масштабируемые, легко тестируемые и поддерживаемые приложения. Эти принципы особенно актуальны для TypeScript, который добавляет статическую типизацию и интерфейсы в JavaScript, предоставляя мощные инструменты для их реализации.
В этой статье мы разберем каждый из пяти принципов SOLID с точки зрения TypeScript, сопроводив теорию практическими примерами кода. Вы узнаете, как применять эти принципы для улучшения архитектуры ваших проектов.
1. Single Responsibility Principle (Принцип единственной ответственности)
Теория: Каждый класс или модуль должен иметь только одну причину для изменения. Другими словами, класс должен отвечать только за одну часть функциональности программы.
Нарушение принципа:
// ❌ Плохо: класс отвечает и за данные, и за сохранение, и за форматирование
class User {
constructor(public name: string, public email: string) {}
saveToDatabase(): void {
console.log(`Сохранение ${this.name} в БД`);
}
generateHTML(): string {
return `<div>${this.name} - ${this.email}</div>`;
}
}
Исправление с применением SRP:
// ✅ Хорошо: разделяем ответственность на три класса
class User {
constructor(public name: string, public email: string) {}
}
class UserRepository {
save(user: User): void {
console.log(`Сохранение ${user.name} в БД`);
}
}
class UserView {
render(user: User): string {
return `<div>${user.name} - ${user.email}</div>`;
}
}
Теперь каждый класс имеет одну четкую обязанность, что упрощает тестирование и изменение кода.
2. Open-Closed Principle (Принцип открытости/закрытости)
Теория: Программные сущности (классы, модули, функции) должны быть открыты для расширения, но закрыты для модификации. Это означает, что вы должны иметь возможность добавлять новую функциональность без изменения существующего кода.
Нарушение принципа:
// ❌ Плохо: при добавлении нового типа фигуры нужно менять класс AreaCalculator
class AreaCalculator {
calculate(shape: any): number {
if (shape.type === 'circle') {
return Math.PI * shape.radius ** 2;
} else if (shape.type === 'rectangle') {
return shape.width * shape.height;
}
return 0;
}
}
Исправление с применением OCP:
// ✅ Хорошо: используем интерфейс и полиморфизм
interface Shape {
area(): number;
}
class Circle implements Shape {
constructor(public radius: number) {}
area(): number {
return Math.PI * this.radius ** 2;
}
}
class Rectangle implements Shape {
constructor(public width: number, public height: number) {}
area(): number {
return this.width * this.height;
}
}
class AreaCalculator {
calculate(shape: Shape): number {
return shape.area(); // Закрыто для модификации, открыто для расширения
}
}
Теперь для добавления новой фигуры (например, Triangle) достаточно создать новый класс, реализующий интерфейс Shape — без изменения AreaCalculator.
3. Liskov Substitution Principle (Принцип подстановки Барбары Лисков)
Теория: Объекты в программе должны быть заменяемыми экземплярами их подтипов без нарушения корректности программы. Если у вас есть базовый класс и наследник, то код, работающий с базовым классом, должен корректно работать и с наследником.
Нарушение принципа:
// ❌ Плохо: наследник нарушает контракт базового класса
class Bird {
fly(): string {
return 'Летит';
}
}
class Penguin extends Bird {
fly(): string {
throw new Error('Пингвины не летают!'); // Нарушение LSP
}
}
function makeBirdFly(bird: Bird): void {
console.log(bird.fly()); // Упадет с ошибкой для Penguin
}
Исправление с применением LSP:
// ✅ Хорошо: разделяем иерархию
interface Bird {
move(): string;
}
class FlyingBird implements Bird {
move(): string {
return 'Летит';
}
}
class Penguin implements Bird {
move(): string {
return 'Плавает';
}
}
function makeBirdMove(bird: Bird): void {
console.log(bird.move()); // Работает для всех птиц
}
Соблюдение LSP гарантирует, что ваш код остается предсказуемым и безопасным при использовании полиморфизма.
4. Interface Segregation Principle (Принцип разделения интерфейса)
Теория: Клиенты не должны зависеть от интерфейсов, которые они не используют. Лучше иметь несколько специализированных интерфейсов, чем один универсальный.
Нарушение принципа:
// ❌ Плохо: толстый интерфейс, который заставляет реализовывать ненужные методы
interface Worker {
work(): void;
eat(): void;
sleep(): void;
}
class Robot implements Worker {
work(): void { console.log('Работает'); }
eat(): void { throw new Error('Роботы не едят'); } // ❌
sleep(): void { throw new Error('Роботы не спят'); } // ❌
}
Исправление с применением ISP:
// ✅ Хорошо: разделяем на специализированные интерфейсы
interface Workable {
work(): void;
}
interface Eatable {
eat(): void;
}
interface Sleepable {
sleep(): void;
}
class Human implements Workable, Eatable, Sleepable {
work(): void { console.log('Человек работает'); }
eat(): void { console.log('Человек ест'); }
sleep(): void { console.log('Человек спит'); }
}
class Robot implements Workable {
work(): void { console.log('Робот работает'); }
// Не нужно реализовывать eat() и sleep()
}
Теперь каждый класс реализует только те интерфейсы, которые ему действительно нужны.
5. Dependency Inversion Principle (Принцип инверсии зависимостей)
Теория: Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба должны зависеть от абстракций. Абстракции не должны зависеть от деталей — детали должны зависеть от абстракций.
Нарушение принци