← Назад к вопросам
Как понизить зацепление сервисов с инфраструктурой и доменной моделью?
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>;
}
Лучшие практики
- Определи интерфейсы (контракты) - перед реализацией
- Инъекция зависимостей - передавай зависимости в конструктор
- Слоистая архитектура - четко разделяй слои
- Используй абстракции - не конкретные классы
- IoC контейнер - централизованное управление зависимостями
- Тестируй с mock объектами - благодаря абстракциям
- SOLID принципы - особенно Dependency Inversion
Снижение связанности делает код более гибким, тестируемым и maintainable.