Сравнение с другими фреймворками
Быстрый обзор
| archtool | dependency-injector | injector | Ручной DI | |
|---|---|---|---|---|
| Стиль регистрации | Конвенция (авто-сканирование) | Явные провайдеры | Декоратор + модуль | Явный |
| Бойлерплейт | Минимальный | Средний–Высокий | Средний | Высокий |
| Проверка границ слоёв | ✅ Встроена | ❌ | ❌ | ❌ |
| CLI-скаффолдинг | ✅ | ❌ | ❌ | ❌ |
from __future__ import annotations |
✅ | ✅ | Частично | — |
| Async-провайдеры | ❌ | ✅ | ❌ | Своя реализация |
| Скоупы / времена жизни | ❌ | ✅ | ✅ | Своя реализация |
| Фабрики / провайдеры | ❌ | ✅ | ✅ | Своя реализация |
| Сообщество / зрелость | Маленькое / новое | Большое / зрелое | Среднее / стабильное | — |
archtool
archtool использует аннотации на уровне класса на конкретных классах для объявления зависимостей. Никакого кода регистрации, никаких декораторов — если класс лежит в правильном файле и наследует правильный базовый класс, он подхватывается автоматически.
# interfaces.py
from archtool.layers.default_layer_interfaces import ABCService, ABCRepo
class UserRepoABC(ABCRepo):
@abstractmethod
def find_all(self) -> list[str]: ...
class UserServiceABC(ABCService):
@abstractmethod
def get_name(self) -> str: ...
# services.py — аннотация на конкретном классе, __init__ не нужен
class UserService(UserServiceABC):
repo: UserRepoABC # archtool читает это, находит UserRepo, вызывает setattr
def get_name(self) -> str:
return self.repo.find_all()[0]
injector = DependencyInjector(
modules_list=[AppModule("app.users")],
project_root=ROOT,
)
injector.inject()
dependency-injector
dependency-injector от ets-labs — наиболее популярная Python DI-библиотека. Использует явные провайдеры, объявленные в классе контейнера.
from dependency_injector import containers, providers
class Container(containers.DeclarativeContainer):
user_repo = providers.Singleton(UserRepo)
user_service = providers.Factory(UserService, repo=user_repo)
container = Container()
service = container.user_service()
Сильные стороны:
- Зрелая, проверенная, большое сообщество
- Богатые типы провайдеров: Singleton, Factory, ThreadLocalSingleton, Resource, Coroutine, …
- Полноценная поддержка async (async-провайдеры, async init_resources)
- Скоупы и управление временем жизни каждого объекта
- Интеграции с фреймворками из коробки (FastAPI, Flask, Django)
Слабые стороны: - Каждую зависимость надо регистрировать руками — десятки строк на большом проекте - Нет проверки границ слоёв - Нет CLI-инструментов
Когда выбрать вместо archtool: нужны скоупы, async-управление ресурсами или интеграции с фреймворками.
injector
injector Алека Томаса — вдохновлён Google Guice. Использует модули и инъекцию через конструктор по аннотациям типов.
from injector import Injector, Module, provider, singleton
class AppModule(Module):
@singleton
@provider
def provide_repo(self) -> UserRepoABC:
return UserRepo()
i = Injector([AppModule()])
service = i.get(UserService) # конструктор: def __init__(self, repo: UserRepoABC)
Важно: injector использует инъекцию через конструктор — зависимость передаётся как параметр конструктора, а не устанавливается как атрибут класса. Это принципиально отличается от стиля archtool.
Сильные стороны: - Чистый API в стиле Guice - Скоупы: singleton, thread-local, кастомные - Авто-биндинг — для простых случаев можно вообще не писать модуль
Слабые стороны: - Требует параметры конструктора для каждой зависимости - Всё равно нужна явная разводка в нетривиальных случаях - Нет проверки архитектуры - Ограниченная поддержка async
Ручной DI (чистый Python)
Для маленьких проектов ручная разводка зачастую является правильным ответом:
repo = UserRepo()
service = UserService()
service.repo = repo # тот же стиль что и archtool, только написанный руками
Сильные стороны: - Ноль зависимостей - Никакой магии — каждая связь явна и отслеживаема - Полный контроль над временем жизни объектов
Слабые стороны: - Код разводки растёт линейно с кодовой базой - Надо поддерживать вручную по мере углубления графа зависимостей - Нет принудительных архитектурных ограничений
Сильные и слабые стороны archtool
Сильные стороны
- Нулевой бойлерплейт — добавь файл в нужное место, унаследуй нужный базовый класс, и archtool найдёт его сам. Никаких вызовов регистрации.
- Принудительная архитектура — нарушения границ слоёв выявляются при старте, а не тихо в рантайме.
- Работает с
from __future__ import annotations— используетtyping.get_type_hints()для разрешения строковых аннотаций. - CLI-скаффолдинг —
archtool initсоздаёт полный каркас проекта со слоистой архитектурой за секунды. - Лёгкий рантайм — только
clickиrichкак зависимости. - Тест сборки — один тест проверяет всю проводку до продакшена.
Что стоит знать (не блокеры)
-
Разумные дефолты, полная расширяемость — встроенные слои (
repos.py,services.pyи т.д.) следуют конвенциям Clean Architecture. Но слои — это просто классы: можно определить собственныйLayerс любымComponentPattern, указывающим на любое имя файла и любой суперкласс. Дефолты — это отправная точка, а не клетка. -
Нет required-параметров конструктора — конкретные классы инстанциируются как
Class(). Это намеренно: зависимости текут через DI-аннотации, а не конструкторы. Если объекту нужна конфигурация при старте, чистые варианты: - Читать её из переменных окружения / конфига в теле
__init__(аргументы не нужны) - Предварительно зарегистрировать через
injector.register(key=ConfigABC, value=my_config)до вызоваinject()— archtool внедрит её как любую другую зависимость -
После
inject()установить напрямую:injector.dependencies["...key..."] = value -
Одна реализация на интерфейс — намеренно. Условная разводка (подменить репозиторий в тестах) делается предварительной регистрацией тестовой реализации до вызова
inject(). archtool уважает вручную предзарегистрированные зависимости и пропускает авто-обнаружение для них. -
Async-инициализация ресурсов — задача archtool — структурная разводка (что с чем соединяется), а не управление жизненным циклом. Async-ресурсы (пулы соединений, клиенты) инициализируются за пределами archtool, затем передаются через
injector.register():
Когда использовать archtool
Подходит хорошо:
- Новый проект, следующий чистой/слоистой архитектуре
- Команда хочет нулевой DI-бойлерплейт: добавил модуль — он автоматически подхватывается
- Важно чтобы архитектурные ограничения проверялись при старте
- Нужно регистрировать async-ресурсы или кастомные объекты наряду с авто-компонентами
Менее подходит:
- Структура домена принципиально не ложится на разбивку service/repo/controller и ты не хочешь определять кастомные слои
- SQLAlchemy
Sessionна запрос — archtool не управляет жизненным циклом сессий. Стандартный паттерн: инжектироватьasync_sessionmakerв репозиторий как обычную зависимость, а в каждом методе открывать сессию через контекстный менеджерUnitOfWork:
class UserRepo(UserRepoABC):
session_maker: async_sessionmaker # archtool инжектирует это
async def get_user(self, user_id: str) -> UserDM:
async with UnitOfWork(self.session_maker) as uow:
session = uow.get_session()
return await session.get(UserORM, user_id)
Сессия живёт ровно столько, сколько нужно методу. Если сервисный метод должен охватить несколько репо-вызовов внутри одной транзакции — UnitOfWork (или саму сессию) передают аргументом. Это явно и понятно — в отличие от props-drilling в React, здесь это просто обычный Python.