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

推荐订阅源

博客园_首页
T
Threat Research - Cisco Blogs
GbyAI
GbyAI
Y
Y Combinator Blog
美团技术团队
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
博客园 - 【当耐特】
S
SegmentFault 最新的问题
IT之家
IT之家
Recent Announcements
Recent Announcements
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
阮一峰的网络日志
阮一峰的网络日志
T
The Blog of Author Tim Ferriss
Martin Fowler
Martin Fowler
Microsoft Azure Blog
Microsoft Azure Blog
V
Visual Studio Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
U
Unit 42
WordPress大学
WordPress大学
博客园 - Franky
L
LangChain Blog
人人都是产品经理
人人都是产品经理
小众软件
小众软件
博客园 - 叶小钗
罗磊的独立博客
酷 壳 – CoolShell
酷 壳 – CoolShell
大猫的无限游戏
大猫的无限游戏
云风的 BLOG
云风的 BLOG
Vercel News
Vercel News
雷峰网
雷峰网
腾讯CDC
Google DeepMind News
Google DeepMind News
博客园 - 三生石上(FineUI控件)
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Help Net Security
Help Net Security
C
Check Point Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
N
News and Events Feed by Topic
V2EX - 技术
V2EX - 技术
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Schneier on Security
Schneier on Security
博客园 - 聂微东
A
Arctic Wolf
H
Heimdal Security Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Recent Commits to openclaw:main
Recent Commits to openclaw:main
T
The Exploit Database - CXSecurity.com
C
Cyber Attacks, Cyber Crime and Cyber Security
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Google DeepMind News
Google DeepMind News

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

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет 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 миллионов точек без потерь
Чистая архитектура на практике: перестаём ломать сервис при каждом релизе
merra123 · 2026-05-13 · via Все публикации подряд на Хабре

Чистая архитектура на практике: перестаём ломать сервис при каждом релизе

Уровень сложностиСредний

Время на прочтение7 мин

Охват и читатели0

Туториал

Введение

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

Знакомо?

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

Разберём это на примерах. Примеры будут псевдореальные, иначе статья быстро превратится в книгу.

Посмотрите на функцию загрузки инвойса:

def upload_invoice(session: Session, base_path: str, invoice_id: UUID, content: bytes) -> str:
    file_path = f"{base_path}/{invoice_id}.pdf"

    with open(file_path, "wb") as f:
        f.write(content)

    invoice = session.query(InvoiceORM).filter(InvoiceORM.id == invoice_id).one()
    invoice.file_path = file_path
    invoice.status = "uploaded"

    session.commit()

    return file_path

Что тут не так, помимо отсутствия примитивов синхронизации?

Функция одновременно:

  • работает с файловой системой;

  • напрямую зависит от ORM;

Пока проект маленький - это кажется очень удобным. Но со временем любая инфрастуктурная задача начинает тянуть изменения через всё приложение.

Допустим, через некоторое время проект начинает расти и вам нужно переехать на S3 хранилище. Приходится писать еще одну функцию или еще хуже - переписывать старую:

def upload_invoice_s3(
    session: Session,
    s3: S3Client,
    bucket: str,
    key: str,
    invoice_id: UUID,
    content: bytes
) -> str:

    s3.put_object(Bucket=bucket, Key=key, Body=content)

    invoice = session.query(InvoiceORM).filter(InvoiceORM.id == invoice_id).one()
    invoice.file_key = key
    invoice.storage_type = "s3"
    invoice.status = "uploaded"

    session.commit()

    return f"//{bucket}.s3.amazonaws.com/{key}"

Но проблема уже глубже. Локальное файловое хранилище, скорее всего, используется по всему проекту:

  • где-то напрямую открываются файлы;

  • где-то собираются file_path;

  • где-то проверяется существование файлов;

  • где-то логика начинает зависеть от структуры директорий.

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

А на следующий день приходит задача:

Для локальной разработки нужно использовать файловую систему!

