惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

The Hacker News
The Hacker News
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
雷峰网
雷峰网
人人都是产品经理
人人都是产品经理
Recent Announcements
Recent Announcements
D
DataBreaches.Net
P
Proofpoint News Feed
V
Visual Studio Blog
J
Java Code Geeks
Recorded Future
Recorded Future
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
F
Full Disclosure
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
The GitHub Blog
The GitHub Blog
Engineering at Meta
Engineering at Meta
C
Cybersecurity and Infrastructure Security Agency CISA
V
Vulnerabilities – Threatpost
罗磊的独立博客
Jina AI
Jina AI
博客园 - 【当耐特】
C
CERT Recently Published Vulnerability Notes
G
GRAHAM CLULEY
Y
Y Combinator Blog
L
LangChain Blog
L
LINUX DO - 热门话题
宝玉的分享
宝玉的分享
月光博客
月光博客
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
H
Help Net Security
云风的 BLOG
云风的 BLOG
C
CXSECURITY Database RSS Feed - CXSecurity.com
博客园_首页
A
About on SuperTechFans
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Latest news
Latest news
T
Threatpost
T
Tenable Blog
有赞技术团队
有赞技术团队
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Stack Overflow Blog
Stack Overflow Blog
C
Cisco Blogs
C
Check Point Blog
T
Tor Project blog
T
Threat Research - Cisco Blogs
T
The Exploit Database - CXSecurity.com
S
Schneier on Security
美团技术团队
I
Intezer
S
Securelist
AWS News Blog
AWS News Blog

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Инвертируем зависимости одного FastAPI-эндпоинта
Stanislav K · 2026-06-21 · via Все публикации подряд на Хабре

Давайте ещё раз поговорим о SOLID. Если ваша работа хоть как-то связана с разработкой программного обеспечения или вы просто интересуетесь программированием, вы наверняка слышали этот печально известный акроним. Ему уже посвящены бесчисленные статьи, публикации в блогах и обучающие видео. Возможно, это одна из самых обсуждаемых аббревиатур в мире разработки. Но в этой статье я хочу подробнее остановиться на последней по порядку, но не по значимости букве – D, которая обозначает принцип инверсии зависимостей (Dependency Inversion PrincipleDIP).

Почему этот принцип важен для написания поддерживаемого кода? Важен ли он вообще? Зачем всё это нужно? Для ответа на эти вопросы попробуем инвертировать зависимости в одном эндпоинтe FastAPI.

Метафора инверсии зависимостей

Метафора инверсии зависимостей

Определение

Для начала стоит определиться с терминами. Классическая формулировка принципа звучит примерно так:

  1. Модули верхнего уровня не должны зависеть от модулей нижнего уровня. И те и другие должны зависеть от абстракций.

  2. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

Как и в случае со многими подобными принципами разработки, это определение звучит довольно абстрактно. Несколько терминов не до конца понятны. Что такое модули верхнего и нижнего уровня? И зачем в определении нужен второй пункт? Чтобы лучше с этим разобраться, рассмотрим одну практическую задачу.

Проблема

Для понимания сути принципа полезно посмотреть на какой-то код. В конце концов, всё это прежде всего именно про код. Посмотрим на следующую функцию1 обработки HTTP-запросов, написанную с использованием популярного фреймворка FastAPI.

@app.post("/tickets", status_code=201)
async def create_ticket(
    request: CreateTicketRequest,
    db: AsyncSession = Depends(get_db),
    http_client: httpx.AsyncClient = Depends(get_http_client),
) -> CreateTicketResponse:
    llm_response = await CLIENT.chat.completions.create(
        model="gpt-5-mini",
        messages=[
            {"role": "developer", "content": LLM_INSTRUCTIONS},
            {"role": "user", "content": request.message},
        ],
    )
    is_critical = llm_response.choices[0].message.content == "CRITICAL"
    ticket = Ticket(
        id=uuid4(),
        customer_email=request.customer_email,
        message=request.message,
        is_critical=is_critical,
    )
    db.add(ticket)
    await db.commit()
    if is_critical:
        await http_client.post(
            f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage",
            json={"chat_id": MANAGER_ID, "text": ticket.message},
        )
    return CreateTicketResponse(id=ticket.id)

