Структура проекта и зависимости

Структура проекта влияет на скорость. Если ты каждый раз ищешь, где лежит логика - ты теряешь время и мотивацию.

Принцип

Логика должна быть ближе к домену, а инфраструктура - по краям.

Это основа слоистой архитектуры: бизнес-правила в центре, детали реализации (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
Go запрещает импорт из `internal/` извне модуля. Это не конвенция - это гарантия компилятора. Всё, что не должно быть публичным API, кладётся в `internal/`.

Слоистая архитектура: направление зависимостей

Ключевое правило: зависимости направлены внутрь - от инфраструктуры к домену, но не наоборот.

┌─────────────────────────────────┐
│  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 делают эту инверсию практически бесплатной.

Если в handler у тебя SQL-запросы и бизнес-правила - потом тестировать и менять будет больно. Handler должен: распарсить запрос → вызвать use case → вернуть ответ. Всё.

Организация пакетов

По слоям 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/

Зарегистрируйтесь бесплатно, чтобы пройти квиз, решить задание с автопроверкой и вести прогресс.