Получается, что инфраструктурная деталь начинает определять структуру бизнес кода.

Какое решение?

Если инфраструктурный компонент может меняться независимо от бизнес логики, имеет смысл вынести его за контракт. Бизнес-логика должна работать не с S3 или локальной директорией напрямую, а с абстракцией:

storage.save(key, content)
storage.get(key)

Тогда use case вообще не знает:

  • как он хранится;

  • какой SDK используется;

  • локальное хранилище или удаленное.

В production DI контейнере используется S3FileStorage, в dev DI контейнере - LocalFileStorage. Смена инфраструктуры превращается в изменение конфигурации, а не в рефакторинг всего приложения.

При этом важно понимать: Clean Architecture совсем не бесплатная абстракция, она:

  • увеличивает количество кода;

  • повышает порог входа;

  • усложняет навигацию по проекту;

  • требует командной дисциплины;

  • замедляет разработку небольших приложений.

Если у вас небольшой CRUD сервис, подобная архитектура может оказаться очень избыточной.

Пошаговое внедрение на практике

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

Контракты

Контракты - это граница между бизнес логикой и внешним миром. Здесь обычно живут:

  • интерфейсы

  • инфраструктурные input/output DTO;

  • инфраструктурные exceptions;

# contracts/files/storage.py
class IFileStorage(abc.ABC):
    @abc.abstractmethod
    def save(self, key: str, content: bytes) -> None: ...

    @abc.abstractmethod
    def get(self, key: str) -> bytes: ...

Важно понимать: интерфейс нужен не “на всякий случай”. Абстракция имеет смысл только тогда, когда компонент неустойчивый и существует несколько реализаций. Создавать интерфейс для каждого класса подряд - такой же анти паттерн, как и полное отсутствие абстракций.

Доменные модели

Доменная модель описывает бизнес-сущность и её правила.

# domain/invoice/entities.py
@dataclass
class Invoice:
    id: UUID
    user_id: UUID
    amount: Decimal
    status: InvoiceStatus

    @property
    def is_paid(self) -> bool:
        return self.status == InvoiceStatus.PAID

    @property
    def is_cancelled(self) -> bool:
    	return self.status == InvoiceStatus.CANCELLED

    def mark_uploaded(self) -> None:
        if self.is_cancelled:
            raise InvoiceCancelledError()
        self.status = InvoiceStatus.UPLOADED

Обратите внимание, домен ничего не знает про инфраструктуру, здесь нет ORM, SQL, pydantic, boto3, etc.

Use Cases

Use case - это описание конкретного бизнес-сценария.

# usecases/invoice/upload.py
class UploadInvoiceUseCase:
    def __init__(self, storage: IFileStorage, uow: IUoW):
        self.storage = storage
        self.uow = uow

    def execute(
        self,
        invoice_id: UUID,
        content: bytes,
    ) -> UploadInvoiceOutput:
        invoice = self.uow.invoice_gate.get_by_id(invoice_id)

        if invoice.is_cancelled:
            raise InvoiceCancelledError()

        key = f"invoices/{invoice_id}.pdf"

        self.storage.save(key, content)

        invoice.mark_uploaded()

        self.uow.invoice_gate.save(invoice)

        return UploadInvoiceOutput(
            invoice_id=invoice.id,
            uploaded=True,
        )

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

Адаптеры

Адаптеры - это инфраструктурный слой. Именно здесь находятся конкретные реализации контрактов: PostgresQL, Kafka, SMTP, HTTP, StripeGateway, etc.

# adapters/files/storage/s3.py
class S3FileStorage(IFileStorage):
    def __init__(self, s3: S3Client, bucket: str):
        self.s3 = s3
        self.bucket = bucket

    def save(self, key: str, content: bytes) -> None:
        # конкретная реализация

    def get(self, key: str) -> bytes:
        # конкретная реализация


