This commit is contained in:
2026-04-13 08:14:09 +03:00
commit 0449337ae7
39 changed files with 2726 additions and 0 deletions
+83
View File
@@ -0,0 +1,83 @@
package api
import (
"fmt"
"log/slog"
"net/http"
)
// UserService — интерфейс сервиса пользователей для хендлера.
type UserService interface {
GetProfile(token string) string
}
// AuthService — интерфейс сервиса авторизации для хендлера.
type AuthService interface {
// методы, которые хендлер использует из сервиса авторизации
}
// NotificationService — интерфейс сервиса уведомлений для хендлера.
type NotificationService interface {
// методы, которые хендлер использует из сервиса уведомлений
}
// Handler — интерфейс HTTP-обработчика.
// Содержит только хендлеры и роутинг.
// Ничего не знает про http.Server, порт или lifecycle —
// это ответственность app-слоя.
// Структура неэкспортируемая — создать объект можно только через NewHandler.
type Handler interface {
Routes() http.Handler
}
// handler — обработчик HTTP-запросов.
type handler struct {
userService UserService
authService AuthService
notificationService NotificationService
}
// NewHandler создаёт обработчик HTTP-запросов.
func NewHandler(
userService UserService,
authService AuthService,
notificationService NotificationService,
) Handler {
return &handler{
userService: userService,
authService: authService,
notificationService: notificationService,
}
}
// Routes возвращает маршрутизатор со всеми зарегистрированными хендлерами.
// App-слой использует этот http.Handler при создании http.Server.
func (h *handler) Routes() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/health", h.healthHandler)
mux.HandleFunc("/users/me", h.getUserProfile)
return mux
}
func (h *handler) healthHandler(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
if _, err := fmt.Fprintln(w, "ok"); err != nil {
slog.Error("ошибка записи ответа", "err", err)
}
}
// getUserProfile — хендлер профиля пользователя.
// Вызывает userService.GetProfile(), который внутри дёрнет authService.ValidateToken().
// Если authService == nil (как в antipattern-broken) — тут будет nil pointer dereference.
func (h *handler) getUserProfile(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
profile := h.userService.GetProfile(token)
w.WriteHeader(http.StatusOK)
if _, err := fmt.Fprintln(w, profile); err != nil {
slog.Error("ошибка записи ответа", "err", err)
}
}
+63
View File
@@ -0,0 +1,63 @@
package app
import (
"log/slog"
"net/http"
"github.com/olezhek28/di-demo/internal/config"
)
// App — структура приложения.
// Содержит DI-контейнер и HTTP-сервер.
// API-слой отвечает за хендлеры и роутинг, а http.Server живёт здесь —
// это инфраструктура, а не бизнес-логика.
type App struct {
diContainer *diContainer
httpServer *http.Server
}
// New создаёт приложение и инициализирует все зависимости через DI-контейнер.
// Вся цепочка зависимостей разрешается здесь: от базы данных до HTTP-хендлеров.
// Если какая-то зависимость не создалась — процесс завершится внутри контейнера.
func New() *App {
a := &App{
diContainer: newDIContainer(),
}
a.initDeps()
return a
}
// initDeps последовательно вызывает функции инициализации.
// Если нужно добавить новый шаг (миграции, метрики и т.д.) —
// просто добавить функцию в слайс inits.
func (a *App) initDeps() {
inits := []func(){
a.initHTTPServer,
}
for _, fn := range inits {
fn()
}
}
// initHTTPServer создаёт HTTP-сервер.
// API-слой (Handler) предоставляет только роутинг — Routes().
// А http.Server с адресом и конфигом создаётся здесь, в app-слое.
func (a *App) initHTTPServer() {
a.httpServer = &http.Server{
Addr: config.AppConfig().HTTPAddr,
Handler: a.diContainer.Handler().Routes(),
}
}
// Run запускает HTTP-сервер.
//
// Сейчас просто запускаем и всё. Перехват сигналов, graceful shutdown,
// закрытие ресурсов в правильном порядке — тема отдельного видео.
func (a *App) Run() error {
slog.Info("сервер запущен", "addr", config.AppConfig().HTTPAddr)
return a.httpServer.ListenAndServe()
}
+194
View File
@@ -0,0 +1,194 @@
package app
import (
"log/slog"
"os"
"github.com/olezhek28/di-demo/internal/api"
"github.com/olezhek28/di-demo/internal/cache"
"github.com/olezhek28/di-demo/internal/config"
"github.com/olezhek28/di-demo/internal/database"
"github.com/olezhek28/di-demo/internal/events"
"github.com/olezhek28/di-demo/internal/repository"
"github.com/olezhek28/di-demo/internal/service"
)
// diContainer — контейнер зависимостей с ленивой инициализацией.
//
// Принцип работы:
// Все поля начинаются как nil. При первом вызове геттера (например, DB())
// зависимость создаётся и сохраняется в поле. При повторном вызове —
// возвращается уже созданный экземпляр. Это гарантирует, что каждая
// зависимость существует в единственном экземпляре (singleton в рамках приложения).
//
// Зачем это нужно:
// В отличие от плоского main.go, где порядок строк определяет порядок инициализации,
// здесь порядок определяется самим графом зависимостей. Каждый геттер вызывает
// геттеры своих зависимостей — и они рекурсивно создаются автоматически.
// Не нужно думать «что создать первым» — контейнер разберётся сам.
//
// Как добавить новую зависимость:
// 1. Добавить поле в структуру
// 2. Написать геттер с проверкой на nil
// 3. Вызвать геттер из нужных мест — всё
// Никакой перестановки строк в main.go.
//
// Почему поля — интерфейсы:
// Каждый пакет экспортирует интерфейс (database.DB, cache.Cache и т.д.),
// а конкретная реализация скрыта (неэкспортируемая структура).
// Это гарантирует, что объект можно создать ТОЛЬКО через конструктор (New).
// Нельзя написать &database.db{} — структура не видна за пределами пакета.
// Контейнер зависит от абстракций, а не от конкретных типов.
//
// Почему геттеры не возвращают ошибку:
// Если зависимость не создалась (например, база не подключилась) —
// приложение всё равно не может работать. Нет смысла протаскивать ошибку
// через 5 уровней вызовов — проще сразу завершить процесс.
// Это упрощает код: геттеры становятся однострочными, без if err != nil.
type diContainer struct {
// Инфраструктура — базовые ресурсы, от которых зависит всё остальное.
db database.DB
cache cache.Cache
eventBus events.EventBus
// Репозитории — слой доступа к данным. Зависят от инфраструктуры.
userRepo repository.UserRepo
sessionRepo repository.SessionRepo
notificationRepo repository.NotificationRepo
// Сервисы — бизнес-логика. Зависят от репозиториев и друг от друга.
authService service.AuthService
userService service.UserService
notificationService service.NotificationService
// API — хендлеры и роутинг. Зависит от сервисов.
handler api.Handler
}
// newDIContainer создаёт новый пустой контейнер.
// Все поля nil — зависимости будут создаваться лениво при первом обращении.
func newDIContainer() *diContainer {
return &diContainer{}
}
// DB возвращает подключение к базе данных.
// Создаётся один раз при первом вызове. При повторном — возвращается тот же экземпляр.
// Если подключение не удалось — завершаем процесс: без базы приложение бессмысленно.
func (d *diContainer) DB() database.DB {
if d.db == nil {
db, err := database.New(config.AppConfig().DSN)
if err != nil {
slog.Error("не удалось подключиться к БД", "err", err)
os.Exit(1)
}
d.db = db
}
return d.db
}
// EventBus возвращает шину событий.
// Чистый pub/sub брокер без зависимостей — все сервисы зависят от него, а не наоборот.
// В антипаттерне EventBus зависел от NotificationService (тупик с циклом).
// Здесь — EventBus не зависит ни от кого, а сервисы подключаются к нему сами.
func (d *diContainer) EventBus() events.EventBus {
if d.eventBus == nil {
d.eventBus = events.NewEventBus()
}
return d.eventBus
}
// Cache возвращает подключение к кэшу.
// Создаётся один раз при первом вызове.
func (d *diContainer) Cache() cache.Cache {
if d.cache == nil {
d.cache = cache.New(config.AppConfig().RedisAddr)
}
return d.cache
}
// UserRepo возвращает репозиторий пользователей.
// Зависит от DB — если база ещё не создана, d.DB() создаст её автоматически.
func (d *diContainer) UserRepo() repository.UserRepo {
if d.userRepo == nil {
d.userRepo = repository.NewUserRepo(d.DB())
}
return d.userRepo
}
// SessionRepo возвращает репозиторий сессий.
// Зависит от DB и Cache — оба подтянутся автоматически при первом вызове.
func (d *diContainer) SessionRepo() repository.SessionRepo {
if d.sessionRepo == nil {
d.sessionRepo = repository.NewSessionRepo(d.DB(), d.Cache())
}
return d.sessionRepo
}
// NotificationRepo возвращает репозиторий уведомлений.
// Зависит от DB.
func (d *diContainer) NotificationRepo() repository.NotificationRepo {
if d.notificationRepo == nil {
d.notificationRepo = repository.NewNotificationRepo(d.DB())
}
return d.notificationRepo
}
// AuthService возвращает сервис авторизации.
// Зависит от UserRepo, SessionRepo, Cache и EventBus.
// Все зависимости подтягиваются рекурсивно — если UserRepo ещё не создан,
// он создастся, а для этого создастся DB, и так далее по цепочке.
// EventBus добавился одной строкой — никакой перестановки, никаких тупиков.
func (d *diContainer) AuthService() service.AuthService {
if d.authService == nil {
d.authService = service.NewAuthService(d.UserRepo(), d.SessionRepo(), d.Cache(), d.EventBus())
}
return d.authService
}
// UserService возвращает сервис пользователей.
// Зависит от UserRepo и AuthService — перекрёстная зависимость между сервисами.
// В плоском main.go это создавало проблему с порядком строк.
// Здесь — просто вызываем d.AuthService(), и он рекурсивно создаст всё что нужно.
func (d *diContainer) UserService() service.UserService {
if d.userService == nil {
d.userService = service.NewUserService(d.UserRepo(), d.AuthService(), d.EventBus())
}
return d.userService
}
// NotificationService возвращает сервис уведомлений.
// Зависит от NotificationRepo и UserService.
// UserService → AuthService → UserRepo → DB — вся цепочка разрешится автоматически.
func (d *diContainer) NotificationService() service.NotificationService {
if d.notificationService == nil {
d.notificationService = service.NewNotificationService(d.NotificationRepo(), d.UserService(), d.EventBus())
}
return d.notificationService
}
// Handler возвращает HTTP-хендлер.
// Вершина графа зависимостей — зависит от всех трёх сервисов.
// Один вызов d.Handler() рекурсивно создаст ВСЕ зависимости приложения.
// Хендлер отвечает только за роутинг и обработку запросов.
// http.Server с адресом создаётся в app.go — это инфраструктура, а не API.
func (d *diContainer) Handler() api.Handler {
if d.handler == nil {
d.handler = api.NewHandler(
d.UserService(),
d.AuthService(),
d.NotificationService(),
)
}
return d.handler
}
+38
View File
@@ -0,0 +1,38 @@
package cache
import "log/slog"
// Cache — интерфейс подключения к кэшу.
// Структура неэкспортируемая — создать объект можно только через New.
type Cache interface {
Get(key string) (string, error)
Set(key, value string) error
Close() error
}
// redisCache — обёртка над подключением к кэшу.
type redisCache struct {
addr string
}
// New создаёт подключение к кэшу.
func New(addr string) Cache {
slog.Info("подключились к кэшу", "addr", addr)
return &redisCache{addr: addr}
}
// Get получает значение по ключу.
func (c *redisCache) Get(key string) (string, error) {
return "", nil
}
// Set устанавливает значение по ключу.
func (c *redisCache) Set(key, value string) error {
return nil
}
// Close закрывает подключение к кэшу.
func (c *redisCache) Close() error {
slog.Info("подключение к кэшу закрыто")
return nil
}
+25
View File
@@ -0,0 +1,25 @@
package config
// Config хранит конфигурацию приложения.
type Config struct {
DSN string
RedisAddr string
HTTPAddr string
}
// appConfig — глобальный экземпляр конфигурации.
var appConfig = &Config{
DSN: "postgres://localhost:5432/demo",
RedisAddr: "localhost:6379",
HTTPAddr: ":8080",
}
// AppConfig возвращает конфигурацию приложения.
func AppConfig() *Config {
return appConfig
}
// New создаёт конфиг с дефолтными значениями.
func New() *Config {
return appConfig
}
+64
View File
@@ -0,0 +1,64 @@
package database
import (
"fmt"
"log/slog"
)
// DB — интерфейс подключения к базе данных.
// Потребители зависят от этого интерфейса, а не от конкретной реализации.
// Структура db неэкспортируемая — создать объект можно только через New.
type DB interface {
Query(query string) error
QueryRow(query string) error
Exec(query string) error
BeginTx() error
BulkInsert(query string, args ...any) error
Close() error
}
// db — обёртка над подключением к базе данных.
type db struct {
dsn string
}
// New создаёт подключение к базе данных.
func New(dsn string) (DB, error) {
if dsn == "" {
return nil, fmt.Errorf("dsn пустой")
}
slog.Info("подключились к базе данных", "dsn", dsn)
return &db{dsn: dsn}, nil
}
// Query выполняет запрос на чтение.
func (d *db) Query(query string) error {
return nil
}
// QueryRow выполняет запрос, возвращающий одну строку.
func (d *db) QueryRow(query string) error {
return nil
}
// Exec выполняет запрос на запись.
func (d *db) Exec(query string) error {
return nil
}
// BeginTx начинает транзакцию.
func (d *db) BeginTx() error {
return nil
}
// BulkInsert выполняет массовую вставку.
func (d *db) BulkInsert(query string, args ...any) error {
return nil
}
// Close закрывает подключение к базе данных.
func (d *db) Close() error {
slog.Info("подключение к базе данных закрыто")
return nil
}
+32
View File
@@ -0,0 +1,32 @@
package events
import "log/slog"
// EventBus — интерфейс шины событий приложения.
// Чистый pub/sub брокер без внешних зависимостей.
// Сервисы сами публикуют события и подписываются на них.
// Структура неэкспортируемая — создать объект можно только через NewEventBus.
type EventBus interface {
Publish(event string)
Subscribe(event string, handler func())
}
// eventBus — реализация шины событий.
type eventBus struct{}
// NewEventBus создаёт шину событий.
func NewEventBus() EventBus {
slog.Info("шина событий создана")
return &eventBus{}
}
// Publish публикует событие.
func (b *eventBus) Publish(event string) {
slog.Info("событие опубликовано", "event", event)
}
// Subscribe подписывается на событие.
func (b *eventBus) Subscribe(event string, handler func()) {
slog.Info("подписка на событие", "event", event)
}
+26
View File
@@ -0,0 +1,26 @@
package repository
import "log/slog"
// NotificationDB — интерфейс базы данных, который нужен NotificationRepo.
// Включает BulkInsert — для массовой вставки уведомлений.
type NotificationDB interface {
Query(query string) error
Exec(query string) error
BulkInsert(query string, args ...any) error
}
// NotificationRepo — интерфейс репозитория уведомлений.
// Структура неэкспортируемая — создать объект можно только через NewNotificationRepo.
type NotificationRepo interface{}
// notificationRepo — репозиторий уведомлений.
type notificationRepo struct {
db NotificationDB
}
// NewNotificationRepo создаёт репозиторий уведомлений.
func NewNotificationRepo(db NotificationDB) NotificationRepo {
slog.Info("репозиторий уведомлений создан")
return &notificationRepo{db: db}
}
+33
View File
@@ -0,0 +1,33 @@
package repository
import "log/slog"
// SessionDB — интерфейс базы данных, который нужен SessionRepo.
// Включает BeginTx — для атомарного создания/удаления сессий.
type SessionDB interface {
Query(query string) error
Exec(query string) error
BeginTx() error
}
// SessionCache — интерфейс кэша, который нужен SessionRepo.
type SessionCache interface {
Get(key string) (string, error)
Set(key, value string) error
}
// SessionRepo — интерфейс репозитория сессий.
// Структура неэкспортируемая — создать объект можно только через NewSessionRepo.
type SessionRepo interface{}
// sessionRepo — репозиторий сессий.
type sessionRepo struct {
db SessionDB
cache SessionCache
}
// NewSessionRepo создаёт репозиторий сессий.
func NewSessionRepo(db SessionDB, cache SessionCache) SessionRepo {
slog.Info("репозиторий сессий создан")
return &sessionRepo{db: db, cache: cache}
}
+26
View File
@@ -0,0 +1,26 @@
package repository
import "log/slog"
// UserDB — интерфейс базы данных, который нужен UserRepo.
// Включает QueryRow — для поиска одного пользователя по ID/email.
type UserDB interface {
Query(query string) error
QueryRow(query string) error
Exec(query string) error
}
// UserRepo — интерфейс репозитория пользователей.
// Структура неэкспортируемая — создать объект можно только через NewUserRepo.
type UserRepo interface{}
// userRepo — репозиторий пользователей.
type userRepo struct {
db UserDB
}
// NewUserRepo создаёт репозиторий пользователей.
func NewUserRepo(db UserDB) UserRepo {
slog.Info("репозиторий пользователей создан")
return &userRepo{db: db}
}
+66
View File
@@ -0,0 +1,66 @@
package service
import "log/slog"
// AuthUserRepository — интерфейс репозитория пользователей для AuthService.
type AuthUserRepository interface {
// методы, которые AuthService использует из репозитория пользователей
}
// AuthSessionRepository — интерфейс репозитория сессий для AuthService.
type AuthSessionRepository interface {
// методы, которые AuthService использует из репозитория сессий
}
// AuthCache — интерфейс кэша для AuthService (хранит токены).
type AuthCache interface {
Get(key string) (string, error)
Set(key, value string) error
}
// AuthEventPublisher — интерфейс для публикации событий авторизации.
type AuthEventPublisher interface {
Publish(event string)
}
// AuthService — интерфейс сервиса авторизации.
// Структура неэкспортируемая — создать объект можно только через NewAuthService.
type AuthService interface {
ValidateToken(token string) bool
}
// authService — сервис авторизации.
type authService struct {
userRepo AuthUserRepository
sessionRepo AuthSessionRepository
cache AuthCache
events AuthEventPublisher
}
// NewAuthService создаёт сервис авторизации.
func NewAuthService(
userRepo AuthUserRepository,
sessionRepo AuthSessionRepository,
cache AuthCache,
events AuthEventPublisher,
) AuthService {
slog.Info("сервис авторизации создан")
return &authService{
userRepo: userRepo,
sessionRepo: sessionRepo,
cache: cache,
events: events,
}
}
// ValidateToken проверяет токен авторизации.
// Проверяет наличие токена в кэше — именно тут взорвётся nil,
// если кэш не был правильно инициализирован (как в antipattern-broken).
func (s *authService) ValidateToken(token string) bool {
slog.Info("проверяем токен", "token", token)
// Проверяем токен в кэше. Если cache == nil — паника.
_, err := s.cache.Get(token)
return err == nil
}
+43
View File
@@ -0,0 +1,43 @@
package service
import "log/slog"
// NotificationRepository — интерфейс репозитория уведомлений для NotificationService.
type NotificationRepository interface {
// методы, которые NotificationService использует из репозитория уведомлений
}
// NotificationUserService — интерфейс сервиса пользователей для NotificationService.
type NotificationUserService interface {
// методы, которые NotificationService использует из сервиса пользователей
}
// NotificationEventSubscriber — интерфейс для подписки на события.
type NotificationEventSubscriber interface {
Subscribe(event string, handler func())
}
// NotificationService — интерфейс сервиса уведомлений.
// Структура неэкспортируемая — создать объект можно только через NewNotificationService.
type NotificationService interface{}
// notificationService — сервис уведомлений.
type notificationService struct {
notificationRepo NotificationRepository
userService NotificationUserService
events NotificationEventSubscriber
}
// NewNotificationService создаёт сервис уведомлений.
func NewNotificationService(
notificationRepo NotificationRepository,
userService NotificationUserService,
events NotificationEventSubscriber,
) NotificationService {
slog.Info("сервис уведомлений создан")
return &notificationService{
notificationRepo: notificationRepo,
userService: userService,
events: events,
}
}
+52
View File
@@ -0,0 +1,52 @@
package service
import "log/slog"
// UserRepository — интерфейс репозитория пользователей для UserService.
type UserRepository interface {
// методы, которые UserService использует из репозитория пользователей
}
// UserAuthService — интерфейс сервиса авторизации для UserService.
type UserAuthService interface {
ValidateToken(token string) bool
}
// UserEventPublisher — интерфейс для публикации событий пользователей.
type UserEventPublisher interface {
Publish(event string)
}
// UserService — интерфейс сервиса пользователей.
// Структура неэкспортируемая — создать объект можно только через NewUserService.
type UserService interface {
GetProfile(token string) string
}
// userService — сервис пользователей.
type userService struct {
userRepo UserRepository
authService UserAuthService
events UserEventPublisher
}
// NewUserService создаёт сервис пользователей.
func NewUserService(userRepo UserRepository, authService UserAuthService, events UserEventPublisher) UserService {
slog.Info("сервис пользователей создан")
return &userService{
userRepo: userRepo,
authService: authService,
events: events,
}
}
// GetProfile возвращает профиль текущего пользователя.
// Сначала проверяет токен через authService — и именно тут взорвётся nil,
// если authService не был правильно инициализирован.
func (s *userService) GetProfile(token string) string {
if !s.authService.ValidateToken(token) {
return "unauthorized"
}
return "user profile data"
}