SOLID принципы в TypeScript: теория и практика от А до Я
SOLID — это акроним пяти фундаментальных принципов объектно-ориентированного программирования, сформулированных Робертом Мартином (дядюшкой Бобом). Эти принципы помогают создавать гибкий, поддерживаемый и масштабируемый код. В этой статье мы подробно разберём каждый принцип SOLID с примерами на TypeScript, от простых определений до реальных сценариев использования.
1. Single Responsibility Principle (SRP) — Принцип единственной ответственности
Определение: Каждый класс должен иметь только одну причину для изменения. Иными словами, класс должен отвечать за одну чётко определённую задачу.
Проблема: Когда класс берёт на себя несколько обязанностей, его становится сложно тестировать, расширять и поддерживать. Изменение одной функции может поломать другую.
Пример нарушения SRP:
// ❌ Нарушение SRP: класс занимается и хранением, и форматированием, и отправкой
class UserService {
saveUser(user: User): void {
// Сохранение в БД
console.log('User saved');
}
formatUserData(user: User): string {
return `Name: ${user.name}, Email: ${user.email}`;
}
sendWelcomeEmail(user: User): void {
// Отправка email
console.log('Email sent');
}
}
Решение с SRP:
// ✅ Каждый класс отвечает за свою задачу
class UserRepository {
save(user: User): void {
console.log('User saved');
}
}
class UserFormatter {
format(user: User): string {
return `Name: ${user.name}, Email: ${user.email}`;
}
}
class EmailService {
sendWelcome(user: User): void {
console.log('Email sent');
}
}
2. Open-Closed Principle (OCP) — Принцип открытости/закрытости
Определение: Классы должны быть открыты для расширения, но закрыты для модификации. Это означает, что вы должны иметь возможность добавлять новую функциональность без изменения существующего кода.
Пример нарушения OCP:
// ❌ При добавлении нового типа фигуры придётся менять метод calculateArea
class AreaCalculator {
calculateArea(shape: any): number {
if (shape.type === 'circle') {
return Math.PI * shape.radius ** 2;
} else if (shape.type === 'rectangle') {
return shape.width * shape.height;
}
throw new Error('Unknown shape');
}
}
Решение с OCP (используем полиморфизм):
// ✅ Используем интерфейс и наследование
interface Shape {
area(): number;
}
class Circle implements Shape {
constructor(private radius: number) {}
area(): number {
return Math.PI * this.radius ** 2;
}
}
class Rectangle implements Shape {
constructor(private width: number, private height: number) {}
area(): number {
return this.width * this.height;
}
}
class AreaCalculator {
calculateArea(shape: Shape): number {
return shape.area(); // Не требует изменений при добавлении новых фигур
}
}
3. Liskov Substitution Principle (LSP) — Принцип подстановки Барбары Лисков
Определение: Объекты в программе должны быть заменяемы экземплярами их подтипов без изменения правильности выполнения программы. Дочерний класс не должен нарушать контракт родительского класса.
Пример нарушения LSP:
// ❌ Нарушение: подкласс изменяет поведение предка
class Bird {
fly(): void {
console.log('Flying');
}
}
class Penguin extends Bird {
fly(): void {
throw new Error('Penguins cannot fly!'); // Нарушение контракта
}
}
Решение с LSP:
// ✅ Разделяем иерархию: не все птицы летают
interface Flyable {
fly(): void;
}
class Bird {
// Общие свойства птиц
}
class Sparrow extends Bird implements Flyable {
fly(): void {
console.log('Sparrow flying');
}
}
class Penguin extends Bird {
// Пингвины не летают, но могут плавать
swim(): void {
console.log('Penguin swimming');
}
}
4. Interface Segregation Principle (ISP) — Принцип разделения интерфейса
Определение: Клиенты не должны зависеть от интерфейсов, которые они не используют. Лучше иметь несколько специализированных интерфейсов, чем один универсальный.
Пример нарушения ISP:
// ❌ Огромный интерфейс, который заставляет реализовывать ненужные методы
interface Worker {
work(): void;
eat(): void;
sleep(): void;
}
class HumanWorker implements Worker {
work(): void { /* работает */ }
eat(): void { /* ест */ }
sleep(): void { /* спит */ }
}
class RobotWorker implements Worker {
work(): void { /* работает */ }
eat(): void { throw new Error('Robot does not eat'); } // ❌
sleep(): void { throw new Error('Robot does not sleep'); } // ❌
}
Решение с ISP:
// ✅ Разделяем на маленькие интерфейсы
interface Workable {
work(): void;
}
interface Eatable {
eat(): void;
}
interface Sleepable {
sleep(): void;
}
class HumanWorker implements Workable, Eatable, Sleepable {
work(): void { /* работает */ }
eat(): void { /* ест */ }
sleep(): void { /* спит */ }
}
class RobotWorker implements Workable {
work(): void { /* работает */ }
}
5. Dependency Inversion Principle (DIP) — Принцип инверсии зависимостей
Определение: Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба должны зависеть от абстракций. Абстракции не должны зависеть от деталей — детали должны зависеть от абстракций.
Пример нарушения DIP:
// ❌ Высокоуровневый модуль зависит от конкретной реализации
class MySQLDatabase {
save(data: string): void {
console.log(`Saving to MySQL: ${data}`);
}
}
class UserService {
private db: MySQLDatabase;
constructor() {
this.db = new MySQLDatabase(); // Жёсткая связь
}
saveUser(name: string): void {
this.db.save(name);
}
}
Решение с DIP (инверсия зависимостей):
// ✅ Зависимость от абстракции (интерфейса)
interface Database {
save(data: string): void;
}
class MySQLDatabase implements Database {
save(data: string): void {
console.log(`Saving to MySQL: ${data}`);
}
}
class PostgreSQLDatabase implements Database {
save(data: string): void {
console.log(`Saving to PostgreSQL: ${data}`);
}
}
class UserService {
constructor(private db: Database