6. SOLID: Dependency Inversion
Design Patterns. Урок: SOLID — Dependency Inversion Principle
Заголовок раздела «Design Patterns. Урок: SOLID — Dependency Inversion Principle»
- Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба должны зависеть от абстракций.
- Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
DIP — самый «архитектурный» из принципов SOLID. Именно он лежит в основе Dependency Injection и инверсии управления (IoC).
Проблема жёстких зависимостей
Заголовок раздела «Проблема жёстких зависимостей»// ❌ Высокоуровневый модуль жёстко зависит от низкоуровневогоclass OrderService { // Прямая зависимость от конкретных классов private emailSender = new GmailSender(); private database = new MySQLDatabase(); private logger = new FileLogger();
async processOrder(order: Order): Promise<void> { await this.database.save(order); await this.emailSender.send(order.userEmail, 'Order confirmed!'); this.logger.log(`Order ${order.id} processed`); }}Проблемы:
- Хочешь заменить Gmail на SendGrid? Меняй
OrderService - Хочешь перейти с MySQL на Postgres? Меняй
OrderService - Как тестировать
OrderServiceбез реального сервера БД и email?
Решение: зависимость через абстракции
Заголовок раздела «Решение: зависимость через абстракции»// ✅ Абстракции (интерфейсы)interface EmailService { send(to: string, message: string): Promise<void>;}
interface Database { save<T>(entity: T): Promise<void>; findById<T>(id: string): Promise<T>;}
interface Logger { log(message: string): void; error(message: string, error?: Error): void;}
// Конкретные реализации (детали зависят от абстракций)class GmailSender implements EmailService { async send(to: string, message: string): Promise<void> { // Gmail-специфичная реализация }}
class SendGridSender implements EmailService { async send(to: string, message: string): Promise<void> { // SendGrid-специфичная реализация }}
class MySQLDatabase implements Database { async save<T>(entity: T): Promise<void> { ... } async findById<T>(id: string): Promise<T> { ... }}
// Высокоуровневый модуль зависит только от абстракцийclass OrderService { constructor( private readonly database: Database, private readonly emailService: EmailService, private readonly logger: Logger, ) {}
async processOrder(order: Order): Promise<void> { await this.database.save(order); await this.emailService.send(order.userEmail, 'Order confirmed!'); this.logger.log(`Order ${order.id} processed`); }}
// Dependency Injection — подключаем конкретные реализации снаружиconst orderService = new OrderService( new MySQLDatabase(), new SendGridSender(), // Легко заменить на GmailSender new ConsoleLogger(),);Dependency Injection (DI)
Заголовок раздела «Dependency Injection (DI)»DI — это паттерн, реализующий DIP. Зависимости передаются извне, а не создаются внутри:
// Constructor Injection (предпочтительный способ)class UserService { constructor( private userRepo: UserRepository, private emailService: EmailService, ) {}}
// Method Injection (если зависимость нужна только для одного метода)class ReportGenerator { generate(data: any[], formatter: ReportFormatter): string { return formatter.format(data); }}
// Property Injection (редко используется)class NotificationSender { logger!: Logger; // Устанавливается снаружи
send(message: string): void { this.logger?.log(`Sending: ${message}`); }}DI контейнер
Заголовок раздела «DI контейнер»В крупных приложениях используют DI-контейнеры (InversifyJS, tsyringe, NestJS):
import { injectable, inject, container } from 'tsyringe';
@injectable()class OrderService { constructor( @inject('Database') private db: Database, @inject('EmailService') private email: EmailService, ) {}}
// Регистрируем реализацииcontainer.register('Database', { useClass: PostgresDatabase });container.register('EmailService', { useClass: SendGridSender });
// Контейнер сам создаёт экземпляр с нужными зависимостямиconst service = container.resolve(OrderService);Тестирование с DIP
Заголовок раздела «Тестирование с DIP»Главное преимущество DIP — лёгкое тестирование через моки:
// Mock-реализации для тестовclass MockEmailService implements EmailService { public sentEmails: Array<{ to: string; message: string }> = [];
async send(to: string, message: string): Promise<void> { this.sentEmails.push({ to, message }); }}
class MockDatabase implements Database { private data = new Map<string, any>();
async save<T extends { id: string }>(entity: T): Promise<void> { this.data.set(entity.id, entity); }
async findById<T>(id: string): Promise<T> { return this.data.get(id) as T; }}
// Тест без реальной БД и email сервисаdescribe('OrderService', () => { it('should send email after processing order', async () => { const mockEmail = new MockEmailService(); const mockDb = new MockDatabase(); const service = new OrderService(mockDb, mockEmail, new ConsoleLogger());
expect(mockEmail.sentEmails).toHaveLength(1); });});DIP в React: Context и Hooks
Заголовок раздела «DIP в React: Context и Hooks»React Context — это DI для компонентов:
// Абстракцияinterface AuthService { login(email: string, password: string): Promise<User>; logout(): Promise<void>; getCurrentUser(): User | null;}
// Context — «DI контейнер» для Reactconst AuthContext = createContext<AuthService | null>(null);
// Provider инжектирует конкретную реализациюfunction App() { const authService = new FirebaseAuthService(); // или MockAuthService для тестов return ( <AuthContext.Provider value={authService}> <Router /> </AuthContext.Provider> );}
// Компонент зависит от абстракции, не от конкретикиfunction LoginForm() { const auth = useContext(AuthContext)!; const handleLogin = async (email: string, password: string) => { await auth.login(email, password); };}Практические задания
Заголовок раздела «Практические задания»- Рефактори следующий класс согласно DIP:
class WeatherWidget { async getWeather(city: string): Promise<string> { const response = await fetch(`https://api.openweathermap.org/weather?q=${city}`); const data = await response.json(); return `${city}: ${data.main.temp}°C`; }}-
Создай мини DI-контейнер: класс
Containerс методамиregister(token, factory)иresolve(token). -
Напиши тесты для
OrderServiceиз примера выше с 3 сценариями. -
Изучи как NestJS реализует DI через декораторы
@Injectable()и@Inject(). -
В чём разница между DIP и DI (Dependency Injection)? (Подсказка: DIP — принцип, DI — паттерн его реализации)