Ответ
Да, я регулярно применяю принципы многослойной архитектуры (Layered Architecture), так как она обеспечивает отличное разделение ответственности и упрощает тестирование и поддержку кода.
Классическая структура, которую я использую, включает три основных слоя:
Слой представления (Presentation/Transport Layer)
- Ответственность: Прием и обработка внешних запросов (HTTP, gRPC, WebSocket), валидация входящих данных, сериализация ответов (JSON, Protobuf). Этот слой не содержит бизнес-логики.
- Примеры: HTTP-хендлеры (например, для
net/http
илиgin
), gRPC-сервисы.
Слой бизнес-логики (Service/Business Logic Layer)
- Ответственность: Ядро приложения. Здесь сосредоточена вся бизнес-логика, координация операций и принятие решений. Этот слой использует репозитории для доступа к данным, но не зависит от конкретной реализации хранилища.
- Примеры:
UserService
,OrderService
.
Слой доступа к данным (Data Access/Repository Layer)
- Ответственность: Абстракция над источниками данных (БД, кэш, внешние API). Он предоставляет CRUD-подобные методы для работы с данными. Слой логики не знает, откуда приходят данные — из PostgreSQL, Redis или стороннего сервиса.
- Примеры:
UserPostgresRepository
,OrderRedisCache
.
Пример с использованием интерфейсов для инверсии зависимостей:
Ключевой аспект — использование интерфейсов для связи между слоями. Это позволяет легко заменять реализации (например, мокировать репозиторий в тестах).
// --- Слой доступа к данным (интерфейс и реализация) ---
type UserRepository interface {
FindByID(ctx context.Context, id int) (*User, error)
}
type UserPostgresRepo struct {
db *sql.DB
}
func (r *UserPostgresRepo) FindByID(ctx context.Context, id int) (*User, error) {
// ... логика запроса к PostgreSQL
return nil, nil
}
// --- Слой бизнес-логики ---
// Сервис зависит от ИНТЕРФЕЙСА, а не от конкретной реализации репозитория.
type UserService struct {
repo UserRepository
}
func (s *UserService) GetUser(ctx context.Context, id int) (*User, error) {
// ... здесь может быть сложная бизнес-логика ...
return s.repo.FindByID(ctx, id)
}
// --- Слой представления ---
// Хендлер зависит от сервиса.
type UserHandler struct {
service *UserService
}
func (h *UserHandler) GetUser(c *gin.Context) {
// ... парсинг и валидация id из запроса ...
// user, err := h.service.GetUser(c.Request.Context(), id)
// ... обработка ошибки и отправка ответа ...
}
Преимущества такого подхода:
- Разделение ответственности (SoC): Каждый слой имеет четкую зону ответственности.
- Тестируемость: Легко писать юнит-тесты для каждого слоя, подменяя зависимости моками (например, тестировать сервис с моком репозитория).
- Гибкость и заменяемость: Можно легко заменить базу данных (с PostgreSQL на MongoDB), просто написав новую реализацию интерфейса репозитория, не меняя код сервиса.
- Поддерживаемость: Код становится более структурированным и понятным.