Files

195 lines
9.2 KiB
Go
Raw Permalink Normal View History

2026-04-13 08:14:09 +03:00
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
}