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

Расшифруй аббревиатуру SOLID

1.3 Junior🔥 221 комментариев
#Архитектура и паттерны#ООП

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI21 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

SOLID: пять принципов Object-Oriented Design

SOLID — это аббревиатура пяти принципов, которые помогают написать масштабируемый, поддерживаемый и гибкий код. Вот расшифровка:

S: Single Responsibility Principle (SRP)

Одна сущность должна иметь только одну причину для изменения.

Класс, функция, модуль должны отвечать за что-то одно. Если у класса несколько причин меняться, пора его разделить.

// ❌ Нарушение SRP: класс делает слишком много
class User {
  name: string;
  email: string;
  
  // Бизнес-логика
  isActive(): boolean {
    return this.email && this.name.length > 0;
  }
  
  // Валидация
  validateEmail(): boolean {
    return /^[^@]+@[^@]+$/.test(this.email);
  }
  
  // Сохранение в БД
  async save(): Promise<void> {
    await db.query('INSERT INTO users ...');
  }
  
  // Отправка email
  async sendWelcomeEmail(): Promise<void> {
    await email.send({ to: this.email, subject: 'Welcome!' });
  }
  
  // Логирование
  log(): void {
    console.log(`User created: ${this.email}`);
  }
}

// Причины для изменения:
// 1. Изменилась бизнес-логика активности
// 2. Изменилась валидация email
// 3. Изменилась схема БД
// 4. Изменился сервис email
// 5. Изменился формат логов
// СЛИШКОМ МНОГО!

// ✅ Правильно: разделяем ответственность

// 1. Entity (только данные)
class User {
  constructor(public name: string, public email: string) {}
}

// 2. Бизнес-логика
class UserService {
  isActive(user: User): boolean {
    return user.email && user.name.length > 0;
  }
}

// 3. Валидация
class EmailValidator {
  validate(email: string): boolean {
    return /^[^@]+@[^@]+$/.test(email);
  }
}

// 4. Репозиторий (работа с БД)
class UserRepository {
  async save(user: User): Promise<void> {
    await db.query('INSERT INTO users (name, email) VALUES ($1, $2)', 
      [user.name, user.email]);
  }
}

// 5. Уведомления
class EmailService {
  async sendWelcomeEmail(to: string): Promise<void> {
    await email.send({ to, subject: 'Welcome!' });
  }
}

// 6. Логирование
class Logger {
  log(message: string): void {
    console.log(message);
  }
}

// Теперь каждый класс меняется по одной причине:
// UserService меняется только если бизнес-правило меняется
// UserRepository меняется только если схема БД меняется
// И т.д.

O: Open/Closed Principle (OCP)

Классы должны быть открыты для расширения, но закрыты для модификации.

Это значит: ты можешь добавлять новую функциональность БЕЗ изменения существующего кода.

// ❌ Нарушение OCP: чтобы добавить новый способ уведомления, меняю класс
class NotificationService {
  send(type: string, message: string): void {
    if (type === 'email') {
      console.log(`Sending email: ${message}`);
    } else if (type === 'sms') {
      console.log(`Sending SMS: ${message}`);
    } else if (type === 'slack') {
      console.log(`Sending Slack: ${message}`);
    }
    // Каждый раз модифицирую этот класс!
  }
}

// ✅ Правильно: используем полиморфизм
interface Notifier {
  send(message: string): Promise<void>;
}

class EmailNotifier implements Notifier {
  async send(message: string): Promise<void> {
    await email.send({ body: message });
  }
}

class SmsNotifier implements Notifier {
  async send(message: string): Promise<void> {
    await sms.send({ body: message });
  }
}

class SlackNotifier implements Notifier {
  async send(message: string): Promise<void> {
    await slack.send({ text: message });
  }
}

class NotificationService {
  constructor(private notifier: Notifier) {}
  
  async notify(message: string): Promise<void> {
    await this.notifier.send(message);
  }
}

// Теперь, чтобы добавить WhatsAppNotifier, я НЕ меняю NotificationService
// Просто создаю новый класс, реализующий Notifier
class WhatsAppNotifier implements Notifier {
  async send(message: string): Promise<void> {
    await whatsapp.send({ body: message });
  }
}

// И использую:
const service = new NotificationService(new WhatsAppNotifier());
await service.notify('Hello!');

L: Liskov Substitution Principle (LSP)

