Антон Ларичев

Введение
Принципы SOLID — это пять фундаментальных правил объектно-ориентированного проектирования, сформулированных Робертом Мартином. Они помогают писать гибкий, расширяемый и поддерживаемый код. TypeScript благодаря строгой типизации, интерфейсам и модификаторам доступа отлично подходит для демонстрации этих принципов на практике. В этой статье разберём каждый принцип SOLID с примерами кода и покажем, как они работают в реальных проектах.
S — Single Responsibility Principle
Принцип единственной ответственности гласит: класс должен иметь только одну причину для изменения. Часто это интерпретируют как «один класс — одна задача».
Плохой пример: класс, который и хранит данные пользователя, и работает с базой, и отправляет письма.
// Нарушение SRP: класс делает слишком много
class User {
constructor(public email: string, public name: string) {}
saveToDatabase(): void {
// логика сохранения в БД
}
sendWelcomeEmail(): void {
// логика отправки письма
}
}
Правильный вариант — разделить ответственности:
class User {
constructor(public email: string, public name: string) {}
}
class UserRepository {
save(user: User): void {
// сохранение в базу данных
}
}
class EmailService {
sendWelcome(user: User): void {
// отправка приветственного письма
}
}
Теперь изменение схемы БД не затронет логику отправки писем, и наоборот.
O — Open/Closed Principle
Классы должны быть открыты для расширения, но закрыты для модификации. Добавление нового поведения не должно требовать правок старого кода.
interface PaymentMethod {
pay(amount: number): void;
}
class CardPayment implements PaymentMethod {
pay(amount: number): void {
// оплата картой
}
}
class CryptoPayment implements PaymentMethod {
pay(amount: number): void {
// оплата криптовалютой
}
}
class Checkout {
process(method: PaymentMethod, amount: number): void {
method.pay(amount);
}
}
Добавление нового способа оплаты сводится к созданию нового класса, реализующего PaymentMethod. Класс Checkout менять не нужно.
L — Liskov Substitution Principle
Объекты подкласса должны быть взаимозаменяемы с объектами базового класса без нарушения корректности программы.
Классический пример — Прямоугольник и Квадрат. Если Square наследуется от Rectangle и переопределяет сеттеры так, что ширина и высота всегда равны, это сломает код, который ожидает поведение прямоугольника.
// Правильный подход: общий интерфейс вместо наследования
interface Shape {
area(): number;
}
class Rectangle implements Shape {
constructor(private width: number, private height: number) {}
area(): number {
return this.width * this.height;
}
}
class Square implements Shape {
constructor(private side: number) {}
area(): number {
return this.side * this.side;
}
}
Здесь обе фигуры реализуют контракт Shape без нарушения ожидаемого поведения.
I — Interface Segregation Principle
Клиенты не должны зависеть от методов, которые они не используют. Лучше несколько узких интерфейсов, чем один «жирный».
// Плохо: один большой интерфейс
interface Worker {
work(): void;
eat(): void;
sleep(): void;
}
// Хорошо: разделение по ролям
interface Workable {
work(): void;
}
interface Eatable {
eat(): void;
}
class Robot implements Workable {
work(): void {
// робот работает, но не ест
}
}
class Human implements Workable, Eatable {
work(): void {}
eat(): void {}
}
Теперь класс Robot не обязан реализовывать ненужные методы.
D — Dependency Inversion Principle
Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба должны зависеть от абстракций.
interface Logger {
log(message: string): void;
}
class ConsoleLogger implements Logger {
log(message: string): void {
console.log(message);
}
}
class OrderService {
// зависимость от абстракции, а не от конкретной реализации
constructor(private logger: Logger) {}
createOrder(): void {
this.logger.log('Заказ создан');
}
}
const service = new OrderService(new ConsoleLogger());
service.createOrder();
Легко заменить ConsoleLogger на FileLogger или мок в тестах — OrderService об этом не узнает.
Частые ошибки
Начинающие разработчики часто впадают в крайности. Перечислим типичные проблемы.
- Чрезмерное дробление классов ради SRP. Если класс делает одно осмысленное действие — этого достаточно, не нужно выделять каждую строку в отдельный модуль.
- Использование наследования вместо композиции. LSP легко нарушить, когда подкласс пытается «уточнить» поведение родителя. Композиция и интерфейсы безопаснее.
- Преждевременные абстракции. Создание интерфейсов для классов, у которых заведомо одна реализация, усложняет код без выгоды. Вводите абстракции, когда появляется второй сценарий использования.
- Игнорирование DIP при работе с фреймворками. Прямое создание зависимостей через
newвнутри классов затрудняет тестирование. Используйте внедрение через конструктор или DI-контейнеры. - Смешение SRP и слоистой архитектуры. SRP относится к причинам изменения, а не к количеству методов в классе.
Заключение
Принципы SOLID — это не догма, а ориентир для принятия архитектурных решений. На TypeScript они особенно естественно ложатся благодаря системе типов и интерфейсам. Применяйте их вдумчиво: цель — не идеальный код по чек-листу, а реальное снижение стоимости изменений и упрощение тестирования. Начните с SRP и DIP — они дают наибольшую отдачу в большинстве проектов. Со временем SOLID станет частью интуиции, и вы будете применять эти принципы автоматически при проектировании любых модулей.






Комментарии
0