Структура проекта и зависимости
Структура проекта влияет на скорость. Если ты каждый раз ищешь, где лежит логика - ты теряешь время и мотивацию.
Принцип
Логика должна быть ближе к домену, а инфраструктура - по краям.
Это основа слоистой архитектуры: бизнес-правила в центре, детали реализации (HTTP, база, кэш) - на периферии. Центр не знает о краях. Архитектурное обоснование этого правила - DIP (Dependency Inversion Principle), а граница между слоями обычно реализуется паттерном Adapter.
Standard Go Project Layout
В Go-сообществе сложились конвенции по структуре проектов:
myapp/
├── cmd/ # точки входа (main.go)
│ └── server/
│ └── main.go
├── internal/ # приватный код (не импортируется извне)
│ ├── domain/ # сущности, бизнес-правила
│ │ ├── user.go
│ │ └── order.go
│ ├── usecase/ # сценарии (application layer)
│ │ ├── register.go
│ │ └── place_order.go
│ └── adapter/ # инфраструктура
│ ├── http/ # хендлеры, роутер
│ ├── postgres/ # реализации репозиториев
│ └── email/ # отправка писем
├── pkg/ # публичные библиотеки (опционально)
├── migrations/ # SQL-миграции
├── go.mod
└── go.sum
Symfony Project Layout
В PHP/Symfony-проектах сложилась похожая, но своя структура. PSR-4 диктует namespace=директория, поэтому имена кричат о слоях:
myapp/
├── bin/ # CLI-точки входа (console, worker)
│ └── console
├── public/ # web-точка входа (index.php)
│ └── index.php
├── src/ # приложение (namespace App\)
│ ├── Domain/ # сущности, value objects, доменные сервисы
│ │ ├── User/
│ │ │ ├── User.php
│ │ │ └── UserRepositoryInterface.php
│ │ └── Order/
│ ├── Application/ # use cases / command handlers
│ │ ├── Register/
│ │ │ ├── RegisterUserCommand.php
│ │ │ └── RegisterUserHandler.php
│ │ └── PlaceOrder/
│ └── Infrastructure/ # инфраструктурные адаптеры
│ ├── Http/ # контроллеры, requests
│ ├── Persistence/ # Doctrine репозитории, миграции
│ └── Notification/ # email, sms
├── config/ # YAML-конфиги (services, packages)
├── migrations/ # Doctrine migrations
├── tests/ # PHPUnit
├── var/ # cache, logs (gitignore)
└── composer.json
Что где живёт
Директория Что внутри Зависит от
───────────── ─────────────────────────── ─────────────
cmd/ main, wire, конфигурация internal/*
internal/domain сущности, интерфейсы ничего
internal/usecase бизнес-сценарии domain
internal/adapter http, db, cache, email domain + usecase
Слоистая архитектура: направление зависимостей
Ключевое правило: зависимости направлены внутрь - от инфраструктуры к домену, но не наоборот.
┌─────────────────────────────────┐
│ adapter (http, db, cache) │ ← знает о usecase и domain
│ ┌───────────────────────────┐ │
│ │ usecase (бизнес-сценарии) │ │ ← знает о domain
│ │ ┌─────────────────────┐ │ │
│ │ │ domain (сущности) │ │ │ ← не знает ни о ком
│ │ └─────────────────────┘ │ │
│ └───────────────────────────┘ │
└─────────────────────────────────┘
// domain/ - определяет интерфейс, НЕ знает про PostgreSQL
type UserRepository interface {
GetByID(ctx context.Context, id int) (*User, error)
Create(ctx context.Context, user *User) error
}
// adapter/postgres/ - реализует интерфейс
type PostgresUserRepo struct {
db *gorm.DB
}
func (r *PostgresUserRepo) GetByID(ctx context.Context, id int) (*User, error) {
var user User
err := r.db.WithContext(ctx).First(&user, id).Error
return &user, err
}
// usecase/ - зависит от интерфейса, не от реализации
type RegisterUC struct {
userRepo domain.UserRepository // интерфейс, не *PostgresUserRepo
}
<?php
declare(strict_types=1);
// src/Domain/User/UserRepositoryInterface.php - интерфейс, НЕ знает про Doctrine
namespace App\Domain\User;
interface UserRepositoryInterface
{
public function findById(int $id): ?User;
public function save(User $user): void;
}
// src/Infrastructure/Persistence/DoctrineUserRepository.php - реализует интерфейс
namespace App\Infrastructure\Persistence;
use App\Domain\User\User;
use App\Domain\User\UserRepositoryInterface;
use Doctrine\ORM\EntityManagerInterface;
final readonly class DoctrineUserRepository implements UserRepositoryInterface
{
public function __construct(private EntityManagerInterface $em) {}
public function findById(int $id): ?User
{
return $this->em->find(User::class, $id);
}
public function save(User $user): void
{
$this->em->persist($user);
$this->em->flush();
}
}
// src/Application/Register/RegisterUserHandler.php - зависит от интерфейса
namespace App\Application\Register;
use App\Domain\User\UserRepositoryInterface;
final readonly class RegisterUserHandler
{
public function __construct(
private UserRepositoryInterface $userRepo, // интерфейс, не DoctrineUserRepository
) {}
}
В Symfony привязка интерфейса к реализации - в config/services.yaml:
services:
App\Domain\User\UserRepositoryInterface:
alias: App\Infrastructure\Persistence\DoctrineUserRepository
Почему это важно: если завтра ты решишь заменить PostgreSQL на MongoDB - меняется только Infrastructure/, бизнес-логика не трогается. Тонкие интерфейсы Go делают эту инверсию практически бесплатной.
Организация пакетов
По слоям vs по фичам
По слоям (стандарт): По фичам (для больших проектов):
internal/ internal/
domain/ user/
user.go domain.go
order.go usecase.go
usecase/ handler.go
register.go repo.go
place_order.go order/
adapter/ domain.go
http/handler.go usecase.go
postgres/user.go handler.go
repo.go
Для проектов до 20-30 сущностей - структура по слоям проще и понятнее. Для больших проектов (50+ сущностей) - структура по фичам уменьшает связность.
Правила именования пакетов
Хорошо Плохо
────────────────────── ─────────────────────
auth authPackage
postgres postgresAdapter
email emailUtils
order orderHelpers
Пакет в Go - это единица абстракции. Имя пакета - существительное, не глагол. Без суффиксов -utils, -helpers, -common.
Пример структуры фронтенда
src/
├── pages/ # страницы (роуты)
│ ├── Home.tsx
│ └── Profile.tsx
├── components/ # переиспользуемые компоненты
│ ├── Button.tsx
│ └── Card.tsx
├── features/ # фичи с логикой
│ ├── auth/
│ └── cart/
├── shared/ # утилиты, хуки, типы
│ ├── hooks/
│ └── api/
└── App.tsx
Принцип тот же: бизнес-логика (features/) отделена от UI (components/) и инфраструктуры (shared/api/).
Что ломает структуру
Антипаттерн Почему плохо
─────────────────────────────── ─────────────────────────────
utils/ с 50 функциями «Свалка» без смысловой группировки
Циклические импорты Пакеты A и B зависят друг от друга
God-пакет на 5000 строк Слишком много ответственности
handler знает про SQL Нарушение слоёв
domain импортирует adapter Зависимость направлена наружу
Мини-задание
- Нарисуй текущую структуру своего проекта (3 уровня)
- Проверь: есть ли циклические зависимости между пакетами?
- Если handler содержит SQL - вынеси в отдельный репозиторий
- Убедись, что
domain/не импортирует пакеты изadapter/