Объект подкласса должен легко заменять объект базового класса.

Если класс B расширяет класс A, то я должен иметь возможность везде использовать B вместо A без проблем.

// ❌ Нарушение LSP
abstract class Bird {
  abstract fly(): void;
}

class Eagle extends Bird {
  fly(): void {
    console.log('Eagle is flying');
  }
}

class Penguin extends Bird {
  fly(): void {
    throw new Error('Penguins cannot fly!'); // ❌ Нарушение!
  }
}

function makeBirdFly(bird: Bird): void {
  bird.fly(); // Работает для Eagle, но крашится для Penguin
}

// ✅ Правильно: используем правильную иерархию
abstract class Bird {
  abstract move(): void;
}

abstract class FlyingBird extends Bird {
  abstract fly(): void;
  move(): void {
    this.fly();
  }
}

abstract class SwimmingBird extends Bird {
  abstract swim(): void;
  move(): void {
    this.swim();
  }
}

class Eagle extends FlyingBird {
  fly(): void {
    console.log('Eagle is flying');
  }
}

class Penguin extends SwimmingBird {
  swim(): void {
    console.log('Penguin is swimming');
  }
}

function makeBirdMove(bird: Bird): void {
  bird.move(); // Работает для Eagle И для Penguin
}

I: Interface Segregation Principle (ISP)

Лучше много специфичных интерфейсов, чем один универсальный.

Класс не должен быть вынужден реализовать методы, которые он не использует.

// ❌ Нарушение ISP: интерфейс слишком большой
interface Worker {
  work(): void;
  eat(): void;
  sleep(): void;
  manage(): void;      // не все работники это делают
  code(): void;        // не все работники это делают
  test(): void;        // не все работники это делают
}

class Manager implements Worker {
  work(): void { /* управление */ }
  eat(): void { /* обед */ }
  sleep(): void { /* сон */ }
  manage(): void { /* управление */ }
  code(): void { throw new Error('Managers don\'t code'); } // ❌
  test(): void { throw new Error('Managers don\'t test'); }  // ❌
}

// ✅ Правильно: разделяем интерфейсы
interface Workable {
  work(): void;
}

interface Eatable {
  eat(): void;
}

interface Sleepable {
  sleep(): void;
}

interface Manageable {
  manage(): void;
}

interface Codeable {
  code(): void;
}

interface Testable {
  test(): void;
}

class Manager implements Workable, Eatable, Sleepable, Manageable {
  work(): void { /* управление */ }
  eat(): void { /* обед */ }
  sleep(): void { /* сон */ }
  manage(): void { /* управление */ }
  // code() и test() не нужны
}

class Developer implements Workable, Eatable, Sleepable, Codeable, Testable {
  work(): void { /* разработка */ }
  eat(): void { /* обед */ }
  sleep(): void { /* сон */ }
  code(): void { /* кодирование */ }
  test(): void { /* тестирование */ }
  // manage() не нужен
}

D: Dependency Inversion Principle (DIP)

Высокоуровневые модули не должны зависеть от низкоуровневых. Обе должны зависеть от абстракций.

// ❌ Нарушение DIP: высокий уровень зависит от низкого
class MySQLDatabase {
  async save(data: any): Promise<void> {
    // Сохранение в MySQL
  }
}

class UserService {
  constructor(private db: MySQLDatabase) {}
  
  async createUser(name: string): Promise<void> {
    const user = { name, createdAt: new Date() };
    await this.db.save(user); // Жёсткая зависимость от MySQL!
  }
}

// Проблема: если хочу переключиться на PostgreSQL, меняю UserService!

// ✅ Правильно: оба зависят от абстракции
interface Database {
  save(data: any): Promise<void>;
  find(id: string): Promise<any>;
}

class MySQLDatabase implements Database {
  async save(data: any): Promise<void> {
    // MySQL реализация
  }
  async find(id: string): Promise<any> {
    // MySQL реализация
  }
}

class PostgresDatabase implements Database {
  async save(data: any): Promise<void> {
    // Postgres реализация
  }
  async find(id: string): Promise<any> {
    // Postgres реализация
  }
}

class UserService {
  constructor(private db: Database) {} // зависимость от ИНТЕРФЕЙСА
  
  async createUser(name: string): Promise<void> {
    const user = { name, createdAt: new Date() };
    await this.db.save(user); // работает с любой БД!
  }
}

