← Назад к вопросам
Приведи пример использования Dependency Inversion Principle
2.0 Middle🔥 141 комментариев
#Архитектура и паттерны#ООП
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI21 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Dependency Inversion Principle (DIP): Практический пример
DIP гласит: Высокоуровневые модули не должны зависеть от низкоуровневых. Обе должны зависеть от абстракций.
Разберу реальный пример: Payment Service для обработки платежей.
Антипаттерн (нарушение DIP)
// payment.service.ts
// ❌ ПЛОХО: высокий уровень зависит от низкого
import { StripeClient } from '@stripe/client'; // Конкретная реализация
import { SendgridMailer } from '@sendgrid/mailer'; // Конкретная реализация
@Injectable()
export class PaymentService {
constructor(
private stripe: StripeClient, // Жёсткая зависимость от Stripe
private sendgrid: SendgridMailer, // Жёсткая зависимость от SendGrid
) {}
async processPayment(amount: number, cardToken: string): Promise<void> {
try {
// Используем Stripe напрямую
const charge = await this.stripe.charges.create({
amount: amount * 100,
currency: 'usd',
source: cardToken,
});
// Отправляем письмо через SendGrid
await this.sendgrid.send({
to: 'user@example.com',
subject: 'Payment received',
text: `Your payment of $${amount} was successful`,
});
console.log(`Payment processed: ${charge.id}`);
} catch (error) {
console.error('Payment failed', error);
throw error;
}
}
}
// Проблемы:
// 1. Если хочу переключиться с Stripe на PayPal — меняю PaymentService ❌
// 2. Если хочу переключиться с SendGrid на Mailgun — меняю PaymentService ❌
// 3. Тестировать сложно: нужны реальные Stripe/SendGrid клиенты
// 4. Несоответствие уровней абстракции
Правильный подход (с DIP)
// 1. Определяю АБСТРАКЦИИ (интерфейсы)
// payment-provider.interface.ts
export interface PaymentProvider {
charge(amount: number, cardToken: string): Promise<PaymentResult>;
}
export interface PaymentResult {
id: string;
amount: number;
currency: string;
status: 'success' | 'failed';
}
// email-service.interface.ts
export interface EmailService {
send(to: string, subject: string, body: string): Promise<void>;
}
// 2. Реализую абстракции (конкретные классы)
// stripe-payment-provider.ts
@Injectable()
export class StripePaymentProvider implements PaymentProvider {
constructor(private stripeClient: StripeClient) {}
async charge(amount: number, cardToken: string): Promise<PaymentResult> {
const charge = await this.stripeClient.charges.create({
amount: amount * 100,
currency: 'usd',
source: cardToken,
});
return {
id: charge.id,
amount: charge.amount / 100,
currency: charge.currency,
status: charge.status === 'succeeded' ? 'success' : 'failed',
};
}
}
// paypal-payment-provider.ts
@Injectable()
export class PayPalPaymentProvider implements PaymentProvider {
constructor(private paypalClient: PayPalClient) {}
async charge(amount: number, cardToken: string): Promise<PaymentResult> {
const sale = await this.paypalClient.sale.create({
amount: amount.toString(),
currency: 'USD',
paymentToken: cardToken,
});
return {
id: sale.id,
amount: parseFloat(sale.amount),
currency: sale.currency,
status: sale.status === 'created' ? 'success' : 'failed',
};
}
}
// sendgrid-email-service.ts
@Injectable()
export class SendgridEmailService implements EmailService {
constructor(private sendgridClient: SendgridClient) {}
async send(to: string, subject: string, body: string): Promise<void> {
await this.sendgridClient.send({
to,
subject,
text: body,
});
}
}
// mailgun-email-service.ts
@Injectable()
export class MailgunEmailService implements EmailService {
constructor(private mailgunClient: MailgunClient) {}
async send(to: string, subject: string, body: string): Promise<void> {
await this.mailgunClient.messages.create({
from: 'noreply@example.com',
to,
subject,
text: body,
});
}
}
// 3. PaymentService зависит от АБСТРАКЦИЙ, не от конкретики
// payment.service.ts
@Injectable()
export class PaymentService {
constructor(
private paymentProvider: PaymentProvider, // Абстракция! Не Stripe
private emailService: EmailService, // Абстракция! Не SendGrid
) {}
async processPayment(
amount: number,
cardToken: string,
userEmail: string,
): Promise<PaymentResult> {
try {
// Используем интерфейсы, не конкретные реализации
const result = await this.paymentProvider.charge(amount, cardToken);
if (result.status === 'success') {
await this.emailService.send(
userEmail,
'Payment Confirmed',
`Your payment of $${result.amount} ${result.currency} was successful.\nTransaction ID: ${result.id}`,
);
}
return result;
} catch (error) {
this.logger.error('Payment processing failed', error);
throw error;
}
}
}
// 4. Конфигурируем зависимости в модуле
// payment.module.ts
@Module({
providers: [
PaymentService,
// Выбираем реализацию через configuration
{
provide: PaymentProvider,
useClass: process.env.PAYMENT_PROVIDER === 'paypal'
? PayPalPaymentProvider
: StripePaymentProvider,
},
{
provide: EmailService,
useClass: process.env.EMAIL_PROVIDER === 'mailgun'
? MailgunEmailService
: SendgridEmailService,
},
// Или через factory
{
provide: PaymentProvider,
useFactory: (config: ConfigService) => {
if (config.get('PAYMENT_PROVIDER') === 'paypal') {
return new PayPalPaymentProvider(new PayPalClient());
}
return new StripePaymentProvider(new StripeClient());
},
inject: [ConfigService],
},
],
exports: [PaymentService],
})
export class PaymentModule {}
Преимущества
// ✅ Переключение на PayPal
// Меняю ТОЛЬКО конфигурацию в модуле, PaymentService не трогаю!
{
provide: PaymentProvider,
useClass: PayPalPaymentProvider, // Вместо StripePaymentProvider
}
// ✅ Переключение на Mailgun
// Опять же, только конфигурация
{
provide: EmailService,
useClass: MailgunEmailService, // Вместо SendgridEmailService
}
// ✅ Тестирование
// Создаю mock реализацию
class MockPaymentProvider implements PaymentProvider {
async charge(amount: number, cardToken: string): Promise<PaymentResult> {
return {
id: 'mock-123',
amount,
currency: 'usd',
status: 'success',
};
}
}
class MockEmailService implements EmailService {
async send(to: string, subject: string, body: string): Promise<void> {
console.log(`Mock email sent to ${to}`);
}
}
// В тестах
describe('PaymentService', () => {
let paymentService: PaymentService;
let mockPaymentProvider: MockPaymentProvider;
let mockEmailService: MockEmailService;
beforeEach(() => {
mockPaymentProvider = new MockPaymentProvider();
mockEmailService = new MockEmailService();
paymentService = new PaymentService(
mockPaymentProvider as PaymentProvider,
mockEmailService as EmailService,
);
});
it('should process payment and send email', async () => {
const result = await paymentService.processPayment(
100,
'tok_visa',
'user@example.com',
);
expect(result.status).toBe('success');
expect(result.amount).toBe(100);
});
});
Диаграмма зависимостей
// ❌ БЕЗ DIP (проблема)
┌─────────────────────┐
│ PaymentService │
│ (high-level) │
└──────────┬──────────┘
depends on
│ │
▼ ▼
┌──────────┐ ┌──────────┐
│ Stripe │ │ SendGrid │
│ (low) │ │ (low) │
└──────────┘ └──────────┘
Проблема: высокий уровень зависит от низкого
// ✅ С DIP (правильно)
┌─────────────────────┐
│ PaymentService │
│ (high-level) │
└──────────┬──────────┘
depends on
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ PaymentProvider │ │ EmailService │
│ (abstraction) │ │ (abstraction) │
└──────────────────┘ └──────────────────┘
▲ ▲
implements implements
│ │
│ ┌───────────┴───────────┐
│ │ │
┌──────────┐ ┌──────────┐ ┌──────────────┐
│ Stripe │ │ PayPal │ │ SendGrid │
│ (low) │ │ (low) │ │ (low) │
└──────────┘ └──────────┘ └──────────────┘
Преимущество: оба зависят от абстракций
Реальный сценарий использования
// Допустим, нужно в production использовать Stripe
// Но в development мне нужна mock реализация
// development.ts
@Module({
providers: [
{
provide: PaymentProvider,
useClass: MockPaymentProvider, // Mock в development
},
],
})
export class DevPaymentModule {}
// production.ts
@Module({
providers: [
{
provide: PaymentProvider,
useClass: StripePaymentProvider, // Real Stripe
},
],
})
export class ProdPaymentModule {}
// app.module.ts
import { isProd } from './config';
@Module({
imports: [
isProd ? ProdPaymentModule : DevPaymentModule,
],
})
export class AppModule {}
// PaymentService НЕ ИЗМЕНЯЕТСЯ!
// Она работает с PaymentProvider интерфейсом
Ключевые моменты DIP
type DIPRules = {
// 1. Зависи от интерфейсов, не от конкретных классов
bad: new PaymentService(new StripeClient()); // ❌
good: new PaymentService(paymentProvider); // ✅
// 2. Инжектируй зависимости через constructor
bad: {
stripe = new StripeClient(); // Создание в конструкторе
}
good: {
constructor(private stripe: PaymentProvider) {} // Инъекция
}
// 3. Используй интерфейсы для определения контракта
bad: {
process(stripe: StripeClient) {} // Конкретный класс
}
good: {
process(provider: PaymentProvider) {} // Интерфейс
}
};
Итог
Dependency Inversion Principle означает:
- Не импортируй конкретные реализации в бизнес-логику
- Определи интерфейс/абстракцию
- Реализуй интерфейс в конкретных классах
- Инжектируй интерфейс, не конкретный класс
- Конфигурируй зависимости в одном месте (IoC контейнер)
Результат:
- PaymentService не знает о Stripe, SendGrid, PayPal
- Легко менять реализации
- Легко тестировать (mock объекты)
- Код гибкий и масштабируемый