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

Приведи пример использования 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 означает:

  1. Не импортируй конкретные реализации в бизнес-логику
  2. Определи интерфейс/абстракцию
  3. Реализуй интерфейс в конкретных классах
  4. Инжектируй интерфейс, не конкретный класс
  5. Конфигурируй зависимости в одном месте (IoC контейнер)

Результат:

  • PaymentService не знает о Stripe, SendGrid, PayPal
  • Легко менять реализации
  • Легко тестировать (mock объекты)
  • Код гибкий и масштабируемый
Приведи пример использования Dependency Inversion Principle | PrepBro