195 lines
9.2 KiB
Go
195 lines
9.2 KiB
Go
|
|
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
|
|||
|
|
}
|