Featured image of post Структурная и поведенческая архитектура: графовый подход к контролю сложности

Структурная и поведенческая архитектура: графовый подход к контролю сложности

AI-агенты генерируют код быстро, но часто создают архитектурный хаос. После двух недель вайб-кодинга я обнаружил функцию, вызываемую трижды с одинаковыми аргументами в цепочке. Стало ясно: без формального контроля архитектуры не обойтись. В статье показываю, как использовать граф для описания структуры и поведения системы, и как автоматически валидировать архитектуру по 16 метрикам теории графов.

Введение: Почему архитектура важна в эпоху AI-кодинга

С появлением AI-агентов я начал активно их тестировать и переписал несколько своих pet-проектов с их помощью. Поначалу всё работало отлично. Однако чем больше становилась кодовая база, тем больше времени агенты тратили на решение задач. Появились зацикливания, избыточная генерация кода, предложения отрефакторить чуть ли не весь проект.

Последней каплей стал момент, когда после двух недель работы над проектом я обнаружил код, где функция вызывалась трижды с одинаковыми аргументами в цепочке вызовов: результат первого вызова передавался во вторую функцию, внутри которой снова вызывалась та же функция с теми же аргументами, и результат шёл в третью функцию, где всё повторялось.

Стало очевидно: вайб-кодинг без жёсткого контроля архитектуры — это путь в никуда. Необходим формальный подход к описанию и валидации архитектуры, который не даст системе превратиться в неконтролируемый клубок зависимостей.

Два вида архитектуры: структура vs поведение

При анализе архитектуры программных систем я выделяю два фундаментально различных аспекта:

Структурная архитектура представляет собой статическую картину системы. Она отвечает на вопросы “из чего состоит система?” и “как компоненты связаны друг с другом?”. Это пакеты, модули, классы, функции и зависимости между ними. Структурная архитектура описывает потенциальные связи — что может вызывать что.

Поведенческая архитектура описывает динамику выполнения системы. Она отвечает на вопрос “что происходит во время работы?”. Это последовательность вызовов функций, поток управления, передача данных между компонентами. Поведенческая архитектура показывает, как система работает в конкретных сценариях.

Важно понимать различие: структурная архитектура показывает все возможные пути, поведенческая — реально пройденные. Первая статична и полна, вторая динамична и зависит от контекста выполнения.

Граф как универсальный язык архитектуры

Оба вида архитектуры можно представить через единую математическую абстракцию — граф. Граф состоит из узлов (вершин) и рёбер (связей между узлами). Эта простая структура оказывается достаточно мощной для описания как структуры, так и поведения.

Структурный граф

В структурном графе:

  • Узлы — компоненты системы (пакеты, типы, методы, функции)
  • Рёбра — статические связи (импорты, зависимости, потенциальные вызовы)

Граф имеет чёткую иерархию: пакеты содержат типы, типы содержат методы. Рёбра показывают, какой компонент от какого зависит.

graph TB pkg[internal/service] order[OrderService] process[ProcessOrder] validate[Validate] repo[internal/repository] orderRepo[OrderRepository] save[Save] pkg --> order order --> process order --> validate repo --> orderRepo orderRepo --> save process -.calls.-> validate process -.calls.-> save style pkg fill:#e1f5ff style repo fill:#e1f5ff style order fill:#fff4e1 style orderRepo fill:#fff4e1 style process fill:#f0f0f0 style validate fill:#f0f0f0 style save fill:#f0f0f0

Граф поведения

Граф поведения отличается принципиально:

  • Узлы — те же компоненты, но только участвующие в конкретном сценарии
  • Рёбра — реальные вызовы, пронумерованные в порядке выполнения
  • Мультирёбра — несколько рёбер между одними узлами (повторные вызовы)

Простая аналогия: берём sequence-диаграмму и преобразуем её в граф. Стрелки на диаграмме становятся рёбрами, нумерация сохраняется.

Построение структурного графа из Go-кода

Процесс построения структурного графа включает несколько этапов:

1. Анализ исходного кода

Используя AST (Abstract Syntax Tree) парсер Go, проходим по всем файлам проекта и извлекаем:

  • Пакеты и их импорты
  • Типы (структуры, интерфейсы)
  • Функции и методы
  • Вызовы между функциями/методами
  • Зависимости типов (поля структур)

2. Формирование узлов

Каждый найденный компонент становится узлом графа с уникальным ID:

  • Пакет: internal/service
  • Тип: internal/service.OrderService
  • Метод: internal/service.OrderService.ProcessOrder
  • Функция: internal/service.ValidateOrder

3. Формирование рёбер