// Теперь легко переключаться:
const mysqlDb = new MySQLDatabase();
const postgresDb = new PostgresDatabase();

const service1 = new UserService(mysqlDb);
const service2 = new UserService(postgresDb);
// UserService НЕ изменился!

Практический пример: Node.js API с SOLID

// 1. Интерфейсы (абстракции)
interface Repository<T> {
  create(data: T): Promise<T>;
  findById(id: string): Promise<T | null>;
  update(id: string, data: Partial<T>): Promise<T>;
  delete(id: string): Promise<void>;
}

interface EmailService {
  send(to: string, subject: string, body: string): Promise<void>;
}

interface Logger {
  info(message: string, context?: any): void;
  error(message: string, error: Error): void;
}

// 2. Сущность (Entity)
class User {
  id: string;
  email: string;
  name: string;
  createdAt: Date;
  
  constructor(email: string, name: string) {
    this.id = generateId();
    this.email = email;
    this.name = name;
    this.createdAt = new Date();
  }
}

// 3. Репозиторий (Dependency Inversion)
class UserRepository implements Repository<User> {
  async create(data: User): Promise<User> {
    await db.query('INSERT INTO users ...');
    return data;
  }
  
  async findById(id: string): Promise<User | null> {
    const result = await db.query('SELECT * FROM users WHERE id = $1', [id]);
    return result.rows[0] || null;
  }
  
  async update(id: string, data: Partial<User>): Promise<User> {
    // ...
    return new User('email', 'name');
  }
  
  async delete(id: string): Promise<void> {
    await db.query('DELETE FROM users WHERE id = $1', [id]);
  }
}

// 4. Сервис (Single Responsibility)
class CreateUserUseCase {
  constructor(
    private userRepository: Repository<User>,
    private emailService: EmailService,
    private logger: Logger
  ) {}
  
  async execute(email: string, name: string): Promise<User> {
    try {
      // Бизнес-логика
      const user = new User(email, name);
      
      // Сохранение
      await this.userRepository.create(user);
      
      // Уведомление
      await this.emailService.send(
        email,
        'Welcome!',
        `Hello ${name}, welcome to our app!`
      );
      
      this.logger.info(`User created: ${email}`);
      
      return user;
    } catch (error) {
      this.logger.error('Failed to create user', error as Error);
      throw error;
    }
  }
}

// 5. Controller (тонкий слой)
class UserController {
  constructor(private createUserUseCase: CreateUserUseCase) {}
  
  async create(req: Request, res: Response): Promise<void> {
    const { email, name } = req.body;
    const user = await this.createUserUseCase.execute(email, name);
    res.json(user);
  }
}

// 6. Инъекция зависимостей (Composition Root)
const userRepository = new UserRepository();
const emailService = new GmailService(); // реализация EmailService
const logger = new WinstonLogger();       // реализация Logger

const createUserUseCase = new CreateUserUseCase(
  userRepository,
  emailService,
  logger
);

const userController = new UserController(createUserUseCase);

// Для тестирования:
const mockEmailService = {
  send: jest.fn(),
} as unknown as EmailService;

const createUserUseCaseForTests = new CreateUserUseCase(
  userRepository,
  mockEmailService,
  logger
);

Благ Checklist

S — Single Responsibility:

  • Класс имеет одну причину для изменения
  • Методы сгруппированы по смыслу
  • Нет "god classes"

O — Open/Closed:

  • Новую функциональность добавляю БЕЗ изменения старого кода
  • Использую полиморфизм вместо if/else
  • Интерфейсы вместо конкретных классов

L — Liskov Substitution:

  • Подклассы заменяют базовые классы без проблем
  • Нет неожиданных выключений в переопределённых методах
  • Контракт соблюдается

I — Interface Segregation:

  • Интерфейсы узкие и специфичные
  • Класс не имплементит ненужные методы
  • Избегаю больших "fat" интерфейсов

D — Dependency Inversion:

  • Зависимости инъектируются, не жёстко закодированы
  • Сервис зависит от интерфейса, не от реализации
  • Легко подменить реализацию на тесты

Итог

SOLID помогает написать код, который:

  • Легко понять
  • Легко менять и расширять
  • Легко тестировать
  • Не становится помехой при добавлении новых фич

Это не догма, это руководство. Иногда можно слегка нарушить SOLID, если это упрощает код. Главное — делать это сознательно и понимать последствия.