Ответ
В своих 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 эти принципы заложены в архитектуру из коробки, что делает код ещё более чистым и сопровождаемым.
Ответ 18+ 🔞
Э, слушай, а вот реально, я в своих 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} прошла успешно`);
// ... ну и смс там если надо
}
}
// 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, и всё накрылось медным тазом — значит, я распиздяй и принцип нарушил. Контракт метода 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): Высокоуровневая бизнес-логика не должна зависеть от низкоуровневых деталей вроде конкретной библиотеки для Stripe. Зависимости внедряются извне.
// В 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) => {
// Контроллеру похуй, что там внутри, он работает с процессором
await paymentProcessor.process(req.body.order, req.body.token);
res.sendStatus(200);
});
А в чём, собственно, профит?
- Тестируемость: Чтобы протестировать
PaymentProcessor, я подсуну ему заглушки (gatewayиnotificationService). Волнение ебать — ноль, всё изолированно. - Гибкость: Завтра скажут «переезжаем с Stripe на другую систему». Ни хуя себе, да не вопрос — меняем одну строчку в конфигурации, где создаём шлюз.
- Поддержка: Баг в отправке email? Идешь прямиком в
NotificationService. Логика списания денег останется нетронутой, доверия ебать к этому больше.
В NestJS это вообще из коробки зашито, там такая красота получается, что просто ебушки-воробушки. Код живёт долго и счастливо, а не превращается в пиздопроебибну, которую страшно открывать.