Представим, что мы разрабатываем большое и сложное приложение для работы с обращениями пользователей в службу поддержки. Перед нами HTTP-обработчик, который принимает новые обращения. Сначала он определяет, является ли обращение критическим важным. Затем сохраняет его в базе данных. Наконец, если обращение критически важное, отправляет уведомление менеджеру в Telegram.

Что можно сказать об этом коде? Плох он или хорош? Ну, как минимум, этот код решает поставленную задачу и делает то, что от него требуется. Если бы нам было нужно лишь реализовать этот API-эндпоинт и больше никогда к нему не возвращаться, такой код, скорее всего, был бы вполне приемлем.

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

Связанность

Где же в этом примере находятся модули верхнего уровня из определения DIP? К таким модулям можно отнести код, который выражает основное назначение приложения, то есть бизнес-логику. Зачем вообще существует этот обработчик? Какую задачу он решает?

При первом чтении кода из примера ответы на эти вопросы могут быть неочевидны: приходится продираться через множество второстепенных технических деталей. Бизнес-логика – основное назначение приложения – «загрязнена» низкоуровневыми деталями: выполнением HTTP-запросов и обращениями к базе данных.

Но если вчитаться в код внимательнее, то можно выделить три основные операции:

  1. Определение приоритета обращения.

  2. Сохранение обращения в базу данных.

  3. Отправка уведомления, если требуется вмешательство.

Однако в текущей реализации высокоуровневый код сильно зависит от низкоуровневых механизмов работы с сетью и базой данных. Это напрямую нарушает принцип инверсии зависимостей.

Бизнес-логика зависит от низкоуровневых деталей

Бизнес-логика зависит от низкоуровневых деталей

Любое изменение низкоуровневого кода так или иначе затрагивает и бизнес-логику. Что, если нам потребуется перейти на другого провайдера LLM? Или отправлять уведомления по электронной почте? В любом из этих случаев придётся изменять код высокоуровневого модуля. Технические детали оказываются сильно связаны с кодом бизнес-уровня.

Давайте представим, что речь идёт не о простом примере из статьи на Хабре, а о сотне подобных API-эндпоинтов, которые должна поддерживать наша вымышленная команда. Проблема приобретает совершенно другой масштаб. Именно сильная связанность (high coupling) в долгосрочной перспективе «убивает» программные системы. Если ею не управлять, код приложения с очень большой вероятностью превратится в неподдерживаемый «большой ком грязи» (big ball of mud).

Тестируемость

Поскольку мы стремимся быть ответственными разработчиками, наш код нужно покрыть тестами. Посмотрим, какие у нас есть варианты. Первое, что бросается в глаза, – настолько связанный код тестировать довольно трудно. Он зависит от глобальных переменных, таких как CLIENT, и от объектов сторонних библиотек, например db.

Но, к счастью, мы пишем на Python и можем воспользоваться всей его магией (ведь все любят магию, правда?). Можно на полную задействовать моки и monkey patching:

# test_api.py
# Arrange
monkeypatch.setattr(
    api,
    "CLIENT",
    SimpleNamespace(
        chat=SimpleNamespace(
            completions=SimpleNamespace(
                create=AsyncMock(return_value=_llm_response("NORMAL"))
            )
        )
    ),
)
db = Mock()
db.commit = AsyncMock()
http_client = Mock()
http_client.post = AsyncMock()

А затем протестировать обработчик с помощью всей этой конструкции:

# test_api.py
# Act
request = api.CreateTicketRequest(
    customer_email="user@example.com",
    message="The export button is slightly misaligned.",
)
response = asyncio.run(
    api.create_ticket(request=request, db=db, http_client=http_client)
)

