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) } }