Зачем archtool?
Цикл архитектурного долга
Каждый бэкенд-проект начинается с добрых намерений. Интерфейсы, чистые слои, никаких циклических импортов. Потом приходят дедлайны, и «временные» костыли множатся.
Через полгода у тебя:
- 200-строчная точка входа, которая вручную разводит 30 объектов в строго правильном порядке
sys.path.insert(0, "..")в начале каждого entrypoint-файла, потому что никто так и не починил импорты- «Сервис», который напрямую импортирует из репо другого модуля, потому что «так было быстрее»
- Тесты, которые ломаются при добавлении новой зависимости, потому что забыли обновить дерево моков
- Новый разработчик, который два дня разбирался как вообще стартует приложение
Это не проблема навыков. Это структурная проблема. Ручная разводка не масштабируется.
Почему другие DI-решения не решают её
Большинство Python DI-библиотек решают не ту проблему. Они заменяют ручную разводку на... ручную регистрацию:
# dependency-injector
class Container(containers.DeclarativeContainer):
user_repo = providers.Singleton(UserRepo)
user_service = providers.Factory(UserService, repo=user_repo)
order_repo = providers.Singleton(OrderRepo)
order_service = providers.Factory(OrderService, repo=order_repo, user_service=user_service)
# ... ещё 50 провайдеров ...
Ты всё равно вручную прописываешь каждую связь. Ты всё равно забываешь обновить контейнер когда добавляешь зависимость. Контейнер превращается во вторую кодовую базу, которую надо синхронизировать с первой.
Они также ничего не делают с архитектурой: ничто не мешает сервису импортировать репо напрямую, или контроллеру обращаться к базе данных. Нарушения невидимы, пока не превратятся в баг.
Что делает archtool иначе
Конвенция заменяет регистрацию.
Если твой класс находится в app/users/repos.py и наследует ABCRepo — archtool его найдёт. Если UserService имеет аннотацию repo: UserRepoABC — archtool подключит её. Никакого контейнера. Никаких вызовов регистрации. Никакой проблемы синхронизации.
Архитектура соблюдается принудительно, а не по надежде.
Ты объявляешь слои. archtool проверяет при старте, что ни один класс не пересекает запрещённую границу. Нарушение — это исключение при загрузке, а не тонкий баг через три месяца.
Структура и есть документация.
Когда каждый проект использует одну и ту же структуру — interfaces.py, services.py, repos.py — новый разработчик сразу знает куда смотреть. Файл интерфейсов буквально является проектной документацией для этого ограниченного контекста.
SOLID, обеспеченный структурой
archtool не просто допускает SOLID — он делает нарушения сложнее, чем соблюдение.
Single Responsibility — каждый AppModule является одним ограниченным контекстом. Users, Orders, Payments разделены. Они не могут случайно просочиться друг в друга.
Open/Closed — добавление нового модуля не требует изменений существующего кода. Инжектор подхватывает его из modules_list. Существующие модули не тронуты.
Liskov Substitution — замени UserRepo на StubUserRepo (предзарегистрируй до inject()), и все потребители получат заглушку. Потребители никогда не знали, что разговаривали с конкретным классом.
Interface Segregation — твои интерфейсы живут в одном сфокусированном файле. Ничто не заставляет тебя запихивать несвязанные методы в один интерфейс. Маленькие сфокусированные ABC — это естественный паттерн.
Dependency Inversion — на этом построен весь фреймворк. Ничто не зависит от конкретных классов. Сервисы зависят от UserRepoABC. Контроллеры зависят от UserServiceABC. Конкреции — это деталь рантайма.
Проектирование от интерфейсов
Правильный способ проектировать систему — начинать с интерфейсов, а не с реализаций.
Твой interfaces.py — это проектный документ для данного ограниченного контекста:
# app/orders/interfaces.py
class OrderRepoABC(ABCRepo):
@abstractmethod
async def get(self, order_id: UUID) -> Order: ...
@abstractmethod
async def save(self, order: Order) -> None: ...
class OrderServiceABC(ABCService):
@abstractmethod
async def place(self, user_id: UUID, items: list[Item]) -> Order: ...
@abstractmethod
async def cancel(self, order_id: UUID) -> None: ...
Читая этот файл, ты точно знаешь что делает ограниченный контекст заказов. Никаких деталей реализации. Никакого шума фреймворка. Только контракт.
Когда ты пишешь здесь docstring'и, ты документируешь поведение — не код. Здесь живут архитектурные решения. Файлы реализации — это просто исполнение этого контракта.
Любой проект может это использовать
Не нужно начинать с нуля. Archtool можно вводить в существующий проект модуль за модулем:
- Выбери один ограниченный контекст
- Вынеси его интерфейсы в
interfaces.py, унаследовав от маркеров archtool - Перемести конкретные классы в
services.py/repos.py - Добавь
AppModuleв инжектор
Остальная кодовая база не меняется. Мигрируешь в своём темпе.
Кастомные слои для кастомных архитектур
archtool поставляется с четырьмя встроенными слоями (Infrastructure, Domain, Application, Presentation) по Clean Architecture. Но если твоя архитектура другая — гексагональная, луковая, полностью своя — ты определяешь свои слои:
class IntegrationsLayer(Layer):
depends_on = InfrastructureLayer
class Components:
clients = ComponentPattern("clients", superclass=ABCClient)
adapters = ComponentPattern("adapters", superclass=ABCAdapter)
archtool будет сканировать clients.py на подклассы ABCClient и adapters.py на подклассы ABCAdapter, и разводить их автоматически. Фреймворк адаптируется к твоей архитектуре, а не наоборот.