# Assert
db.add.assert_called_once()
db.commit.assert_awaited_once()
http_client.post.assert_not_awaited()

Замечательно. Почему бы просто не использовать все эти возможности модуля unittest.mock повсюду и не сэкономить время и силы? Проблема в том, что такие тесты очень хрупкие и сложные в сопровождении. Моков слишком много. Можно легко представить, что однажды бизнес-логика изменится, а тесты при этом продолжат радостно проходить. Да и вообще, что именно мы здесь проверяем: бизнес-логику приложения или работу моков?

Если для тестирования некоторого фрагмента кода приходится так активно применять моки и monkey patching, это часто указывает на более глубокие структурные проблемы и сильную связанность. Мы не можем изолировать код для тестирования и поэтому вынуждены искать обходные пути. К тому же в Python-сообществе чрезмерное использование магических моков и monkey patching обычно считается признаком плохого дизайна. Это отношение ярко выражает фраза: «Monkey patching – это банкротство ПО».

Другой вариант, который у нас есть для тестирования, – это полностью положиться на интеграционные и сквозные (end-to-end) тесты. Мы могли бы запустить базу данных в отдельном Docker-контейнере и обращаться к ней в каждом тесте. Могли бы даже поднять отдельный HTTP-сервер для обработки тестовых запросов. Технически это сработало бы. Но представим, что у нас сотни таких тестов. Каждому из них нужен доступ к базе данных, а значит, перед тестом и после него придётся подготавливать и очищать тестовые данные, готовить специальное состояние. Всё это сложно настраивать и поддерживать, а весь набор тестов каждый раз будет выполняться непозволительно долго, снижая мотивацию и производительность нашей команды. Кроме того, если обратиться к устоявшимся практикам в индустрии, окажется, что наша пирамида тестирования перевёрнута вверх ногами. Это тоже вряд ли хороший знак.

Перевёрнутая пирамида тестирования

Перевёрнутая пирамида тестирования

Изменения

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

Посмотрим, насколько легко в текущей реализации заменить зависимости или добавить новое поведение. Например, переход с OpenAI SDK на Anthropic SDK, как уже говорилось выше, потребовал бы изменений в модуле, который содержит бизнес-логику. Нам пришлось бы очень осторожно убирать старый связанный код и заменять его новым – практически хирургическая операция.

А что, если нам понадобится добавить мониторинг запросов к базе данных, чтобы анализировать их производительность? В текущем дизайне это могло бы выглядеть примерно так:

# async def create_ticket(...):
start = perf_counter()
ticket = Ticket(
    id=uuid4(),
    customer_email=request.customer_email,
    message=request.message,
    is_critical=is_critical,
)
db.add(ticket)
await db.commit()
end = perf_counter()
print("Total time:", end - start)
# ...

Каждый раз, когда мы вносим подобные низкоуровневые изменения в высокоуровневый модуль, вероятность случайно добавить ошибку в бизнес-логику становится выше. Каждое следующее изменение такого рода реализовать сложнее, чем предыдущее. Бизнес-логика всё сильнее и сильнее «загрязняется» посторонними техническими деталями, и сопровождать её становится всё труднее.

Понятность кода

Размывание бизнес-логики техническими деталями в нашем примере само по себе представляет довольно серьёзную проблему. Когда внутри одной единицы кода смешиваются разные уровни абстракции, такой код становится трудно читать и понимать (и, конечно, для этого тоже существует отдельный принцип).

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

DIP

Как принцип инверсии зависимостей может помочь решить все эти проблемы? Согласно DIP, низкоуровневые детали должны зависеть от высокоуровневых модулей, а не наоборот. Бизнес-логика не должна зависеть от инфраструктурных механизмов, но в текущем дизайне происходит именно это. Нам нужно каким-то образом развернуть направление зависимостей. Для этого попробуем изолировать бизнес-логику, введя абстракции, которые соответствуют нашей предметной задаче:

# core.py
class PriorityDetector(Protocol):
    async def detect(self, text: str) -> Priority: ...


class TicketRepository(Protocol):
    async def save(self, ticket: Ticket) -> None: ...


class Notifier(Protocol):
    async def notify(self, ticket: Ticket) -> None: ...


async def submit_ticket(
    customer_email: str,
    message: str,
    ticket_repository: TicketRepository,
    priority_detector: PriorityDetector,
    notifier: Notifier,
) -> Ticket:
    priority = await priority_detector.detect(message)
    ticket = Ticket(
        customer_email=customer_email,
        message=message,
        priority=priority,
    )
    await ticket_repository.save(ticket)
    if ticket.is_critical:
        await notifier.notify(ticket)
    return ticket

Здесь core.py – это модуль верхнего уровня, в котором бизнес-логика выражена через абстракции. В этом модуле нет ни OpenAI SDK, ни HTTP-запросов, ни запросов к базе данных. Функция submit_ticket представляет бизнес-логику и не зависит от инфраструктуры. Она работает с обычными Python-классами и зависит только от абстракций, как того требует первый пункт принципа.

Согласно DIP, низкоуровневые модули тоже должны зависеть от абстракций. В нашем случае можно создать отдельный модуль impl.py с конкретными классами, реализующими высокоуровневые протоколы. Например, определение приоритета обращения можно реализовать с помощью OpenAI SDK:

# impl.py
class OpenAiPriorityDetector(PriorityDetector):
    def __init__(self, api_key: str) -> None:
        self._client = AsyncOpenAI(api_key=api_key)

    async def detect(self, text: str) -> Priority:
        llm_instructions = (
            "Analyze the priority of the following support ticket. "
            "Respond with 'CRITICAL' or 'NORMAL'."
        )
        llm_response = await self._client.chat.completions.create(
            model="gpt-5-mini",
            messages=[
                {"role": "developer", "content": llm_instructions},
                {"role": "user", "content": text},
            ],
        )
        return (
            Priority.CRITICAL
            if llm_response.choices[0].message.content == "CRITICAL"
            else Priority.NORMAL
        )

TicketRepository и Notifier 2 можно реализовать аналогичным образом, используя любые подходящие технологии и библиотеки. Теперь зависимости инвертированы.

Инверсия зависимостей

Инверсия зависимостей

Обратите внимание: именно высокоуровневый модуль core.py «владеет» абстракциями. Они определены строго в терминах предметной области, а не технических механизмов. У нас нет протокола OpenAiClient или абстрактного класса TelegramClient. Детали зависят от абстракций, как того требует второй пункт принципа.

Связанность

По сути, мы «развязали» бизнес-логику и низкоуровневые детали. Теперь оба уровня нашего приложения гораздо проще изменять независимо друг от друга. Например, если будет нужно отправлять уведомления для всех обращений, а не только для критических, то такое изменение можно локально внести в функцию submit_ticket. Остальные части приложения останутся нетронутыми. Точно так же OpenAI SDK можно заменить на Anthropic SDK, просто реализовав новый класс, соответствующий протоколу PriorityDetector. Содержащий бизнес-логику модуль при этом вообще не изменится.

Тестируемость

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

Функция submit_ticket явно перечисляет все свои зависимости в сигнатуре. Поэтому для тестирования остаётся только реализовать несколько вспомогательных классов, которые обычно называют тестовыми дублёрами (test doubles):

# conftest.py
class FakePriorityDetector(PriorityDetector):
    CRITICAL_MESSAGE = "critical"

    async def detect(self, text: str) -> Priority:
        return (
            Priority.CRITICAL
            if text == self.CRITICAL_MESSAGE
            else Priority.NORMAL
        )


class FakeTicketRepository(TicketRepository):
    # ...


class FakeNotifier(Notifier):
    # ...

После этого модульный тест можно написать с использованием этих тестовых реализаций:

# test_core.py
async def test_submit_ticket_notifies_for_critical_ticket(
    ticket_repository: FakeTicketRepository,
    priority_detector: FakePriorityDetector,
    notifier: FakeNotifier,
) -> None:
    # Act
    ticket = await submit_ticket(
        customer_email="customer@example.com",
        message=FakePriorityDetector.CRITICAL_MESSAGE,
        ticket_repository=ticket_repository,
        priority_detector=priority_detector,
        notifier=notifier,
    )
    # Assert
    assert await ticket_repository.get(ticket.id) == ticket
    assert ticket.priority is Priority.CRITICAL
    notifier.assert_notification_sent()

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

Устойчивая пирамида тестирования

Устойчивая пирамида тестирования

Изменения

Благодаря слабой связанности добавлять новое поведение и изменять существующее стало гораздо проще. Замена одной реализации протокола на другую – например, уже упомянутый переход на Anthropic SDK – теперь почти тривиальна. Достаточно написать новый класс, реализующий соответствующий протокол.

Посмотрим ещё раз на пример с мониторингом запросов к базе данных. Теперь высокоуровневый код вообще не нужно изменять. Вместо этого мы можем добавить новое поведение, используя паттерн проектирования «Декоратор»:

class TicketRepositoryWithInstrumentation(TicketRepository):
    def __init__(self, inner: TicketRepository) -> None:
        self._inner = inner

    async def save(self, ticket: Ticket) -> None:
        start = perf_counter()
        await self._inner.save(ticket)
        end = perf_counter()
        print("Total time:", end - start)

Объект этого класса можно просто передать в высокоуровневую функцию submit_ticket в качестве аргумента. Что особенно важно, с точки зрения бизнес-логики ничего не изменилось: функция по-прежнему принимает какую-то реализацию протокола TicketRepository. Но фактически мы добавили в приложение новое поведение. Например, если потребуется добавить кэширование на уровне работы с базой данных, тот же приём можно повторить ещё раз с другим классом-декоратором.

Использование паттерна «Декоратор» для добавления новой функциональности

Использование паттерна «Декоратор» для добавления новой функциональности

Это также пример принципа открытости/закрытости (open–closed principle) в действии: мы добавляем новое поведение, не изменяя существующие классы и модули.

Понятность кода

После применения DIP проблема смешения разных уровней абстракции становится гораздо менее выраженной. Модули лучше отделены друг от друга. Бизнес-логика в модуле core.py содержит только высокоуровневый код. Низкоуровневые детали также выделены в отдельные модули. Читателю кода больше не нужно пробираться через лабиринт технических нюансов, чтобы понять бизнес-логику.

Можно построить всё приложение на основе одного простого, но строгого правила – правила направления зависимостей. В совокупности всё это снижает когнитивную нагрузку при чтении и понимании кода.

В качестве заключения

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

У всего есть своя цена. В случае DIP это дополнительные слои абстракций, больше файлов и больше формальностей. Если вы разрабатываете приложение в одиночку и не собираетесь поддерживать его в будущем, подобные принципы и абстракции могут оказаться не нужны. Просто используйте тот подход, который решает вашу задачу.

Но если над приложением работает команда из нескольких разработчиков, а срок его поддержки заранее не ограничен, ситуация меняется. В таком случае связанностью необходимо управлять. Нужно внимательно проектировать абстракции и бережно относиться к бизнес-логике. Иначе «большой ком грязи» всегда готов начать катиться.

Что ещё почитать

  1. Глава On Coupling and Abstractions из отличной книги Architecture Patterns with Python.

  2. Layers, Onions, Ports, Adapters: it’s all the same.

  3. DIP in the Wild.

  4. Increasing Cohesion in Go with Generic Decorators.

Примечания

  1. Полная версия кода доступна в репозитории на Github.

  2. В реальном приложении стоит рассмотреть использование паттерна Outbox для отправки уведомлений.

  3. Оригинальная англоязычная версия этой статьи опубликована в моём блоге.