Files
2026-04-13 08:14:09 +03:00

195 lines
9.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package app
import (
"log/slog"
"os"
"github.com/olezhek28/di-demo/internal/api"
"github.com/olezhek28/di-demo/internal/cache"
"github.com/olezhek28/di-demo/internal/config"
"github.com/olezhek28/di-demo/internal/database"
"github.com/olezhek28/di-demo/internal/events"
"github.com/olezhek28/di-demo/internal/repository"
"github.com/olezhek28/di-demo/internal/service"
)
// diContainer — контейнер зависимостей с ленивой инициализацией.
//
// Принцип работы:
// Все поля начинаются как nil. При первом вызове геттера (например, DB())
// зависимость создаётся и сохраняется в поле. При повторном вызове —
// возвращается уже созданный экземпляр. Это гарантирует, что каждая
// зависимость существует в единственном экземпляре (singleton в рамках приложения).
//
// Зачем это нужно:
// В отличие от плоского main.go, где порядок строк определяет порядок инициализации,
// здесь порядок определяется самим графом зависимостей. Каждый геттер вызывает
// геттеры своих зависимостей — и они рекурсивно создаются автоматически.
// Не нужно думать «что создать первым» — контейнер разберётся сам.
//
// Как добавить новую зависимость:
// 1. Добавить поле в структуру
// 2. Написать геттер с проверкой на nil
// 3. Вызвать геттер из нужных мест — всё
// Никакой перестановки строк в main.go.
//
// Почему поля — интерфейсы:
// Каждый пакет экспортирует интерфейс (database.DB, cache.Cache и т.д.),
// а конкретная реализация скрыта (неэкспортируемая структура).
// Это гарантирует, что объект можно создать ТОЛЬКО через конструктор (New).
// Нельзя написать &database.db{} — структура не видна за пределами пакета.
// Контейнер зависит от абстракций, а не от конкретных типов.
//
// Почему геттеры не возвращают ошибку:
// Если зависимость не создалась (например, база не подключилась) —
// приложение всё равно не может работать. Нет смысла протаскивать ошибку
// через 5 уровней вызовов — проще сразу завершить процесс.
// Это упрощает код: геттеры становятся однострочными, без if err != nil.
type diContainer struct {
// Инфраструктура — базовые ресурсы, от которых зависит всё остальное.
db database.DB
cache cache.Cache
eventBus events.EventBus
// Репозитории — слой доступа к данным. Зависят от инфраструктуры.
userRepo repository.UserRepo
sessionRepo repository.SessionRepo
notificationRepo repository.NotificationRepo
// Сервисы — бизнес-логика. Зависят от репозиториев и друг от друга.
authService service.AuthService
userService service.UserService
notificationService service.NotificationService
// API — хендлеры и роутинг. Зависит от сервисов.
handler api.Handler
}
// newDIContainer создаёт новый пустой контейнер.
// Все поля nil — зависимости будут создаваться лениво при первом обращении.
func newDIContainer() *diContainer {
return &diContainer{}
}
// DB возвращает подключение к базе данных.
// Создаётся один раз при первом вызове. При повторном — возвращается тот же экземпляр.
// Если подключение не удалось — завершаем процесс: без базы приложение бессмысленно.
func (d *diContainer) DB() database.DB {
if d.db == nil {
db, err := database.New(config.AppConfig().DSN)
if err != nil {
slog.Error("не удалось подключиться к БД", "err", err)
os.Exit(1)
}
d.db = db
}
return d.db
}
// EventBus возвращает шину событий.
// Чистый pub/sub брокер без зависимостей — все сервисы зависят от него, а не наоборот.
// В антипаттерне EventBus зависел от NotificationService (тупик с циклом).
// Здесь — EventBus не зависит ни от кого, а сервисы подключаются к нему сами.
func (d *diContainer) EventBus() events.EventBus {
if d.eventBus == nil {
d.eventBus = events.NewEventBus()
}
return d.eventBus
}
// Cache возвращает подключение к кэшу.
// Создаётся один раз при первом вызове.
func (d *diContainer) Cache() cache.Cache {
if d.cache == nil {
d.cache = cache.New(config.AppConfig().RedisAddr)
}
return d.cache
}
// UserRepo возвращает репозиторий пользователей.
// Зависит от DB — если база ещё не создана, d.DB() создаст её автоматически.
func (d *diContainer) UserRepo() repository.UserRepo {
if d.userRepo == nil {
d.userRepo = repository.NewUserRepo(d.DB())
}
return d.userRepo
}
// SessionRepo возвращает репозиторий сессий.
// Зависит от DB и Cache — оба подтянутся автоматически при первом вызове.
func (d *diContainer) SessionRepo() repository.SessionRepo {
if d.sessionRepo == nil {
d.sessionRepo = repository.NewSessionRepo(d.DB(), d.Cache())
}
return d.sessionRepo
}
// NotificationRepo возвращает репозиторий уведомлений.
// Зависит от DB.
func (d *diContainer) NotificationRepo() repository.NotificationRepo {
if d.notificationRepo == nil {
d.notificationRepo = repository.NewNotificationRepo(d.DB())
}
return d.notificationRepo
}
// AuthService возвращает сервис авторизации.
// Зависит от UserRepo, SessionRepo, Cache и EventBus.
// Все зависимости подтягиваются рекурсивно — если UserRepo ещё не создан,
// он создастся, а для этого создастся DB, и так далее по цепочке.
// EventBus добавился одной строкой — никакой перестановки, никаких тупиков.
func (d *diContainer) AuthService() service.AuthService {
if d.authService == nil {
d.authService = service.NewAuthService(d.UserRepo(), d.SessionRepo(), d.Cache(), d.EventBus())
}
return d.authService
}
// UserService возвращает сервис пользователей.
// Зависит от UserRepo и AuthService — перекрёстная зависимость между сервисами.
// В плоском main.go это создавало проблему с порядком строк.
// Здесь — просто вызываем d.AuthService(), и он рекурсивно создаст всё что нужно.
func (d *diContainer) UserService() service.UserService {
if d.userService == nil {
d.userService = service.NewUserService(d.UserRepo(), d.AuthService(), d.EventBus())
}
return d.userService
}
// NotificationService возвращает сервис уведомлений.
// Зависит от NotificationRepo и UserService.
// UserService → AuthService → UserRepo → DB — вся цепочка разрешится автоматически.
func (d *diContainer) NotificationService() service.NotificationService {
if d.notificationService == nil {
d.notificationService = service.NewNotificationService(d.NotificationRepo(), d.UserService(), d.EventBus())
}
return d.notificationService
}
// Handler возвращает HTTP-хендлер.
// Вершина графа зависимостей — зависит от всех трёх сервисов.
// Один вызов d.Handler() рекурсивно создаст ВСЕ зависимости приложения.
// Хендлер отвечает только за роутинг и обработку запросов.
// http.Server с адресом создаётся в app.go — это инфраструктура, а не API.
func (d *diContainer) Handler() api.Handler {
if d.handler == nil {
d.handler = api.NewHandler(
d.UserService(),
d.AuthService(),
d.NotificationService(),
)
}
return d.handler
}