This commit is contained in:
2026-04-13 08:14:09 +03:00
commit 0449337ae7
39 changed files with 2726 additions and 0 deletions
+99
View File
@@ -0,0 +1,99 @@
package main
// Сломанный антипаттерн: тот же код, но строки переставлены.
//
// Сценарий: два разработчика параллельно добавляли сервисы,
// при мерже строки перемешались. Код скомпилировался.
// go build прошёл. CI прошёл (юнит-тесты не трогают main.go).
// Деплой. Первый запрос — паника.
//
// Запустите: go run ./cmd/antipattern-broken/
// curl http://localhost:8080/health → 200 ok (всё «работает»)
// curl http://localhost:8080/users/me → nil pointer dereference (ПАНИКА)
//
// /health не трогает зависимости — поэтому проходит.
// /users/me вызывает userService.GetProfile() → authService.ValidateToken() → nil!
import (
"log/slog"
"net/http"
"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"
)
func main() {
cfg := config.New()
// === Инфраструктура ===
db, err := database.New(cfg.DSN)
if err != nil {
slog.Error("не удалось подключиться к БД", "err", err)
os.Exit(1)
}
// === Репозитории и сервисы ===
// После мержа строки перемешались. Всё компилируется — Go проверяет
// только что переменная ОБЪЯВЛЕНА, а не что внутри валидное значение.
var (
c cache.Cache
userRepo repository.UserRepo
sessionRepo repository.SessionRepo
notificationRepo repository.NotificationRepo
eventBus events.EventBus
authService service.AuthService
userService service.UserService
notificationService service.NotificationService
)
userRepo = repository.NewUserRepo(db)
notificationRepo = repository.NewNotificationRepo(db)
// ⚠️ ПРОБЛЕМА 1: sessionRepo создаётся ДО cache.
// cache = nil → sessionRepo хранит nil вместо кэша.
sessionRepo = repository.NewSessionRepo(db, c) // c == nil!
c = cache.New(cfg.RedisAddr) // поздно — sessionRepo уже создан с nil
eventBus = events.NewEventBus()
// ⚠️ ПРОБЛЕМА 2: userService создаётся ДО authService.
// authService = nil → userService хранит nil вместо сервиса авторизации.
userService = service.NewUserService(userRepo, authService, eventBus) // authService == nil!
authService = service.NewAuthService(userRepo, sessionRepo, c, eventBus) // поздно
notificationService = service.NewNotificationService(
notificationRepo,
userService,
eventBus,
)
// === Сервер ===
// Всё скомпилировалось. go vet пройдёт. Линтер пройдёт.
// Но внутри userService лежит nil вместо authService.
// Первый запрос, который дёрнет авторизацию — паника.
handler := api.NewHandler(userService, authService, notificationService)
srv := &http.Server{
Addr: cfg.HTTPAddr,
Handler: handler.Routes(),
}
slog.Info("сервер запущен", "addr", cfg.HTTPAddr)
if err = srv.ListenAndServe(); err != nil {
slog.Error("ошибка сервера", "err", err)
}
_ = db.Close()
_ = c.Close()
}
+124
View File
@@ -0,0 +1,124 @@
package main
import (
"log/slog"
"net/http"
"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"
)
func main() {
cfg := config.New()
// === Инфраструктура ===
db, err := database.New(cfg.DSN)
if err != nil {
slog.Error("не удалось подключиться к БД", "err", err)
os.Exit(1)
}
c := cache.New(cfg.RedisAddr) // ⚠️ если перенести ниже sessionRepo — nil pointer
// === Репозитории ===
userRepo := repository.NewUserRepo(db)
sessionRepo := repository.NewSessionRepo(db, c) // ⚠️ зависит от db И cache
notificationRepo := repository.NewNotificationRepo(db) // ⚠️ зависит от db
// === EventBus ===
// Пока EventBus — простой брокер без зависимостей, всё нормально.
// Проблемы начнутся когда EventBus понадобится зависимость (см. комментарии ниже).
eventBus := events.NewEventBus()
// === Сервисы ===
// Тут начинается самое интересное — перекрёстные зависимости.
// Порядок критичен: authService нужен для userService,
// а userService нужен для notificationService.
// Переставь любые два — и привет, nil pointer.
authService := service.NewAuthService(userRepo, sessionRepo, c, eventBus) // ⚠️ зависит от repos, cache, eventBus
userService := service.NewUserService(userRepo, authService, eventBus) // ⚠️ зависит от authService — порядок!
notificationService := service.NewNotificationService( // ⚠️ зависит от userService — порядок!
notificationRepo,
userService,
eventBus,
)
// === Сервер ===
handler := api.NewHandler(userService, authService, notificationService)
srv := &http.Server{
Addr: cfg.HTTPAddr,
Handler: handler.Routes(),
}
// =====================================================================
// === НОВАЯ ЗАДАЧА: EventBus должен сам доставлять уведомления ===
// =====================================================================
//
// Продакт говорит: EventBus должен не просто пересылать события,
// а сам доставлять уведомления. Нужно чтобы EventBus вызывал
// NotificationService напрямую.
//
// Сейчас EventBus — простой брокер:
//
// type EventBus interface {
// Publish(event string)
// Subscribe(event string, handler func())
// }
// func NewEventBus() EventBus
//
// А нужно так:
//
// type EventBus interface {
// Publish(event string)
// Subscribe(event string, handler func())
// }
// func NewEventBus(notifications NotificationSender) EventBus
//
// Казалось бы — добавили одно поле. Но теперь зависимости:
//
// EventBus → нужен NotificationService
// NotificationService → нужен UserService
// UserService → нужен AuthService
// AuthService → нужен EventBus ← ЦИКЛ
//
// Куда воткнуть EventBus в этот main.go?
//
// Попытка 1: создать EventBus после notificationService
// eventBus := events.NewEventBus(notificationService)
// ...но authService и userService уже созданы с eventBus выше (строки 48-49).
// Поздно — они уже инициализированы со старым eventBus.
//
// Попытка 2: создать EventBus до сервисов, передать nil
// eventBus := events.NewEventBus(nil) // NotificationService ещё не существует
// ...потом: eventBus.SetNotificationService(notificationService)
// Костыль с мутабельным состоянием. И гонка: кто-то вызовет
// EventBus до SetNotificationService — получит nil pointer.
//
// Попытка 3: переписать всю цепочку
// Разорвать одну из зависимостей, переделать интерфейсы...
// Пол дня работы. И страшно — вдруг сломаю то что уже работает.
//
// Вот она — хрупкость в действии.
// Одна зависимость с одним полем — и вся инициализация встала колом.
// =====================================================================
slog.Info("всё хорошо, сервер запускается")
if err = srv.ListenAndServe(); err != nil {
slog.Error("ошибка сервера", "err", err)
}
_ = db.Close()
_ = c.Close()
}
+115
View File
@@ -0,0 +1,115 @@
package main
// Пример циклической зависимости в Fx.
//
// Граф зависимостей замкнулся в кольцо:
//
// NewEventBus(NotificationSender) — EventBus хочет отправлять уведомления
// NewNotificationService(Users, EventSubscriber) — уведомления хотят знать email
// NewUserService(Auth) — пользователи хотят проверять токен
// NewAuthService(EventPublisher) — авторизация хочет публиковать события
// ↑ ↓
// └──────── EventBus реализует EventPublisher ─────────┘
//
// Ни один объект нельзя создать первым — каждому нужен ещё не созданный.
//
// Wire ловит такой цикл при кодогенерации (до запуска).
// Fx ловит его в рантайме (при старте приложения).
// Результат одинаковый — приложение не запустится.
//
// Запустите: go run ./cmd/fx-di-broken/
// Результат: ошибка "In: cannot depend on the provided type"
import (
"log/slog"
"go.uber.org/fx"
)
// === Интерфейсы ===
type NotificationSender interface {
Send(msg string)
}
type EventPublisher interface {
Publish(event string)
}
type EventSubscriber interface {
Subscribe(event string, handler func())
}
type Auth interface {
Validate(token string) bool
}
type Users interface {
GetEmail(userID int) string
}
// === Реализации ===
type EventBus struct {
notifications NotificationSender // ← вот она, циклическая зависимость
}
func NewEventBus(notifications NotificationSender) *EventBus {
slog.Info("шина событий создана")
return &EventBus{notifications: notifications}
}
func (b *EventBus) Publish(event string) {}
func (b *EventBus) Subscribe(event string, handler func()) {}
type AuthService struct {
events EventPublisher
}
func NewAuthService(events EventPublisher) *AuthService {
slog.Info("сервис авторизации создан")
return &AuthService{events: events}
}
func (s *AuthService) Validate(token string) bool { return true }
type UserService struct {
auth Auth
}
func NewUserService(auth Auth) *UserService {
slog.Info("сервис пользователей создан")
return &UserService{auth: auth}
}
func (s *UserService) GetEmail(userID int) string { return "user@example.com" }
type NotificationService struct {
users Users
events EventSubscriber
}
func NewNotificationService(users Users, events EventSubscriber) *NotificationService {
slog.Info("сервис уведомлений создан")
return &NotificationService{users: users, events: events}
}
func (s *NotificationService) Send(msg string) {
slog.Info("уведомление отправлено", "msg", msg)
}
// === Fx ===
func main() {
fx.New(
fx.Provide(
fx.Annotate(NewEventBus, fx.As(new(EventPublisher)), fx.As(new(EventSubscriber))),
fx.Annotate(NewAuthService, fx.As(new(Auth))),
fx.Annotate(NewUserService, fx.As(new(Users))),
fx.Annotate(NewNotificationService, fx.As(new(NotificationSender))),
),
fx.Invoke(func(pub EventPublisher) {
slog.Info("всё запустилось") // сюда не дойдём
}),
).Run()
}
+65
View File
@@ -0,0 +1,65 @@
package main
// API-слой — HTTP-хендлер и сервер.
// Собирает сервисы из всех доменов в единый HTTP-интерфейс.
import (
"context"
"fmt"
"log/slog"
"net"
"net/http"
"go.uber.org/fx"
)
var APIModule = fx.Module("api",
fx.Provide(newHandler),
fx.Invoke(startHTTPServer),
)
// --- Handler ---
type handler struct {
userService UserService
authService AuthService
notificationService NotificationService
}
func newHandler(userService UserService, authService AuthService, notificationService NotificationService) *handler {
return &handler{userService: userService, authService: authService, notificationService: notificationService}
}
func (h *handler) routes() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/health", func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, "ok")
})
return mux
}
// --- HTTP Server ---
func startHTTPServer(lc fx.Lifecycle, h *handler) {
srv := &http.Server{
Addr: ":8080",
Handler: h.routes(),
}
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
ln, err := net.Listen("tcp", srv.Addr)
if err != nil {
return err
}
slog.Info("HTTP-сервер запущен", "addr", srv.Addr)
go srv.Serve(ln)
return nil
},
OnStop: func(ctx context.Context) error {
slog.Info("HTTP-сервер останавливается...")
return srv.Shutdown(ctx)
},
})
}
+46
View File
@@ -0,0 +1,46 @@
package main
// Домен авторизации.
// Этим файлом владеет команда авторизации.
//
// Что внутри: сессии, проверка токенов, логин/логаут.
// Что НЕ знает: как устроены уведомления, как устроены профили.
//
// Добавить TokenValidator? Правим ТОЛЬКО этот файл:
// 1. Добавляем тип + конструктор
// 2. Добавляем fx.Provide(newTokenValidator) в AuthModule
// 3. main.go не трогаем — ноль мерж-конфликтов
import "go.uber.org/fx"
var AuthModule = fx.Module("auth",
fx.Provide(
newSessionRepo,
newAuthService,
// newTokenValidator, ← команда добавит сюда, не в main.go
),
)
// --- SessionRepo ---
type sessionRepo struct {
db DB
cache Cache
}
func newSessionRepo(db DB, cache Cache) SessionRepository {
return &sessionRepo{db: db, cache: cache}
}
// --- AuthService ---
type authServiceImpl struct {
userRepo UserRepository
sessionRepo SessionRepository
cache Cache
events EventBus
}
func newAuthService(userRepo UserRepository, sessionRepo SessionRepository, cache Cache, events EventBus) AuthService {
return &authServiceImpl{userRepo: userRepo, sessionRepo: sessionRepo, cache: cache, events: events}
}
+91
View File
@@ -0,0 +1,91 @@
package main
// Общая инфраструктура: БД, кэш, шина событий.
// Этим владеет платформенная команда (или DevOps).
// Бизнес-команды используют, но не трогают.
import (
"context"
"log/slog"
"go.uber.org/fx"
)
var InfraModule = fx.Module("infra",
fx.Provide(newDB, newCache, newEventBus),
)
// --- DB ---
type dbImpl struct{ dsn string }
func (db *dbImpl) Query(query string) error { return nil }
func (db *dbImpl) QueryRow(query string) error { return nil }
func (db *dbImpl) Exec(query string) error { return nil }
func (db *dbImpl) BeginTx() error { return nil }
func (db *dbImpl) BulkInsert(query string, args ...any) error { return nil }
func (db *dbImpl) Close() error {
slog.Info("БД: соединение закрыто")
return nil
}
func newDB(lc fx.Lifecycle) (DB, error) {
db := &dbImpl{dsn: "postgres://localhost:5432/demo"}
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
slog.Info("БД: подключение установлено", "dsn", db.dsn)
return nil
},
OnStop: func(ctx context.Context) error {
return db.Close()
},
})
return db, nil
}
// --- Cache ---
type cacheImpl struct{}
func (c *cacheImpl) Get(key string) (string, error) { return "", nil }
func (c *cacheImpl) Set(key, value string) error { return nil }
func (c *cacheImpl) Close() error {
slog.Info("Кэш: соединение закрыто")
return nil
}
func newCache(lc fx.Lifecycle) Cache {
c := &cacheImpl{}
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
slog.Info("Кэш: подключение установлено")
return nil
},
OnStop: func(ctx context.Context) error {
return c.Close()
},
})
return c
}
// --- EventBus ---
type eventBusImpl struct{}
func (b *eventBusImpl) Publish(event string) { slog.Info("событие", "event", event) }
func (b *eventBusImpl) Subscribe(event string, handler func()) { slog.Info("подписка", "event", event) }
func newEventBus(lc fx.Lifecycle) EventBus {
bus := &eventBusImpl{}
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
slog.Info("EventBus: запущен")
return nil
},
OnStop: func(ctx context.Context) error {
slog.Info("EventBus: остановлен")
return nil
},
})
return bus
}
+37
View File
@@ -0,0 +1,37 @@
package main
// Общие интерфейсы — один пакет, все потребители импортируют.
// Как в Java/Spring: один UserRepository на весь проект.
// fx.Module работает красиво именно с таким подходом.
// Инфраструктура
type DB interface {
Query(query string) error
QueryRow(query string) error
Exec(query string) error
BeginTx() error
BulkInsert(query string, args ...any) error
}
type Cache interface {
Get(key string) (string, error)
Set(key, value string) error
}
type EventBus interface {
Publish(event string)
Subscribe(event string, handler func())
}
// Репозитории
type UserRepository interface{}
type SessionRepository interface{}
type NotificationRepository interface{}
// Сервисы
type AuthService interface{}
type UserService interface{}
type NotificationService interface{}
+37
View File
@@ -0,0 +1,37 @@
package main
// fx.Module — модульный монолит с Fx.
//
// Каждый бизнес-домен живёт в своём файле:
// infra.go — БД, кэш, шина событий (общая инфраструктура)
// auth_module.go — авторизация: сессии + проверка токенов
// user_module.go — профили пользователей
// notification_module.go — уведомления
// api.go — HTTP-хендлер и сервер
//
// Зачем это нужно:
// Команда авторизации добавляет TokenValidator → правит ТОЛЬКО auth_module.go.
// main.go не трогает. Конфликтов при мерже с командой уведомлений — ноль.
//
// Без fx.Module все провайдеры в одном main.go. Три команды правят один файл.
// Мерж-конфликты на каждом PR.
//
// Сравните с cmd/fx-di-shared/ — тот же код, но всё в одном файле.
import "go.uber.org/fx"
func main() {
fx.New(
// Общая инфраструктура
InfraModule,
// Бизнес-домены — каждая команда владеет своим файлом.
// Добавить новый домен = новый файл + одна строка здесь.
AuthModule,
UserModule,
NotificationModule,
// API — собирает сервисы в HTTP-хендлер
APIModule,
).Run()
}
+36
View File
@@ -0,0 +1,36 @@
package main
// Домен уведомлений.
// Этим файлом владеет команда уведомлений.
//
// Что внутри: отправка уведомлений, шаблоны, каналы доставки.
// Что НЕ знает: как устроена авторизация, как устроены профили внутри.
import "go.uber.org/fx"
var NotificationModule = fx.Module("notification",
fx.Provide(
newNotificationRepo,
newNotificationService,
),
)
// --- NotificationRepo ---
type notificationRepo struct{ db DB }
func newNotificationRepo(db DB) NotificationRepository {
return &notificationRepo{db: db}
}
// --- NotificationService ---
type notificationServiceImpl struct {
notificationRepo NotificationRepository
userService UserService
events EventBus
}
func newNotificationService(notificationRepo NotificationRepository, userService UserService, events EventBus) NotificationService {
return &notificationServiceImpl{notificationRepo: notificationRepo, userService: userService, events: events}
}
+36
View File
@@ -0,0 +1,36 @@
package main
// Домен профилей пользователей.
// Этим файлом владеет команда профилей.
//
// Что внутри: пользователи, профили, настройки.
// Что НЕ знает: как устроена авторизация внутри, как устроены уведомления.
import "go.uber.org/fx"
var UserModule = fx.Module("user",
fx.Provide(
newUserRepo,
newUserService,
),
)
// --- UserRepo ---
type userRepo struct{ db DB }
func newUserRepo(db DB) UserRepository {
return &userRepo{db: db}
}
// --- UserService ---
type userServiceImpl struct {
userRepo UserRepository
authService AuthService
events EventBus
}
func newUserService(userRepo UserRepository, authService AuthService, events EventBus) UserService {
return &userServiceImpl{userRepo: userRepo, authService: authService, events: events}
}
+41
View File
@@ -0,0 +1,41 @@
package main
// =====================================================
// Общие интерфейсы — один пакет, все потребители импортируют.
// Как в Java/Spring: один UserRepository на весь проект.
//
// Каждый интерфейс определён ОДИН РАЗ и переиспользуется всеми.
// В отличие от Go-идиомы, где каждый потребитель определяет свой.
// =====================================================
// Инфраструктура
type DB interface {
Query(query string) error
QueryRow(query string) error
Exec(query string) error
BeginTx() error
BulkInsert(query string, args ...any) error
}
type Cache interface {
Get(key string) (string, error)
Set(key, value string) error
}
type EventBus interface {
Publish(event string)
Subscribe(event string, handler func())
}
// Репозитории
type UserRepository interface{}
type SessionRepository interface{}
type NotificationRepository interface{}
// Сервисы
type AuthService interface{}
type UserService interface{}
type NotificationService interface{}
+79
View File
@@ -0,0 +1,79 @@
package main
// Fx с ОБЩИМИ интерфейсами — чистый и простой пример.
//
// Сравните:
// cmd/fx-di/main.go — consumer-defined interfaces (Go-идиома) → 330 строк
// cmd/fx-di-shared/ — shared interfaces (Java-подход) → вот этот файл
//
// Разница — только в подходе к определению интерфейсов.
// Когда один тип → один интерфейс, Fx работает без fx.Out/fx.In/named-тегов.
//
// Это НЕ рекомендация так писать на Go. Это демонстрация того,
// под какую архитектуру Fx был спроектирован.
//
// Lifecycle:
// Fx управляет жизненным циклом компонентов через хуки OnStart/OnStop.
// При запуске — вызывает OnStart в порядке зависимостей (сначала DB, потом сервисы).
// При остановке — вызывает OnStop в обратном порядке (сначала сервер, потом DB).
// Отправьте SIGINT (Ctrl+C) чтобы увидеть порядок остановки в логах.
//
// Хотите увидеть как это масштабируется на несколько команд?
// Смотрите cmd/fx-di-modules/ — тот же код, но каждый домен в своём файле.
import (
"context"
"log/slog"
"net"
"net/http"
"go.uber.org/fx"
)
func main() {
fx.New(
fx.Provide(
newDB,
newCache,
newEventBus,
newUserRepo,
newSessionRepo,
newNotificationRepo,
newAuthService,
newUserService,
newNotificationService,
newHandler,
),
fx.Invoke(startHTTPServer),
// Вот и всё. 10 провайдеров, 1 invoke. Fx разрулил сам.
// Потому что один тип → один интерфейс → нет неоднозначности.
//
// А lifecycle хуки (OnStart/OnStop) — внутри провайдеров newDB, newCache
// и startHTTPServer. Fx сам вызовет их в правильном порядке.
).Run()
}
func startHTTPServer(lc fx.Lifecycle, h *handler) {
srv := &http.Server{
Addr: ":8080",
Handler: h.routes(),
}
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
ln, err := net.Listen("tcp", srv.Addr)
if err != nil {
return err
}
slog.Info("HTTP-сервер запущен", "addr", srv.Addr)
go srv.Serve(ln)
return nil
},
OnStop: func(ctx context.Context) error {
slog.Info("HTTP-сервер останавливается...")
return srv.Shutdown(ctx)
},
})
}
+175
View File
@@ -0,0 +1,175 @@
package main
import (
"context"
"fmt"
"log/slog"
"net/http"
"go.uber.org/fx"
)
// =====================================================
// Реализации — конструкторы возвращают интерфейсы.
// Один тип → один интерфейс → Fx разруливает автоматически.
//
// DB и Cache используют fx.Lifecycle для регистрации хуков:
// OnStart — пинг/проверка соединения при запуске
// OnStop — корректное закрытие при остановке
//
// Fx сам вызывает хуки в правильном порядке:
// Старт: DB → Cache → ... → HTTP-сервер
// Остановка: HTTP-сервер → ... → Cache → DB
// =====================================================
// --- Инфраструктура ---
type dbImpl struct{ dsn string }
func (db *dbImpl) Query(query string) error { return nil }
func (db *dbImpl) QueryRow(query string) error { return nil }
func (db *dbImpl) Exec(query string) error { return nil }
func (db *dbImpl) BeginTx() error { return nil }
func (db *dbImpl) BulkInsert(query string, args ...any) error { return nil }
func (db *dbImpl) Close() error {
slog.Info("БД: соединение закрыто")
return nil
}
func newDB(lc fx.Lifecycle) (DB, error) {
db := &dbImpl{dsn: "postgres://localhost:5432/demo"}
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
slog.Info("БД: подключение установлено", "dsn", db.dsn)
return nil // здесь был бы db.Ping(ctx)
},
OnStop: func(ctx context.Context) error {
return db.Close()
},
})
return db, nil
}
type cacheImpl struct{}
func (c *cacheImpl) Get(key string) (string, error) { return "", nil }
func (c *cacheImpl) Set(key, value string) error { return nil }
func (c *cacheImpl) Close() error {
slog.Info("Кэш: соединение закрыто")
return nil
}
func newCache(lc fx.Lifecycle) Cache {
c := &cacheImpl{}
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
slog.Info("Кэш: подключение установлено")
return nil // здесь был бы c.Ping(ctx)
},
OnStop: func(ctx context.Context) error {
return c.Close()
},
})
return c
}
type eventBusImpl struct{}
func (b *eventBusImpl) Publish(event string) { slog.Info("событие", "event", event) }
func (b *eventBusImpl) Subscribe(event string, handler func()) { slog.Info("подписка", "event", event) }
func newEventBus(lc fx.Lifecycle) EventBus {
bus := &eventBusImpl{}
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
slog.Info("EventBus: запущен")
return nil
},
OnStop: func(ctx context.Context) error {
slog.Info("EventBus: очередь событий дренирована")
return nil
},
})
return bus
}
// --- Репозитории ---
type userRepo struct{ db DB }
type sessionRepo struct {
db DB
cache Cache
}
type notificationRepo struct{ db DB }
func newUserRepo(db DB) UserRepository {
return &userRepo{db: db}
}
func newSessionRepo(db DB, cache Cache) SessionRepository {
return &sessionRepo{db: db, cache: cache}
}
func newNotificationRepo(db DB) NotificationRepository {
return &notificationRepo{db: db}
}
// --- Сервисы ---
type authServiceImpl struct {
userRepo UserRepository
sessionRepo SessionRepository
cache Cache
events EventBus
}
func newAuthService(userRepo UserRepository, sessionRepo SessionRepository, cache Cache, events EventBus) AuthService {
return &authServiceImpl{userRepo: userRepo, sessionRepo: sessionRepo, cache: cache, events: events}
}
type userServiceImpl struct {
userRepo UserRepository
authService AuthService
events EventBus
}
func newUserService(userRepo UserRepository, authService AuthService, events EventBus) UserService {
return &userServiceImpl{userRepo: userRepo, authService: authService, events: events}
}
type notificationServiceImpl struct {
notificationRepo NotificationRepository
userService UserService
events EventBus
}
func newNotificationService(notificationRepo NotificationRepository, userService UserService, events EventBus) NotificationService {
return &notificationServiceImpl{notificationRepo: notificationRepo, userService: userService, events: events}
}
// --- Handler ---
type handler struct {
userService UserService
authService AuthService
notificationService NotificationService
}
func newHandler(userService UserService, authService AuthService, notificationService NotificationService) *handler {
return &handler{userService: userService, authService: authService, notificationService: notificationService}
}
func (h *handler) routes() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/health", func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, "ok")
})
return mux
}
+358
View File
@@ -0,0 +1,358 @@
package main
// Это пример инициализации через Uber Fx — runtime DI-контейнер.
// Fx делает всё в рантайме: через рефлексию анализирует сигнатуры конструкторов
// и автоматически собирает граф зависимостей.
//
// Плюсы:
// - Не нужна кодогенерация
// - Lifecycle хуки из коробки
//
// Минусы:
// - Рефлексия: ошибки только в рантайме, а не при компиляции
// - fx.Annotate + fx.As для каждого интерфейса — бойлерплейта не меньше, чем wire.Bind
// - Один конкретный тип → несколько интерфейсов? Нужны обёртки-адаптеры (fx.Out)
// - Свой логгер (zap), свой lifecycle — навязывает архитектуру
// - Магия: непонятно что происходит, пока не запустишь
//
// =====================================================
// ПОЧЕМУ ЗДЕСЬ СТОЛЬКО БОЙЛЕРПЛЕЙТА?
// =====================================================
//
// В этом проекте мы определяем интерфейсы по месту использования —
// Go-идиоматичный подход (Interface Segregation Principle):
//
// // service/auth.go — свой интерфейс
// type AuthUserRepository interface { ... }
//
// // service/user.go — свой интерфейс
// type UserRepository interface { ... }
//
// Один repository.UserRepo реализует оба. Fx не умеет это разрулить
// без fx.Out/fx.In структур и named-тегов — отсюда 300+ строк.
//
// Если у вас другой подход — общие интерфейсы в одном пакете:
//
// // shared/interfaces.go
// type UserRepository interface { ... } // один на всех потребителей
//
// То Fx ложится ГОРАЗДО проще: один тип → один интерфейс → нет проблемы.
// Просто fx.Provide(repository.NewUserRepo) — и готово.
//
// Это не плохой подход, просто другой. Java/Spring так и работают.
// Fx по сути портированный Dagger/Spring — он заточен под shared interfaces.
// Если у вас на проекте именно такая архитектура — Fx будет комфортным.
// =====================================================
import (
"log/slog"
"net/http"
"go.uber.org/fx"
"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"
)
func main() {
fx.New(
// Конфиг
fx.Provide(config.AppConfig),
// === Инфраструктура ===
// Здесь начинается самое «весёлое».
//
// database.DB реализует 3 интерфейса:
// - repository.UserDB
// - repository.SessionDB
// - repository.NotificationDB
//
// cache.Cache реализует 2 интерфейса:
// - repository.SessionCache
// - service.AuthCache
//
// events.EventBus реализует 3 интерфейса:
// - service.AuthEventPublisher
// - service.UserEventPublisher
// - service.NotificationEventSubscriber
//
// В Fx нельзя просто написать fx.As(new(A), new(B), new(C)).
// Если один провайдер возвращает конкретный тип, а его нужно
// подать как РАЗНЫЕ интерфейсы в разные потребители — нужно
// писать специальные структуры-адаптеры с fx.Out.
//
// Либо — провайдить конкретный тип + отдельные provide-обёртки
// для каждого интерфейса. Оба варианта — бойлерплейт.
//
// Мы используем fx.Out структуры — каноничный подход Fx.
fx.Provide(provideDB), // → dbOut{UserDB, SessionDB, NotificationDB}
fx.Provide(provideCache), // → cacheOut{SessionCache, AuthCache}
fx.Provide(provideEvents), // → eventsOut{AuthPub, UserPub, NotifSub}
// === Репозитории ===
// Та же история: repository.UserRepo реализует 2 интерфейса (service.AuthUserRepository
// и service.UserRepository), поэтому нужна fx.Out обёртка.
fx.Provide(provideUserRepo), // → userRepoOut{AuthUserRepo, UserRepo}
fx.Provide(provideSessionRepo), // → fx.As(service.AuthSessionRepository)
fx.Provide(provideNotificationRepo), // → fx.As(service.NotificationRepository)
// === Сервисы ===
// И здесь то же самое.
fx.Provide(provideAuthService), // → authServiceOut{UserAuth, APIAuth}
fx.Provide(provideUserService), // → userServiceOut{NotifUser, APIUser}
fx.Provide(provideNotificationService), // → api.NotificationService
// Handler — тоже нужна обёртка, потому что зависимости named.
fx.Provide(provideHandler),
// HTTP-сервер — через fx.Invoke.
fx.Invoke(startHTTPServer),
// Итого: чтобы «автоматически» собрать граф из 12 зависимостей,
// мы написали 7 fx.Out структур + 7 fx.In структур + 11 provide-обёрток.
// Файл ~300 строк для того же результата что 40 строк в DI-контейнере.
//
// А ТЕПЕРЬ ГЛАВНОЕ — как Fx работает внутри?
//
// При вызове fx.New() он:
// 1. Собирает все провайдеры
// 2. Через рефлексию анализирует сигнатуры (кто что принимает, кто что возвращает)
// 3. Строит граф зависимостей
// 4. Делает топологическую сортировку
// 5. Вызывает ВСЕ конструкторы разом, в правильном порядке
//
// То есть инициализация — EAGER, не ленивая.
// Всё создаётся при старте, как в антипаттерне и в Wire.
// Просто порядок вычисляется автоматически через рефлексию.
//
// Что это значит на практике:
// - Ошибки — только в рантайме (забыл провайдер → паника при старте)
// - Циклическая зависимость → Fx упадёт при старте (тот же тупик)
// - Ленивой инициализации нет — всё создаётся сразу
//
// Fx решает ту же задачу что Wire — «не вычисляй порядок руками».
// Но наш DI-контейнер не имеет порядка вообще — каждый геттер
// рекурсивно подтягивает зависимости при первом вызове.
// Нечему ломаться, нечего сортировать.
).Run()
}
// =====================================================
// Обёртки-адаптеры для Fx (fx.Out)
// =====================================================
// Каждая структура с fx.Out нужна потому что один конкретный тип
// реализует несколько интерфейсов для разных потребителей.
// Без этих структур Fx не знает как разрулить зависимости.
// --- database.DB → 3 интерфейса ---
type dbOut struct {
fx.Out
UserDB repository.UserDB `name:"userDB"`
SessionDB repository.SessionDB `name:"sessionDB"`
NotificationDB repository.NotificationDB `name:"notificationDB"`
}
func provideDB(cfg *config.Config) (dbOut, error) {
db, err := database.New(cfg.DSN)
if err != nil {
return dbOut{}, err
}
return dbOut{
UserDB: db,
SessionDB: db,
NotificationDB: db,
}, nil
}
// --- cache.Cache → 2 интерфейса ---
type cacheOut struct {
fx.Out
SessionCache repository.SessionCache `name:"sessionCache"`
AuthCache service.AuthCache `name:"authCache"`
}
func provideCache(cfg *config.Config) cacheOut {
c := cache.New(cfg.RedisAddr)
return cacheOut{
SessionCache: c,
AuthCache: c,
}
}
// --- events.EventBus → 3 интерфейса ---
type eventsOut struct {
fx.Out
AuthPublisher service.AuthEventPublisher `name:"authPublisher"`
UserPublisher service.UserEventPublisher `name:"userPublisher"`
NotifSub service.NotificationEventSubscriber `name:"notifSubscriber"`
}
func provideEvents() eventsOut {
bus := events.NewEventBus()
return eventsOut{
AuthPublisher: bus,
UserPublisher: bus,
NotifSub: bus,
}
}
// --- repository.UserRepo → 2 интерфейса ---
type userRepoOut struct {
fx.Out
AuthUserRepo service.AuthUserRepository `name:"authUserRepo"`
UserRepo service.UserRepository `name:"userRepo"`
}
type userRepoIn struct {
fx.In
DB repository.UserDB `name:"userDB"`
}
func provideUserRepo(in userRepoIn) userRepoOut {
repo := repository.NewUserRepo(in.DB)
return userRepoOut{
AuthUserRepo: repo,
UserRepo: repo,
}
}
// --- repository.SessionRepo → 1 интерфейс ---
type sessionRepoIn struct {
fx.In
DB repository.SessionDB `name:"sessionDB"`
Cache repository.SessionCache `name:"sessionCache"`
}
func provideSessionRepo(in sessionRepoIn) service.AuthSessionRepository {
return repository.NewSessionRepo(in.DB, in.Cache)
}
// --- repository.NotificationRepo → 1 интерфейс ---
type notificationRepoIn struct {
fx.In
DB repository.NotificationDB `name:"notificationDB"`
}
type notificationRepoOut struct {
fx.Out
Repo service.NotificationRepository `name:"notifRepo"`
}
func provideNotificationRepo(in notificationRepoIn) notificationRepoOut {
return notificationRepoOut{
Repo: repository.NewNotificationRepo(in.DB),
}
}
// --- service.AuthService → 2 интерфейса ---
type authServiceOut struct {
fx.Out
UserAuthService service.UserAuthService `name:"userAuthService"`
APIAuthService api.AuthService `name:"apiAuthService"`
}
type authServiceIn struct {
fx.In
UserRepo service.AuthUserRepository `name:"authUserRepo"`
SessionRepo service.AuthSessionRepository
Cache service.AuthCache `name:"authCache"`
Events service.AuthEventPublisher `name:"authPublisher"`
}
func provideAuthService(in authServiceIn) authServiceOut {
svc := service.NewAuthService(in.UserRepo, in.SessionRepo, in.Cache, in.Events)
return authServiceOut{
UserAuthService: svc,
APIAuthService: svc,
}
}
// --- service.UserService → 2 интерфейса ---
type userServiceOut struct {
fx.Out
NotifUserService service.NotificationUserService `name:"notifUserService"`
APIUserService api.UserService `name:"apiUserService"`
}
type userServiceIn struct {
fx.In
UserRepo service.UserRepository `name:"userRepo"`
AuthService service.UserAuthService `name:"userAuthService"`
Events service.UserEventPublisher `name:"userPublisher"`
}
func provideUserService(in userServiceIn) userServiceOut {
svc := service.NewUserService(in.UserRepo, in.AuthService, in.Events)
return userServiceOut{
NotifUserService: svc,
APIUserService: svc,
}
}
// --- service.NotificationService (fx.Annotate выше, но нужен fx.In для named deps) ---
type notificationServiceIn struct {
fx.In
Repo service.NotificationRepository `name:"notifRepo"`
UserSvc service.NotificationUserService `name:"notifUserService"`
Events service.NotificationEventSubscriber `name:"notifSubscriber"`
}
// Заменяем fx.Annotate на отдельный provide — иначе не прокинуть named зависимости.
func provideNotificationService(in notificationServiceIn) api.NotificationService {
return service.NewNotificationService(in.Repo, in.UserSvc, in.Events)
}
// --- api.Handler (fx.In для named deps) ---
type handlerIn struct {
fx.In
UserService api.UserService `name:"apiUserService"`
AuthService api.AuthService `name:"apiAuthService"`
NotificationService api.NotificationService
}
func provideHandler(in handlerIn) api.Handler {
return api.NewHandler(in.UserService, in.AuthService, in.NotificationService)
}
func startHTTPServer(handler api.Handler) {
srv := &http.Server{
Addr: config.AppConfig().HTTPAddr,
Handler: handler.Routes(),
}
slog.Info("сервер запущен", "addr", config.AppConfig().HTTPAddr)
if err := srv.ListenAndServe(); err != nil {
slog.Error("ошибка сервера", "err", err)
}
}
+17
View File
@@ -0,0 +1,17 @@
package main
import (
"log/slog"
"os"
"github.com/olezhek28/di-demo/internal/app"
)
func main() {
a := app.New()
if err := a.Run(); err != nil {
slog.Error("ошибка приложения", "err", err)
os.Exit(1)
}
}
+5
View File
@@ -0,0 +1,5 @@
package main
func main() {
_, _ = InitializeEventBus()
}
+98
View File
@@ -0,0 +1,98 @@
package main
// Это упрощённый граф зависимостей с циклом.
// Та самая ситуация из антипаттерна: продакт попросил чтобы EventBus
// сам доставлял уведомления через NotificationService.
//
// Цепочка:
// EventBus → NotificationService → UserService → AuthService → EventBus
// ↑ ЦИКЛ
//
// Wire не сможет это сгенерировать. Запустите:
// task generate-wire-broken
// и увидите ошибку.
import "log/slog"
// === Интерфейсы (consumer-defined) ===
// NotificationSender — то что EventBus хочет от NotificationService.
type NotificationSender interface {
Send(msg string)
}
// EventPublisher — то что AuthService хочет от EventBus.
type EventPublisher interface {
Publish(event string)
}
// EventSubscriber — то что NotificationService хочет от EventBus.
type EventSubscriber interface {
Subscribe(event string, handler func())
}
// Auth — то что UserService хочет от AuthService.
type Auth interface {
Validate(token string) bool
}
// Users — то что NotificationService хочет от UserService.
type Users interface {
GetEmail(userID int) string
}
// === Реализации ===
// EventBus — шина событий, которая САМА доставляет уведомления.
// Вот эта зависимость на NotificationSender создаёт цикл.
type EventBus struct {
notifications NotificationSender
}
func NewEventBus(notifications NotificationSender) *EventBus {
slog.Info("шина событий создана")
return &EventBus{notifications: notifications}
}
func (b *EventBus) Publish(event string) {}
func (b *EventBus) Subscribe(event string, handler func()) {}
// AuthService — сервис авторизации. Зависит от EventBus для публикации событий.
type AuthService struct {
events EventPublisher
}
func NewAuthService(events EventPublisher) *AuthService {
slog.Info("сервис авторизации создан")
return &AuthService{events: events}
}
func (s *AuthService) Validate(token string) bool { return true }
// UserService — сервис пользователей. Зависит от AuthService.
type UserService struct {
auth Auth
}
func NewUserService(auth Auth) *UserService {
slog.Info("сервис пользователей создан")
return &UserService{auth: auth}
}
func (s *UserService) GetEmail(userID int) string { return "user@example.com" }
// NotificationService — сервис уведомлений.
// Зависит от UserService и EventBus (для подписки на события).
type NotificationService struct {
users Users
events EventSubscriber
}
func NewNotificationService(users Users, events EventSubscriber) *NotificationService {
slog.Info("сервис уведомлений создан")
return &NotificationService{users: users, events: events}
}
func (s *NotificationService) Send(msg string) {
slog.Info("уведомление отправлено", "msg", msg)
}
+32
View File
@@ -0,0 +1,32 @@
//go:build wireinject
package main
import "github.com/google/wire"
// InitializeEventBus — попытка собрать граф с циклической зависимостью.
//
// EventBus → NotificationService → UserService → AuthService → EventBus
//
// Wire сделает топологическую сортировку, обнаружит цикл и упадёт.
// Ленивой инициализации нет — Wire должен знать порядок создания заранее.
// А при цикле порядка не существует.
//
// Запустите: task generate-wire-broken
// Результат: "cycle for *EventBus"
func InitializeEventBus() (*EventBus, error) {
wire.Build(
NewEventBus,
NewAuthService,
NewUserService,
NewNotificationService,
wire.Bind(new(NotificationSender), new(*NotificationService)),
wire.Bind(new(EventPublisher), new(*EventBus)),
wire.Bind(new(EventSubscriber), new(*EventBus)),
wire.Bind(new(Auth), new(*AuthService)),
wire.Bind(new(Users), new(*UserService)),
)
return nil, nil
}
+28
View File
@@ -0,0 +1,28 @@
package main
import (
"log/slog"
"net/http"
"os"
"github.com/olezhek28/di-demo/internal/config"
)
func main() {
handler, err := InitializeHandler()
if err != nil {
slog.Error("не удалось инициализировать приложение", "err", err)
os.Exit(1)
}
srv := &http.Server{
Addr: config.AppConfig().HTTPAddr,
Handler: handler.Routes(),
}
slog.Info("сервер запущен", "addr", config.AppConfig().HTTPAddr)
if err = srv.ListenAndServe(); err != nil {
slog.Error("ошибка сервера", "err", err)
}
}
+115
View File
@@ -0,0 +1,115 @@
//go:build wireinject
package main
// Это файл, который пишет разработчик. Wire анализирует его и генерирует wire_gen.go.
// Build-тег wireinject означает что этот файл НЕ попадает в итоговую сборку —
// вместо него компилируется wire_gen.go.
import (
"github.com/google/wire"
"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"
)
// InitializeHandler — инжектор. Wire смотрит на типы и строит граф зависимостей.
// Разработчик перечисляет провайдеры, Wire разбирается кому что нужно.
func InitializeHandler() (api.Handler, error) {
wire.Build(
// Конфиг
config.AppConfig,
// Инфраструктура — провайдеры-обёртки, потому что конструкторы
// принимают строки (DSN, addr), а Wire различает по типу.
provideDB,
provideCache,
events.NewEventBus,
// Репозитории
repository.NewUserRepo,
repository.NewSessionRepo,
repository.NewNotificationRepo,
// Сервисы
service.NewAuthService,
service.NewUserService,
service.NewNotificationService,
// API
api.NewHandler,
// А теперь самое «весёлое» — wire.Bind.
// Каждый конструктор принимает интерфейс, а Wire работает с типами.
// Нужно ВРУЧНУЮ указать: какой тип реализует какой интерфейс.
//
// database.DB реализует 3 интерфейса в разных пакетах:
wire.Bind(new(repository.UserDB), new(database.DB)),
wire.Bind(new(repository.SessionDB), new(database.DB)),
wire.Bind(new(repository.NotificationDB), new(database.DB)),
// cache.Cache реализует 2 интерфейса:
wire.Bind(new(repository.SessionCache), new(cache.Cache)),
wire.Bind(new(service.AuthCache), new(cache.Cache)),
// events.EventBus реализует 3 интерфейса:
wire.Bind(new(service.AuthEventPublisher), new(events.EventBus)),
wire.Bind(new(service.UserEventPublisher), new(events.EventBus)),
wire.Bind(new(service.NotificationEventSubscriber), new(events.EventBus)),
// repository.UserRepo реализует 2 интерфейса:
wire.Bind(new(service.AuthUserRepository), new(repository.UserRepo)),
wire.Bind(new(service.UserRepository), new(repository.UserRepo)),
// Остальные репозитории — по одному интерфейсу:
wire.Bind(new(service.AuthSessionRepository), new(repository.SessionRepo)),
wire.Bind(new(service.NotificationRepository), new(repository.NotificationRepo)),
// Сервисы тоже реализуют интерфейсы выше по графу:
wire.Bind(new(service.UserAuthService), new(service.AuthService)),
wire.Bind(new(service.NotificationUserService), new(service.UserService)),
wire.Bind(new(api.UserService), new(service.UserService)),
wire.Bind(new(api.AuthService), new(service.AuthService)),
wire.Bind(new(api.NotificationService), new(service.NotificationService)),
)
// 10 провайдеров + 18 wire.Bind = 28 строк конфигурации.
// Для 12 зависимостей. И при каждом новом интерфейсе — новый Bind.
//
// А ТЕПЕРЬ ГЛАВНОЕ.
// Откройте wire_gen.go и посмотрите что Wire нагенерировал.
// Это та же самая каша что в cmd/antipattern/main.go:
//
// db, err := provideDB(configConfig)
// userRepo := repository.NewUserRepo(db)
// sessionRepo := repository.NewSessionRepo(db, cache)
// authService := service.NewAuthService(userRepo, sessionRepo, cache, eventBus)
// ...
//
// Последовательная инициализация. Жёсткий порядок. Eager — всё создаётся сразу.
//
// Wire решил ровно одну задачу: тебе не надо самому вычислять порядок строк.
// Он сделал топологическую сортировку за тебя. Всё.
//
// Но хрупкость никуда не делась:
// - Добавил зависимость → правь wire.go + wire.Bind + go generate
// - Циклическая зависимость → Wire упадёт при генерации (тот же тупик)
// - Ленивой инициализации нет — всё создаётся при старте
//
// По сути Wire автоматизирует неправильное решение.
// Вместо того чтобы убрать хрупкость — он генерирует хрупкий код за тебя.
return nil, nil
}
func provideDB(cfg *config.Config) (database.DB, error) {
return database.New(cfg.DSN)
}
func provideCache(cfg *config.Config) cache.Cache {
return cache.New(cfg.RedisAddr)
}
+49
View File
@@ -0,0 +1,49 @@
// Code generated by Wire. DO NOT EDIT.
//go:generate go run -mod=mod github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject
package main
import (
"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"
)
// Injectors from wire.go:
// InitializeHandler — инжектор. Wire смотрит на типы и строит граф зависимостей.
// Разработчик перечисляет провайдеры, Wire разбирается кому что нужно.
func InitializeHandler() (api.Handler, error) {
configConfig := config.AppConfig()
db, err := provideDB(configConfig)
if err != nil {
return nil, err
}
userRepo := repository.NewUserRepo(db)
cacheCache := provideCache(configConfig)
sessionRepo := repository.NewSessionRepo(db, cacheCache)
eventBus := events.NewEventBus()
authService := service.NewAuthService(userRepo, sessionRepo, cacheCache, eventBus)
userService := service.NewUserService(userRepo, authService, eventBus)
notificationRepo := repository.NewNotificationRepo(db)
notificationService := service.NewNotificationService(notificationRepo, userService, eventBus)
handler := api.NewHandler(userService, authService, notificationService)
return handler, nil
}
// wire.go:
func provideDB(cfg *config.Config) (database.DB, error) {
return database.New(cfg.DSN)
}
func provideCache(cfg *config.Config) cache.Cache {
return cache.New(cfg.RedisAddr)
}