Перейти к содержанию

Сравнение с другими фреймворками

Быстрый обзор

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():

    pool = await asyncpg.create_pool(DATABASE_URL)
    injector.register(key=DBPoolABC, value=pool)
    injector.inject()   # структурная разводка остаётся синхронной; пул уже доступен
    


Когда использовать 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.