Расшифруй аббревиатуру SOLID
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
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, если это упрощает код. Главное — делать это сознательно и понимать последствия.