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

Как понизить зацепление сервисов с инфраструктурой и доменной моделью?

2.7 Senior🔥 202 комментариев
#Архитектура и паттерны

Комментарии (2)

🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Снижение связанности (loose coupling) между сервисами и инфраструктурой

Это вопрос про архитектуру и применение принципов SOLID, особенно Dependency Inversion и Adapter паттернов. Расскажу как правильно организовать код.

1. Проблема: тесное связывание (tight coupling)

Сначала покажу плохую архитектуру:

// BAD: Сервис напрямую зависит от конкретной реализации
class UserService {
  constructor() {
    // Жёсткая зависимость от PostgreSQL драйвера
    this.db = new PostgresClient();
    this.cache = new RedisCache();
  }

  async getUser(id) {
    // Сервис "знает" о деталях инфраструктуры
    const result = this.db.query(`SELECT * FROM users WHERE id = $1`, [id]);
    return result;
  }
}

// Проблемы:
// 1. Трудно тестировать (нужны настоящие БД и Redis)
// 2. Трудно менять реализацию (PostgreSQL -> MongoDB)
// 3. Сервис смешивает бизнес-логику с деталями инфраструктуры

2. Решение: Dependency Injection + Interfaces

Используй абстракции вместо конкретных реализаций:

// GOOD: Сервис зависит от абстракции (интерфейса)

// Определяю контракт (интерфейс)
class UserRepository {
  async findById(id) {
    throw new Error('Not implemented');
  }
  async save(user) {
    throw new Error('Not implemented');
  }
}

class CacheService {
  async get(key) {
    throw new Error('Not implemented');
  }
  async set(key, value) {
    throw new Error('Not implemented');
  }
}

// Сервис зависит от абстракций, а не от конкретных реализаций
class UserService {
  constructor(userRepository, cacheService) {
    // Инъекция зависимостей - сервис не создаёт сам
    this.repository = userRepository;
    this.cache = cacheService;
  }

  async getUser(id) {
    // Сервис НЕ знает как работает БД или кеш
    // Просто использует интерфейс
    const cached = await this.cache.get(`user:${id}`);
    if (cached) {
      return cached;
    }

    const user = await this.repository.findById(id);
    await this.cache.set(`user:${id}`, user);
    return user;
  }
}

// Конкретная реализация для PostgreSQL
class PostgresUserRepository extends UserRepository {
  constructor(pgClient) {
    super();
    this.db = pgClient;
  }

  async findById(id) {
    const result = await this.db.query(
      'SELECT * FROM users WHERE id = $1',
      [id]
    );
    return result.rows[0];
  }

  async save(user) {
    // PostgreSQL сохранение
  }
}

// Конкретная реализация для Redis
class RedisCache extends CacheService {
  constructor(redisClient) {
    super();
    this.redis = redisClient;
  }

  async get(key) {
    return await this.redis.get(key);
  }

  async set(key, value) {
    await this.redis.setex(key, 3600, JSON.stringify(value));
  }
}

// Использование:
const pgRepository = new PostgresUserRepository(pgClient);
const redisCache = new RedisCache(redisClient);
const userService = new UserService(pgRepository, redisCache);

// Для тестирования:
class MockUserRepository extends UserRepository {
  async findById(id) {
    return { id, name: 'Test User' };
  }
}

const mockService = new UserService(
  new MockUserRepository(),
  new MockCache()
);

3. TypeScript для явных интерфейсов

Типизация делает контракты явными:

// Определяю интерфейсы для договора
interface IUserRepository {
  findById(id: string): Promise<User>;
  save(user: User): Promise<void>;
  delete(id: string): Promise<void>;
}

interface ICacheService {
  get<T>(key: string): Promise<T | null>;
  set<T>(key: string, value: T, ttl?: number): Promise<void>;
  delete(key: string): Promise<void>;
}

interface ILogger {
  info(message: string): void;
  error(message: string, error?: Error): void;
}

// Сервис зависит от интерфейсов
class UserService {
  constructor(
    private repository: IUserRepository,
    private cache: ICacheService,
    private logger: ILogger
  ) {}

  async getUser(id: string): Promise<User> {
    try {
      // Сервис знает только интерфейсы
      const cached = await this.cache.get<User>(`user:${id}`);
      if (cached) {
        this.logger.info(`User ${id} from cache`);
        return cached;
      }

      const user = await this.repository.findById(id);
      if (!user) {
        throw new Error('User not found');
      }

      await this.cache.set(`user:${id}`, user, 3600);
      return user;
    } catch (error) {
      this.logger.error('Failed to get user', error);
      throw error;
    }
  }
}

// Реализация для PostgreSQL
class PostgresUserRepository implements IUserRepository {
  constructor(private db: Database) {}

  async findById(id: string): Promise<User> {
    const query = 'SELECT * FROM users WHERE id = $1';
    const result = await this.db.query(query, [id]);
    return result.rows[0];
  }

  async save(user: User): Promise<void> {
    // Postgresql save
  }

  async delete(id: string): Promise<void> {
    // Postgresql delete
  }
}

// Реализация для MongoDB
class MongoUserRepository implements IUserRepository {
  constructor(private db: MongoDatabase) {}

