This commit is contained in:
2026-04-13 08:14:09 +03:00
commit 0449337ae7
39 changed files with 2726 additions and 0 deletions
+33
View File
@@ -0,0 +1,33 @@
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Code coverage profiles and other test artifacts
*.out
coverage.*
*.coverprofile
profile.cov
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
go.work.sum
# env file
.env
# Editor/IDE
.idea/
.vscode/
bin/
+198
View File
@@ -0,0 +1,198 @@
## 👋 Привет! Я Олег Козырев
Staff Golang Engineer · Ex Ozon, Avito, Tinkoff
📺 [YouTube](https://www.youtube.com/@olezhek28go) · 💬 [Telegram](https://t.me/olezhek28go)
---
# Dependency Injection в Go: от антипаттернов к элегантным решениям
Репозиторий с примерами различных подходов к внедрению зависимостей (DI) в Go-приложениях.
Все примеры используют **одну и ту же бизнес-логику** из `internal/` — меняется только способ сборки зависимостей.
## Структура проекта
```
internal/
├── config/ — конфигурация приложения (DSN, Redis, HTTP-адрес)
├── database/ — абстракция БД (интерфейс DB + реализация)
├── cache/ — абстракция кэша (интерфейс Cache + Redis-заглушка)
├── events/ — шина событий (Publish/Subscribe)
├── repository/ — репозитории (user, session, notification)
├── service/ — сервисы (auth, user, notification)
├── api/ — HTTP-хендлер и роутинг
└── app/ — сборка приложения + кастомный DI-контейнер
cmd/
├── antipattern/ — ручная сборка (проблема)
├── antipattern-broken/ — ручная сборка (сломанная)
├── wire-di/ — Google Wire
├── wire-di-broken/ — Wire + циклические зависимости
├── fx-di/ — Uber Fx (свои интерфейсы у каждого потребителя)
├── fx-di-broken/ — Fx + циклические зависимости
├── fx-di-shared/ — Uber Fx (общие интерфейсы)
├── fx-di-modules/ — Uber Fx (модульная организация)
└── manual-di/ — кастомный lazy-контейнер (решение)
```
## Ключевой паттерн: интерфейс определяет потребитель
Каждый сервис и репозиторий определяет **свой собственный интерфейс** для каждой зависимости — только с теми методами, которые ему нужны. Это Go-идиома и принцип разделения интерфейсов (ISP).
Например, `UserRepo` определяет интерфейс `UserDB` с нужными ему методами, а `SessionRepo` — свой `SessionDB`. Оба реализуются одним и тем же `*database.db`.
## Примеры
### 1. `cmd/antipattern/` — Ручная сборка зависимостей
Все зависимости создаются вручную в `main()` в строгом порядке. Работает, но:
- порядок инициализации жёстко зафиксирован
- при добавлении зависимости легко ошибиться
- код main() растёт линейно с количеством компонентов
```bash
go run ./cmd/antipattern/
```
### 2. `cmd/antipattern-broken/` — Сломанная ручная сборка
Тот же код, но строки переставлены местами. **Компилируется без ошибок**, но падает в рантайме с nil pointer dereference — кэш и сервисы используются до инициализации.
Показывает, почему ручная сборка хрупкая: компилятор не ловит ошибки порядка инициализации.
```bash
go run ./cmd/antipattern-broken/
# curl http://localhost:8080/users/me → panic: nil pointer
# curl http://localhost:8080/health → 200 OK (зависимости не задействованы)
```
### 3. `cmd/wire-di/` — Google Wire (кодогенерация)
Разработчик описывает провайдеры и привязки в `wire.go`, Wire генерирует корректный код инициализации в `wire_gen.go`.
Решает проблему порядка, но:
- 18 вызовов `wire.Bind()` для маппинга типов → интерфейсы
- eager-инициализация (всё создаётся сразу при старте)
- когда каждый потребитель определяет свой интерфейс, количество Bind растёт быстро
```bash
task generate-wire # генерация кода
go run ./cmd/wire-di/
```
### 4. `cmd/wire-di-broken/` — Wire и циклические зависимости
Демонстрирует цикл: `EventBus → NotificationService → UserService → AuthService → EventBus`.
Wire обнаруживает цикл **на этапе генерации** и отказывается генерировать код.
```bash
task generate-wire-broken # ошибка: cycle for *EventBus
```
### 5. `cmd/fx-di/` — Uber Fx (свои интерфейсы у каждого потребителя)
Рантайм DI-контейнер на рефлексии. Работает, но когда каждый потребитель определяет свой интерфейс — требует много бойлерплейта:
- `fx.Out`/`fx.In` структуры для каждого провайдера
- именованные зависимости через struct-теги
- ~360 строк кода
Показывает, что Fx заточен под «один тип — один интерфейс» (как в Java/Spring), и при Go-подходе к интерфейсам становится громоздким.
```bash
go run ./cmd/fx-di/
```
### 6. `cmd/fx-di-broken/` — Fx и циклические зависимости
Тот же цикл, что и в `wire-di-broken`, но обнаруживается **в рантайме** при старте приложения.
```bash
go run ./cmd/fx-di-broken/ # ошибка: cannot depend on the provided type
```
### 7. `cmd/fx-di-shared/` — Uber Fx (общие интерфейсы)
Fx с «Java-style» архитектурой: один общий интерфейс на тип. Код значительно проще (~56 строк), lifecycle-хуки для graceful старта и остановки.
Показывает, что Fx отлично работает, когда интерфейсы общие, а не определяются каждым потребителем отдельно.
```bash
go run ./cmd/fx-di-shared/
```
### 8. `cmd/fx-di-modules/` — Uber Fx (модульная организация)
Продвинутый пример с `fx.Module`: каждый домен (auth, user, notification) — отдельный модуль в отдельном файле.
Подход для больших команд: каждая команда редактирует только свой модуль, конфликтов в `main.go` нет.
```bash
go run ./cmd/fx-di-modules/
```
### 9. `cmd/manual-di/` — Кастомный lazy-контейнер (решение)
Финальное решение: кастомный DI-контейнер из `internal/app/di.go` с ленивой инициализацией.
- ~17 строк в main, ~50 строк в контейнере
- зависимости создаются при первом обращении (lazy)
- рекурсивное разрешение зависимостей — порядок не важен
- никакой магии, рефлексии и кодогенерации
- ошибки ловятся на этапе компиляции
```bash
go run ./cmd/manual-di/
```
## Сравнение подходов
| Пример | Подход | Обнаружение ошибок | Бойлерплейт | Циклы |
|--------|--------|--------------------|-------------|-------|
| `antipattern` | Ручная сборка | Рантайм (nil pointer) | Нет | Не обнаруживает |
| `wire-di` | Кодогенерация | Генерация кода | 18 Bind() | Ловит при генерации |
| `fx-di` | Рефлексия (runtime) | Рантайм (старт) | fx.In/Out структуры | Ловит при старте |
| `fx-di-shared` | Рефлексия (runtime) | Рантайм (старт) | Минимальный | Ловит при старте |
| `fx-di-modules` | Рефлексия (runtime) | Рантайм (старт) | Минимальный | Ловит при старте |
| `manual-di` | Lazy-контейнер | Компиляция | Нет | Решает через lazy |
## Требования
- Go 1.26+
- [Task](https://taskfile.dev/) — таск-раннер (замена Make)
### Установка Task
```bash
# macOS
brew install go-task
# Linux (snap)
sudo snap install task --classic
# Или через go install
go install github.com/go-task/task/v3/cmd/task@latest
```
## Запуск
```bash
# Установка зависимостей
go mod download
# Установка Wire (для примеров с кодогенерацией)
task install-wire
# Запуск любого примера
go run ./cmd/<имя-примера>/
```
### Доступные task-команды
```bash
task install-wire # установить Wire локально в bin/
task generate-wire # сгенерировать wire_gen.go для cmd/wire-di/
task generate-wire-broken # попытка генерации с циклом (упадёт с ошибкой)
```
+23
View File
@@ -0,0 +1,23 @@
version: '3'
vars:
LOCAL_BIN: '{{.ROOT_DIR}}/bin'
WIRE_VERSION: v0.7.0
tasks:
install-wire:
desc: Установить Wire локально в bin/
cmds:
- GOBIN={{.LOCAL_BIN}} go install github.com/google/wire/cmd/wire@{{.WIRE_VERSION}}
generate-wire:
desc: Сгенерировать wire_gen.go
deps: [install-wire]
cmds:
- '{{.LOCAL_BIN}}/wire ./cmd/wire-di/'
generate-wire-broken:
desc: Попытка сгенерировать Wire с циклической зависимостью (упадёт)
deps: [install-wire]
cmds:
- '{{.LOCAL_BIN}}/wire ./cmd/wire-di-broken/'
+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)
}
+15
View File
@@ -0,0 +1,15 @@
module github.com/olezhek28/di-demo
go 1.26.0
require (
github.com/google/wire v0.7.0
go.uber.org/fx v1.24.0
)
require (
go.uber.org/dig v1.19.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.1 // indirect
golang.org/x/sys v0.42.0 // indirect
)
+29
View File
@@ -0,0 +1,29 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4=
github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4=
go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE=
go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg=
go.uber.org/fx v1.24.0/go.mod h1:AmDeGyS+ZARGKM4tlH4FY2Jr63VjbEDJHtqXTGP5hbo=
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+83
View File
@@ -0,0 +1,83 @@
package api
import (
"fmt"
"log/slog"
"net/http"
)
// UserService — интерфейс сервиса пользователей для хендлера.
type UserService interface {
GetProfile(token string) string
}
// AuthService — интерфейс сервиса авторизации для хендлера.
type AuthService interface {
// методы, которые хендлер использует из сервиса авторизации
}
// NotificationService — интерфейс сервиса уведомлений для хендлера.
type NotificationService interface {
// методы, которые хендлер использует из сервиса уведомлений
}
// Handler — интерфейс HTTP-обработчика.
// Содержит только хендлеры и роутинг.
// Ничего не знает про http.Server, порт или lifecycle —
// это ответственность app-слоя.
// Структура неэкспортируемая — создать объект можно только через NewHandler.
type Handler interface {
Routes() http.Handler
}
// handler — обработчик HTTP-запросов.
type handler struct {
userService UserService
authService AuthService
notificationService NotificationService
}
// NewHandler создаёт обработчик HTTP-запросов.
func NewHandler(
userService UserService,
authService AuthService,
notificationService NotificationService,
) Handler {
return &handler{
userService: userService,
authService: authService,
notificationService: notificationService,
}
}
// Routes возвращает маршрутизатор со всеми зарегистрированными хендлерами.
// App-слой использует этот http.Handler при создании http.Server.
func (h *handler) Routes() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/health", h.healthHandler)
mux.HandleFunc("/users/me", h.getUserProfile)
return mux
}
func (h *handler) healthHandler(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
if _, err := fmt.Fprintln(w, "ok"); err != nil {
slog.Error("ошибка записи ответа", "err", err)
}
}
// getUserProfile — хендлер профиля пользователя.
// Вызывает userService.GetProfile(), который внутри дёрнет authService.ValidateToken().
// Если authService == nil (как в antipattern-broken) — тут будет nil pointer dereference.
func (h *handler) getUserProfile(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
profile := h.userService.GetProfile(token)
w.WriteHeader(http.StatusOK)
if _, err := fmt.Fprintln(w, profile); err != nil {
slog.Error("ошибка записи ответа", "err", err)
}
}
+63
View File
@@ -0,0 +1,63 @@
package app
import (
"log/slog"
"net/http"
"github.com/olezhek28/di-demo/internal/config"
)
// App — структура приложения.
// Содержит DI-контейнер и HTTP-сервер.
// API-слой отвечает за хендлеры и роутинг, а http.Server живёт здесь —
// это инфраструктура, а не бизнес-логика.
type App struct {
diContainer *diContainer
httpServer *http.Server
}
// New создаёт приложение и инициализирует все зависимости через DI-контейнер.
// Вся цепочка зависимостей разрешается здесь: от базы данных до HTTP-хендлеров.
// Если какая-то зависимость не создалась — процесс завершится внутри контейнера.
func New() *App {
a := &App{
diContainer: newDIContainer(),
}
a.initDeps()
return a
}
// initDeps последовательно вызывает функции инициализации.
// Если нужно добавить новый шаг (миграции, метрики и т.д.) —
// просто добавить функцию в слайс inits.
func (a *App) initDeps() {
inits := []func(){
a.initHTTPServer,
}
for _, fn := range inits {
fn()
}
}
// initHTTPServer создаёт HTTP-сервер.
// API-слой (Handler) предоставляет только роутинг — Routes().
// А http.Server с адресом и конфигом создаётся здесь, в app-слое.
func (a *App) initHTTPServer() {
a.httpServer = &http.Server{
Addr: config.AppConfig().HTTPAddr,
Handler: a.diContainer.Handler().Routes(),
}
}
// Run запускает HTTP-сервер.
//
// Сейчас просто запускаем и всё. Перехват сигналов, graceful shutdown,
// закрытие ресурсов в правильном порядке — тема отдельного видео.
func (a *App) Run() error {
slog.Info("сервер запущен", "addr", config.AppConfig().HTTPAddr)
return a.httpServer.ListenAndServe()
}
+194
View File
@@ -0,0 +1,194 @@
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
}
+38
View File
@@ -0,0 +1,38 @@
package cache
import "log/slog"
// Cache — интерфейс подключения к кэшу.
// Структура неэкспортируемая — создать объект можно только через New.
type Cache interface {
Get(key string) (string, error)
Set(key, value string) error
Close() error
}
// redisCache — обёртка над подключением к кэшу.
type redisCache struct {
addr string
}
// New создаёт подключение к кэшу.
func New(addr string) Cache {
slog.Info("подключились к кэшу", "addr", addr)
return &redisCache{addr: addr}
}
// Get получает значение по ключу.
func (c *redisCache) Get(key string) (string, error) {
return "", nil
}
// Set устанавливает значение по ключу.
func (c *redisCache) Set(key, value string) error {
return nil
}
// Close закрывает подключение к кэшу.
func (c *redisCache) Close() error {
slog.Info("подключение к кэшу закрыто")
return nil
}
+25
View File
@@ -0,0 +1,25 @@
package config
// Config хранит конфигурацию приложения.
type Config struct {
DSN string
RedisAddr string
HTTPAddr string
}
// appConfig — глобальный экземпляр конфигурации.
var appConfig = &Config{
DSN: "postgres://localhost:5432/demo",
RedisAddr: "localhost:6379",
HTTPAddr: ":8080",
}
// AppConfig возвращает конфигурацию приложения.
func AppConfig() *Config {
return appConfig
}
// New создаёт конфиг с дефолтными значениями.
func New() *Config {
return appConfig
}
+64
View File
@@ -0,0 +1,64 @@
package database
import (
"fmt"
"log/slog"
)
// DB — интерфейс подключения к базе данных.
// Потребители зависят от этого интерфейса, а не от конкретной реализации.
// Структура db неэкспортируемая — создать объект можно только через New.
type DB interface {
Query(query string) error
QueryRow(query string) error
Exec(query string) error
BeginTx() error
BulkInsert(query string, args ...any) error
Close() error
}
// db — обёртка над подключением к базе данных.
type db struct {
dsn string
}
// New создаёт подключение к базе данных.
func New(dsn string) (DB, error) {
if dsn == "" {
return nil, fmt.Errorf("dsn пустой")
}
slog.Info("подключились к базе данных", "dsn", dsn)
return &db{dsn: dsn}, nil
}
// Query выполняет запрос на чтение.
func (d *db) Query(query string) error {
return nil
}
// QueryRow выполняет запрос, возвращающий одну строку.
func (d *db) QueryRow(query string) error {
return nil
}
// Exec выполняет запрос на запись.
func (d *db) Exec(query string) error {
return nil
}
// BeginTx начинает транзакцию.
func (d *db) BeginTx() error {
return nil
}
// BulkInsert выполняет массовую вставку.
func (d *db) BulkInsert(query string, args ...any) error {
return nil
}
// Close закрывает подключение к базе данных.
func (d *db) Close() error {
slog.Info("подключение к базе данных закрыто")
return nil
}
+32
View File
@@ -0,0 +1,32 @@
package events
import "log/slog"
// EventBus — интерфейс шины событий приложения.
// Чистый pub/sub брокер без внешних зависимостей.
// Сервисы сами публикуют события и подписываются на них.
// Структура неэкспортируемая — создать объект можно только через NewEventBus.
type EventBus interface {
Publish(event string)
Subscribe(event string, handler func())
}
// eventBus — реализация шины событий.
type eventBus struct{}
// NewEventBus создаёт шину событий.
func NewEventBus() EventBus {
slog.Info("шина событий создана")
return &eventBus{}
}
// Publish публикует событие.
func (b *eventBus) Publish(event string) {
slog.Info("событие опубликовано", "event", event)
}
// Subscribe подписывается на событие.
func (b *eventBus) Subscribe(event string, handler func()) {
slog.Info("подписка на событие", "event", event)
}
+26
View File
@@ -0,0 +1,26 @@
package repository
import "log/slog"
// NotificationDB — интерфейс базы данных, который нужен NotificationRepo.
// Включает BulkInsert — для массовой вставки уведомлений.
type NotificationDB interface {
Query(query string) error
Exec(query string) error
BulkInsert(query string, args ...any) error
}
// NotificationRepo — интерфейс репозитория уведомлений.
// Структура неэкспортируемая — создать объект можно только через NewNotificationRepo.
type NotificationRepo interface{}
// notificationRepo — репозиторий уведомлений.
type notificationRepo struct {
db NotificationDB
}
// NewNotificationRepo создаёт репозиторий уведомлений.
func NewNotificationRepo(db NotificationDB) NotificationRepo {
slog.Info("репозиторий уведомлений создан")
return &notificationRepo{db: db}
}
+33
View File
@@ -0,0 +1,33 @@
package repository
import "log/slog"
// SessionDB — интерфейс базы данных, который нужен SessionRepo.
// Включает BeginTx — для атомарного создания/удаления сессий.
type SessionDB interface {
Query(query string) error
Exec(query string) error
BeginTx() error
}
// SessionCache — интерфейс кэша, который нужен SessionRepo.
type SessionCache interface {
Get(key string) (string, error)
Set(key, value string) error
}
// SessionRepo — интерфейс репозитория сессий.
// Структура неэкспортируемая — создать объект можно только через NewSessionRepo.
type SessionRepo interface{}
// sessionRepo — репозиторий сессий.
type sessionRepo struct {
db SessionDB
cache SessionCache
}
// NewSessionRepo создаёт репозиторий сессий.
func NewSessionRepo(db SessionDB, cache SessionCache) SessionRepo {
slog.Info("репозиторий сессий создан")
return &sessionRepo{db: db, cache: cache}
}
+26
View File
@@ -0,0 +1,26 @@
package repository
import "log/slog"
// UserDB — интерфейс базы данных, который нужен UserRepo.
// Включает QueryRow — для поиска одного пользователя по ID/email.
type UserDB interface {
Query(query string) error
QueryRow(query string) error
Exec(query string) error
}
// UserRepo — интерфейс репозитория пользователей.
// Структура неэкспортируемая — создать объект можно только через NewUserRepo.
type UserRepo interface{}
// userRepo — репозиторий пользователей.
type userRepo struct {
db UserDB
}
// NewUserRepo создаёт репозиторий пользователей.
func NewUserRepo(db UserDB) UserRepo {
slog.Info("репозиторий пользователей создан")
return &userRepo{db: db}
}
+66
View File
@@ -0,0 +1,66 @@
package service
import "log/slog"
// AuthUserRepository — интерфейс репозитория пользователей для AuthService.
type AuthUserRepository interface {
// методы, которые AuthService использует из репозитория пользователей
}
// AuthSessionRepository — интерфейс репозитория сессий для AuthService.
type AuthSessionRepository interface {
// методы, которые AuthService использует из репозитория сессий
}
// AuthCache — интерфейс кэша для AuthService (хранит токены).
type AuthCache interface {
Get(key string) (string, error)
Set(key, value string) error
}
// AuthEventPublisher — интерфейс для публикации событий авторизации.
type AuthEventPublisher interface {
Publish(event string)
}
// AuthService — интерфейс сервиса авторизации.
// Структура неэкспортируемая — создать объект можно только через NewAuthService.
type AuthService interface {
ValidateToken(token string) bool
}
// authService — сервис авторизации.
type authService struct {
userRepo AuthUserRepository
sessionRepo AuthSessionRepository
cache AuthCache
events AuthEventPublisher
}
// NewAuthService создаёт сервис авторизации.
func NewAuthService(
userRepo AuthUserRepository,
sessionRepo AuthSessionRepository,
cache AuthCache,
events AuthEventPublisher,
) AuthService {
slog.Info("сервис авторизации создан")
return &authService{
userRepo: userRepo,
sessionRepo: sessionRepo,
cache: cache,
events: events,
}
}
// ValidateToken проверяет токен авторизации.
// Проверяет наличие токена в кэше — именно тут взорвётся nil,
// если кэш не был правильно инициализирован (как в antipattern-broken).
func (s *authService) ValidateToken(token string) bool {
slog.Info("проверяем токен", "token", token)
// Проверяем токен в кэше. Если cache == nil — паника.
_, err := s.cache.Get(token)
return err == nil
}
+43
View File
@@ -0,0 +1,43 @@
package service
import "log/slog"
// NotificationRepository — интерфейс репозитория уведомлений для NotificationService.
type NotificationRepository interface {
// методы, которые NotificationService использует из репозитория уведомлений
}
// NotificationUserService — интерфейс сервиса пользователей для NotificationService.
type NotificationUserService interface {
// методы, которые NotificationService использует из сервиса пользователей
}
// NotificationEventSubscriber — интерфейс для подписки на события.
type NotificationEventSubscriber interface {
Subscribe(event string, handler func())
}
// NotificationService — интерфейс сервиса уведомлений.
// Структура неэкспортируемая — создать объект можно только через NewNotificationService.
type NotificationService interface{}
// notificationService — сервис уведомлений.
type notificationService struct {
notificationRepo NotificationRepository
userService NotificationUserService
events NotificationEventSubscriber
}
// NewNotificationService создаёт сервис уведомлений.
func NewNotificationService(
notificationRepo NotificationRepository,
userService NotificationUserService,
events NotificationEventSubscriber,
) NotificationService {
slog.Info("сервис уведомлений создан")
return &notificationService{
notificationRepo: notificationRepo,
userService: userService,
events: events,
}
}
+52
View File
@@ -0,0 +1,52 @@
package service
import "log/slog"
// UserRepository — интерфейс репозитория пользователей для UserService.
type UserRepository interface {
// методы, которые UserService использует из репозитория пользователей
}
// UserAuthService — интерфейс сервиса авторизации для UserService.
type UserAuthService interface {
ValidateToken(token string) bool
}
// UserEventPublisher — интерфейс для публикации событий пользователей.
type UserEventPublisher interface {
Publish(event string)
}
// UserService — интерфейс сервиса пользователей.
// Структура неэкспортируемая — создать объект можно только через NewUserService.
type UserService interface {
GetProfile(token string) string
}
// userService — сервис пользователей.
type userService struct {
userRepo UserRepository
authService UserAuthService
events UserEventPublisher
}
// NewUserService создаёт сервис пользователей.
func NewUserService(userRepo UserRepository, authService UserAuthService, events UserEventPublisher) UserService {
slog.Info("сервис пользователей создан")
return &userService{
userRepo: userRepo,
authService: authService,
events: events,
}
}
// GetProfile возвращает профиль текущего пользователя.
// Сначала проверяет токен через authService — и именно тут взорвётся nil,
// если authService не был правильно инициализирован.
func (s *userService) GetProfile(token string) string {
if !s.authService.ValidateToken(token) {
return "unauthorized"
}
return "user profile data"
}