import
This commit is contained in:
+33
@@ -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/
|
||||||
@@ -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 # попытка генерации с циклом (упадёт с ошибкой)
|
||||||
|
```
|
||||||
@@ -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/'
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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{}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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 ¬ificationRepo{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- NotificationService ---
|
||||||
|
|
||||||
|
type notificationServiceImpl struct {
|
||||||
|
notificationRepo NotificationRepository
|
||||||
|
userService UserService
|
||||||
|
events EventBus
|
||||||
|
}
|
||||||
|
|
||||||
|
func newNotificationService(notificationRepo NotificationRepository, userService UserService, events EventBus) NotificationService {
|
||||||
|
return ¬ificationServiceImpl{notificationRepo: notificationRepo, userService: userService, events: events}
|
||||||
|
}
|
||||||
@@ -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}
|
||||||
|
}
|
||||||
@@ -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{}
|
||||||
@@ -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)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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 ¬ificationRepo{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 ¬ificationServiceImpl{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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
_, _ = InitializeEventBus()
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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=
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
Vendored
+38
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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 ¬ificationRepo{db: db}
|
||||||
|
}
|
||||||
@@ -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}
|
||||||
|
}
|
||||||
@@ -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}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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 ¬ificationService{
|
||||||
|
notificationRepo: notificationRepo,
|
||||||
|
userService: userService,
|
||||||
|
events: events,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user