Создаём рёбра различных типов:

  • contains — пакет содержит тип, тип содержит метод
  • import — пакет импортирует другой пакет
  • calls — функция/метод вызывает другую функцию/метод
  • uses — тип использует другой тип (через поля)
  • embeds — тип встраивает другой тип

4. Сериализация в YAML

Результирующий граф сохраняется в формате YAML:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
components:
  internal/service:
    title: service
    entity: package

  internal/service.OrderService:
    title: OrderService
    entity: struct
    properties:
      package: internal/service

  internal/service.OrderService.ProcessOrder:
    title: ProcessOrder
    entity: method
    properties:
      package: internal/service
      receiver: OrderService

links:
  internal/service:
    - to: internal/service.OrderService
      type: contains

  internal/service.OrderService:
    - to: internal/service.OrderService.ProcessOrder
      type: contains

  internal/service.OrderService.ProcessOrder:
    - to: internal/repository.OrderRepository.Save
      type: calls

Граф поведения: от sequence-диаграмм к мультиграфу

Поведенческая архитектура строится из трассировок выполнения. Для этого в код добавляются точки инструментации:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func (s *OrderService) ProcessOrder(order Order) error {
    tracer.Enter("OrderService.ProcessOrder")

    if err := s.Validate(order); err != nil {
        tracer.ExitError("OrderService.ProcessOrder", err)
        return err
    }

    if err := s.repo.Save(order); err != nil {
        tracer.ExitError("OrderService.ProcessOrder", err)
        return err
    }

    tracer.ExitSuccess("OrderService.ProcessOrder")
    return nil
}

Во время выполнения тестов собираются трассировки вызовов, которые преобразуются в два представления:

Sequence-диаграмма

sequenceDiagram participant Test participant OrderService participant Validator participant Repository Test->>OrderService: ProcessOrder() OrderService->>Validator: Validate() Validator-->>OrderService: ok OrderService->>Repository: Save() Repository-->>OrderService: ok OrderService-->>Test: ok

Граф поведения

graph LR test[Test] service[OrderService.ProcessOrder] validate[Validator.Validate] repo[Repository.Save] test -->|1| service service -->|2| validate service -->|3| repo style test fill:#e1f5ff style service fill:#fff4e1 style validate fill:#f0f0f0 style repo fill:#f0f0f0

Ключевые отличия графа поведения:

  1. Рёбра пронумерованы (порядок вызовов)
  2. Между двумя узлами может быть несколько рёбер (мультиграф)
  3. Граф отражает конкретный сценарий, а не все возможные пути

Практический пример и инструменты

Для автоматизации построения графов я разработал инструмент aiarch, который состоит из двух частей:

Сбор структурной архитектуры

1
2
# Анализ Go-кода и построение графа
aiarch collect . -l go -o architecture.yaml

Анализатор проходит по всему коду, парсит AST и строит полный граф зависимостей.

Генерация графов поведения

1
2
# Генерация контекстов из трассировок тестов
aiarch trace ./test/traces -o contexts.yaml

Из собранных во время тестов трассировок генерируются sequence-диаграммы в формате PlantUML и контексты выполнения.

Валидация графа

Построенный структурный граф можно валидировать по правилам теории графов:

1
aiarch validate architecture.yaml

Инструмент проверяет архитектурные инварианты, такие как отсутствие циклических зависимостей, ограничения на связность модулей, метрики центральности и другие аспекты качества архитектуры. Подробнее о метриках и правилах валидации расскажу в следующих статьях.

Заключение: контроль сложности в эпоху AI

Графовый подход к архитектуре даёт несколько преимуществ:

Унификация. Один математический объект описывает и структуру, и поведение. Это упрощает инструментарий и позволяет применять единый набор метрик.

Формализация. Граф — точная, недвусмысленная модель. Можно автоматически проверять правила, вычислять метрики, отслеживать изменения.

Визуализация. Граф легко представить визуально, что помогает понять архитектуру и обнаружить проблемы.

Контроль AI-кода. Автоматическая проверка графа после каждой генерации кода позволяет отловить архитектурные антипаттерны на ранней стадии. Можно настроить CI/CD так, чтобы блокировать изменения, нарушающие архитектурные инварианты.

В эпоху AI-кодинга контроль архитектуры становится критически важным. Агенты отлично генерируют код, но не всегда видят общую картину. Графовый подход даёт инструмент для формального контроля сложности и предотвращения архитектурной деградации.

Инструментарий для построения и валидации графов архитектуры доступен в открытом репозитории: github.com/mshogin/aiarch

Создано при помощи Hugo
Тема Stack, дизайн Jimmy