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 }