Ответ
В своих Node.js проектах, особенно при построении сервисов на Express или NestJS, я сознательно применяю SOLID. Приведу пример из модуля обработки платежей в интернет-магазине.
1. Принцип единственной ответственности (SRP):
Каждый класс отвечает за одну вещь. Вместо монолитного PaymentController я разделил логику.
// payment/NotificationService.js - Отправка уведомлений
class NotificationService {
constructor(emailClient, smsClient) {
this.emailClient = emailClient;
this.smsClient = smsClient;
}
async sendPaymentSuccess(user, amount) {
await this.emailClient.send(user.email, `Оплата ${amount} прошла успешно`);
// ... логика SMS
}
}
// payment/PaymentGateway.js - Взаимодействие с внешним платёжным шлюзом
class StripeGateway {
async charge(amount, token) {
// Вызов API Stripe
return await stripe.charges.create({ amount, source: token });
}
}
// payment/PaymentProcessor.js - Координация процесса оплаты
class PaymentProcessor {
constructor(gateway, notificationService) {
this.gateway = gateway;
this.notificationService = notificationService;
}
async process(order, paymentToken) {
const result = await this.gateway.charge(order.total, paymentToken);
await this.notificationService.sendPaymentSuccess(order.user, order.total);
return result;
}
}
2. Принцип открытости/закрытости (OCP): Система открыта для расширения, но закрыта для модификации. Чтобы добавить новый платёжный метод (например, PayPal), не меняем существующий код.
// Абстракция платёжного шлюза
class PaymentGateway {
async charge(amount, details) {
throw new Error('Метод charge должен быть реализован');
}
}
class StripeGateway extends PaymentGateway { /* ... */ }
class PayPalGateway extends PaymentGateway {
async charge(amount, details) {
// Реализация для PayPal API
}
}
// Теперь PaymentProcessor может работать с любым шлюзом.
3. Принцип подстановки Барбары Лисков (LSP):
Наследники PaymentGateway взаимозаменяемы. PayPalGateway можно подставить вместо StripeGateway, и PaymentProcessor продолжит работать корректно, так как контракт метода charge соблюдён.
4. Принцип разделения интерфейса (ISP):
Вместо одного "толстого" интерфейса IPaymentService со методами charge, refund, getHistory, generateInvoice, я создаю специфичные интерфейсы.
// В TypeScript это выглядело бы так:
interface IPaymentExecutor {
charge(amount: number, details: any): Promise<PaymentResult>;
}
interface IRefundHandler {
refund(paymentId: string): Promise<void>;
}
// Класс может реализовывать только нужные ему интерфейсы.
class BasicGateway implements IPaymentExecutor { /* только charge */ }
class FullFeaturedGateway implements IPaymentExecutor, IRefundHandler { /* оба метода */ }
5. Принцип инверсии зависимостей (DIP): Модули верхнего уровня (бизнес-логика) не зависят от модулей нижнего уровня (детали реализации). Это достигается через Dependency Injection (DI).
// В Express с ручной инъекцией
const stripeGateway = new StripeGateway(process.env.STRIPE_KEY);
const notificationService = new NotificationService(emailClient, smsClient);
const paymentProcessor = new PaymentProcessor(stripeGateway, notificationService);
app.post('/pay', (req, res) => {
// Контроллер зависит от абстракции PaymentProcessor
await paymentProcessor.process(req.body.order, req.body.token);
res.sendStatus(200);
});
Практическая польза:
- Тестируемость:
PaymentProcessorлегко протестировать с мок-объектамиgatewayиnotificationService. - Гибкость: Замена Stripe на другой шлюз требует изменения только в одном месте — конфигурации DI.
- Поддержка: Из-за чёткого разделения ответственности, исправление бага в отправке email не затронет логику списания денег.
В фреймворке NestJS эти принципы заложены в архитектуру из коробки, что делает код ещё более чистым и сопровождаемым.