  async findById(id: string): Promise<User> {
    return await this.db.collection('users').findOne({ _id: id });
  }

  async save(user: User): Promise<void> {
    // MongoDB save
  }

  async delete(id: string): Promise<void> {
    // MongoDB delete
  }
}

// Реализация для Redis
class RedisCacheService implements ICacheService {
  constructor(private redis: RedisClient) {}

  async get<T>(key: string): Promise<T | null> {
    const data = await this.redis.get(key);
    return data ? JSON.parse(data) : null;
  }

  async set<T>(key: string, value: T, ttl = 3600): Promise<void> {
    await this.redis.setex(key, ttl, JSON.stringify(value));
  }

  async delete(key: string): Promise<void> {
    await this.redis.del(key);
  }
}

// Реализация для логирования
class ConsoleLogger implements ILogger {
  info(message: string): void {
    console.log(`[INFO] ${message}`);
  }

  error(message: string, error?: Error): void {
    console.error(`[ERROR] ${message}`, error);
  }
}

4. Слоистая архитектура (Onion Architecture)

Презентация (Presentation) 
  ↓ зависит от
Приложение (Application Services)
  ↓ зависит от
Домен (Domain / Business Logic)
  ↓ зависит от
Инфраструктура (Infrastructure) 


Зависимости указывают только ВНУТРЬ:
Presentation -> Application -> Domain <- Infrastructure

Домен НЕ зависит от инфраструктуры!
// Domain Layer (не зависит ни от чего)
class User {
  constructor(
    public id: string,
    public name: string,
    public email: string
  ) {}

  isValid(): boolean {
    return this.name.length > 0 && this.email.includes('@');
  }
}

// Application Layer (бизнес-логика, зависит от доменных моделей)
class CreateUserUseCase {
  constructor(private userRepository: IUserRepository) {}

  async execute(name: string, email: string): Promise<User> {
    const user = new User(generateId(), name, email);
    
    if (!user.isValid()) {
      throw new Error('Invalid user data');
    }

    await this.userRepository.save(user);
    return user;
  }
}

// Infrastructure Layer (детали реализации)
class PostgresUserRepository implements IUserRepository {
  // ...
}

// Presentation Layer (Express контроллер)
class UserController {
  constructor(private createUserUseCase: CreateUserUseCase) {}

  async create(req: Request, res: Response) {
    const { name, email } = req.body;
    const user = await this.createUserUseCase.execute(name, email);
    res.json(user);
  }
}

5. Инверсия контроля (IoC контейнер)

Для управления зависимостями:

// IoC контейнер (простая реализация)
class Container {
  private services = new Map<string, any>();

  register(name: string, factory: () => any) {
    this.services.set(name, factory);
  }

  get(name: string) {
    const factory = this.services.get(name);
    if (!factory) {
      throw new Error(`Service ${name} not registered`);
    }
    return factory();
  }
}

// Настройка контейнера
const container = new Container();

// Регистрирую зависимости
container.register('database', () => new PostgresDatabase());
container.register('redis', () => new RedisClient());
container.register('logger', () => new ConsoleLogger());

container.register('userRepository', () => 
  new PostgresUserRepository(container.get('database'))
);

container.register('cache', () => 
  new RedisCacheService(container.get('redis'))
);

container.register('userService', () => 
  new UserService(
    container.get('userRepository'),
    container.get('cache'),
    container.get('logger')
  )
);

// Использование
const userService = container.get('userService');

6. React контекст для инъекции зависимостей

В фронтенде можешь использовать Context для DI:

// Определяю интерфейсы сервисов
interface IAuthService {
  login(email: string, password: string): Promise<void>;
  logout(): void;
  isAuthenticated(): boolean;
}

interface IApiClient {
  get(url: string): Promise<any>;
  post(url: string, data: any): Promise<any>;
}

// Создаю контекст
const ServicesContext = React.createContext<{
  authService: IAuthService;
  apiClient: IApiClient;
}>(null!);

// Provider компонент
function ServicesProvider({ children }: { children: React.ReactNode }) {
  const authService = new AuthService(new ApiClient());
  const apiClient = new ApiClient();

  return (
    <ServicesContext.Provider value={{ authService, apiClient }}>
      {children}
    </ServicesContext.Provider>
  );
}

// Хук для использования сервисов
function useServices() {
  return React.useContext(ServicesContext);
}

// Использование в компоненте
function LoginComponent() {
  const { authService } = useServices(); // Получу сервис из контекста

  const handleLogin = async (email: string, password: string) => {
    await authService.login(email, password);
  };

  return <button onClick={() => handleLogin('user@example.com', 'pass')}>Login</button>;
}

Лучшие практики

  1. Определи интерфейсы (контракты) - перед реализацией
  2. Инъекция зависимостей - передавай зависимости в конструктор
  3. Слоистая архитектура - четко разделяй слои
  4. Используй абстракции - не конкретные классы
  5. IoC контейнер - централизованное управление зависимостями
  6. Тестируй с mock объектами - благодаря абстракциям
  7. SOLID принципы - особенно Dependency Inversion

Снижение связанности делает код более гибким, тестируемым и maintainable.

Как понизить зацепление сервисов с инфраструктурой и доменной моделью? | PrepBro