# adapters/files/storage/local.py
class LocalFileStorage(IFileStorage):
    def __init__(self, base_path: str):
        self.base_path = base_path

    def save(self, key: str, content: bytes) -> None:
        # конкретная реализация

    def get(self, key: str) -> bytes:
        # конкретная реализация

Именно adapter знает:

  • как устроены библиотеки;

  • какие примитивы синхронизации использовать;

  • лимиты и особенности конкретного провайдера

  • как управлять транзакцией;

  • стратегии повторов, таймауты и как вести себя при деградации;

  • и т.д.

Слой представления

Presentation layer - самая внешняя часть системы. На практике это обычно transport handler: http api, cli, message broker consumer, etc.

Его задача принять запрос, преобразовать данные, вызвать юскейс и вернуть ответ:

# handlers/api/v1/invoice/routes.py
@router.post("/invoices/{invoice_id}/upload", response_model=UploadInvoiceResponse)
@inject
def upload_invoice(
    invoice_id: UUID,
    file: UploadFile,
    use_case: FromInjector[UploadInvoiceUseCase],
) -> UploadInvoiceResponse:
    result = use_case.execute(
        invoice_id=invoice_id,
        content=file.file.read(),
    )
    return UploadInvoiceResponse(
        invoice_id=result.invoice_id,
        uploaded=result.uploaded,
    )

Нюансы написания Use Cases на практике

На практике почти всегда хочется “упростить жизнь” и собрать весь сценарий в один большой execute(). Например: создать инвойс, загрузить файл, отправить евент, обновить статистику.

Сначала это выглядит удобно. Есть один вход, один метод, один “бизнес-процесс”.

Проблемы начинаются позже. Например:

  • появляется новый Actor, которому уведомления уже не нужны;

  • появляется новый Actor, которому нужен batch processing;

  • появляется новый Actor, которому нужна какая-то новая фича;

  • или вообще появляется новый транспортный слой, у которого есть зависимость от внешних callbacks;

И со временем монолитный use case начинает зависеть от контекста вызова.

Поэтому на практике я стараюсь придерживаться простого правила: один use case - один атомарный бизнес-процесс, т.е мы объединяем в use case шаги, которые не имеют смысла по отдельности с точки зрения бизнес контекста вне этой операции. Но это правило не является абсолютным, есть исключения.

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

  • use cases остаются переиспользуемыми;

  • они проще тестируются;

  • уменьшается связанность системы.

Также в большинстве случаев стоит избегать вызова одного use case из другого - это часто приводит к скрытой связности.

Если знаете другие подходы к написанию use cases, которые хорошо работают на практике, буду очень благодарен за ваш опыт!

Контракты и границы слоёв

Когда говорят про Clean Architecture, обычно фокусируются на направлении зависимостей: domain не зависит от infrastructure, use cases не знают про framework.

Но на практике этого недостаточно. В Clean Architecture важно контролировать не только направление зависимостей, но и то, какие контракты пересекают границы слоёв. Даже при формально правильных зависимостях инфраструктура всё равно может постепенно начать протекать внутрь системы.

Обычно это происходит незаметно. Сначала инфрастуктурные DTO начинают использоваться как результат выполнения use case, затем транспортный слой просто пробрасывает его дальше и всё работает, первый актор доволен - контракт идеально подходит под его сценарий.

Проблема появляется позже.

Появляется второй актор, которому этот же ответ уже не подходит:

  • часть полей лишняя;

  • формат не удобен;

  • нужны дополнительные данные;

  • структура ответа должна выглядеть иначе.

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

Формат ответа должен оставаться локальным для конкретного сценария, а адаптация под нужды конкретного актора - происходить там, где эта потребность возникает.

Заключение

Clean Architecture - это точно не обязательный стандарт для любого проекта. Если у вас небольшой CRUD сервис без сложных интеграций, подобная архитектура вполне может оказаться избыточной.

Проблемы обычно начинаются позже.

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

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

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

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