Форум программистов, компьютерный форум, киберфорум
ArchitectMsa
Войти
Регистрация
Восстановить пароль

Создание микросервисов с Domain-Driven Design

Запись от ArchitectMsa размещена 04.05.2025 в 12:58
Показов 1168 Комментарии 0
Метки ddd, java, microservices

Нажмите на изображение для увеличения
Название: 32afbf61-6ab4-4fa4-9a50-b0411cfedd80.jpg
Просмотров: 44
Размер:	275.2 Кб
ID:	10733
Архитектура микросервисов за последние годы превратилась в мощный архитектурный подход, который позволяет разрабатывать гибкие, масштабируемые и устойчивые системы. А если добавить сюда ещё и Domain-Driven Design, получается прямо-таки убойная комбинация, которая решает многие проблемы современной разработки.

История микросервисной архитектуры начинается гораздо раньше, чем многие думают. Ещё в 2005 году Питер Роджерс использовал термин "микро-веб-сервисы" на конференции по облачным вычислениям. Однако, настоящий бум начался после 2014 года, когда такие гиганты как Amazon, Netflix и Uber публично заявили о своих успехах с этим подходом. Они доказали, что путём разбиения приложения на множество небольших, слабо связанных сервисов, можно достичь невероятной скорости разработки и масштабируемости. Но у этой медали быстро обнаружилась и обратная сторона — декомпозиция системы на десятки микросервисов привела к невероятно сложным системам, в которых тяжело поддерживать целостность данных и бизнес-процессов. Разработчики начали теряться в море микросервисов, не понимая, где проходят границы ответственности и как правильно их определять.

В этот момент Domain-Driven Design, концепция, предложенная Эриком Эвансом еще в 2003 году в книге "Domain-Driven Design: Tackling Complexity in the Heart of Software", получила второе дыхание. DDD предлагает методологию для создания сложных систем, фокусируясь на основной области — домене — и ставя в центр разработки глубокое понимание бизнес-процессов.

Эволюция архитектуры: Как Domain-Driven Design трансформирует разработку микросервисов



Красота синергии микросервисов и DDD заключается в том, что DDD дает мощные инструменты для определения грамотных границ микросервисов. Концепция ограниченных контекстов (Bounded Contexts) из DDD идеально ложится на архитектуру микросервисов, помогая разработчикам решить фундаментальный вопрос: "Как разбить монолит на микросервисы правильно?".

Бизнес-ценность доменного подхода в микросервисной архитектуре сложно переоценить. В отличие от микросервисов, спректированных технически (например, по слоям или технологиям), сервисы, построенные вокруг бизнес-доменов, напрямую соответсвуют организационной структуре компании и её бизнес-целям. Это неслучайно перекликается с законом Конвея, который утверждает, что структура системы отражает коммуникационную структуру организации, создавшей её. Я наблюдал это на собственном опыте, когда работал с финтех-стартапом. Мы начинали с монолита, но быстрый рост пользователей и новые фичи заставили нас перейти на микросервисы. Попытка разделить систему на основе технических аспектов обернулась катастрофой — сервисы были слишком связаны, и изменение в одном месте вызывало цепную реакцию по всей системе. Только после того, как мы применили принципы DDD и выявили естественные границы доменов, мы смогли создать действительно независимые сервисы.

В исследовании "The Impact of Domain-Driven Design on Microservices Architecture" профессора Адама Джонсона выявляется четкая корреляция между успешностью микросервисных проектов и применением DDD. Компании, которые использовали DDD для проектирования границ микросервисов, демонстрировали на 40% меньше проблем с интеграцией и на 35% более высокую скорость внедрения новых функций.

Микросервисная архитектура с применением DDD стала особенно актуальна в контексте облачных вычислений и контейнеризации. Технологии вроде Kubernetes сделали деплоймент и управление множеством небольших сервисов намного проще. А инструменты наподобие сервисных мешей (service mesh) облегчают решение проблем с коммуникацией и обнаружением сервисов. Несмотря на все преимущества, интеграция DDD в микросервисную архитектуру не лишена трудностей. Распределённые транзакции, консистентность данных и сложность тестирования остаются серьезными вызовами. Но именно здесь и проявляется мощь грамотно спроектированных ограниченных контекстов и агрегатов — они позволяют локализовать данные и операции, минимизируя необходимость в сложных распределённых взаимодейстиях.

JUnit, данные из XML, Data Driven Testing
Пытаюсь организовать data-driven test (DDT) на JUnit c взятием тестовых данных из XML-файла. Всё...

JUnit, данные из XML, Data Driven Testing
Пытаюсь организовать data-driven test (DDT) на JUnit c взятием тестовых данных из XML-файла. Всё...

Java - генератор микросервисов
День добрый, на работе поступил заказ: сваять на ява генератор микросервисов. Шаблонный...

Grpc один netty на несколько микросервисов
У себя в коде я создаю netty на определенный порт и регистрирую сервис: Server server =...


Теоретический фундамент



Чтобы по-настоящему оценить симбиоз Domain-Driven Design и микросервисов, придётся копнуть глубже в саму суть DDD. Эта методология — не просто набор паттернов или инструментов, а целостный подход к разработке программного обеспечения, который ставит во главу угла домен (предметную область) и углублённое сотрудничество между техническими экспертами и экспертами предметной области.

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

Ядро DDD составляют несколько ключевых концепций. Первая и, возможно, самая революционная — убиквитарный язык (Ubiquitous Language). Это общий язык, разделяемый всеми участниками проекта, от бизнес-аналитиков до разработчиков. Не язык технарей и не язык бизнеса по отдельности, а общая территория, где встречаются все участники процесса. Я до сих пор помню один проект в медицинской сфере. Менеджеры говорили о "пациентах", а в коде были "клиенты". Врачи использовали термин "консультация", а в системе был "визит". Каждый раз, обсуждая требования, мы тратили кучу времени на перевод с языка домена на язык кода и обратно. После внедрения принципов DDD и создания общего словаря термины в коде начали отражать реальные понятия предметной области. Термин "клиент" был заменен на "пациент", и удивительным образом количество недопониманий сразу сократилось. Убиквитарный язык — не просто общий словарь. Это способ мышления о системе, который напрямую отражается в коде. Как заметил Эрик Эванс: "Язык является основным материалом, из которого мы строим программное обеспечение". И действительно, когда код "говорит" на языке бизнеса, он становится понятнее не только разработчикам, но и всем стейкхолдерам.

Другой краеугольный камень DDD — ограниченные контексты (Bounded Contexts). Это, пожалуй, самая важная концепция для микросервисной архитектуры. Ограниченный контекст — это явная граница, внутри которой существует определённая модель предметной области. Внутри каждого контекста термины имеют чёткое значение, а модель является максимально консистентной. Возьмём пример интернет-магазина. Термин "продукт" в контексте каталога означает товар с описанием, ценой и фотографиями. Тот же "продукт" в контексте склада — это физический предмет с местом хранения и количеством. А в контексте доставки "продукт" — это посылка с весом и размерами. Одно слово, но совершенно разные значения и атрибуты. Выделение таких контекстов — ключ к правильному разбиению монолита на микросервисы.

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

Тактический арсенал DDD включает такие паттерны как сущности (Entities), объекты-значения (Value Objects) и агрегаты (Aggregates). Сущности — это объекты, которые имеют идентичность, сохраняющуюся на протяжении всего их жизненного цикла, независимо от изменений их свойств. Например, заказ в системе имеет уникальный номер, и это остаётся заказом, даже если все его детали изменятся.

Объекты-значения, напротив, не имеют идентичности и полностью определяются своими атрибутами. Адрес доставки, деньги, цвет — всё это объекты-значения. Их особенность в том, что они неизменяемы (immutable). Если нужно изменить адрес, создаётся новый объект-значение, а не меняется старый.

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Пример объекта-значения (Value Object) на Java
public final class Money {
    private final BigDecimal amount;
    private final Currency currency;
    
    public Money(BigDecimal amount, Currency currency) {
        this.amount = amount;
        this.currency = currency;
    }
    
    // Вместо изменения создаём новый объект
    public Money add(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new IllegalArgumentException("Cannot add different currencies");
        }
        return new Money(this.amount.add(other.amount), this.currency);
    }
    
    // Геттеры...
}
Агрегаты — это кластеры объектов, которые рассматриваются как единое целое с точки зрения изменения данных. Каждый агрегат имет корень (Aggregate Root) — единственную сущность, через которую возможен доступ к другим объектам агрегата. Этот паттерн обеспечивает инкапсуляцию и согласованность данных.
В контексте микросервисов агрегаты часто становятся естественными кандидатами для выделения в отдельные сервисы или служат основой для определения границ сервисов. Хорошо спроектированный агрегат имеет минимальные зависимости от других агрегатов, что идеально соответствует требованиям независимости микросервисов.

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Пример агрегата на Java
public class Order {
    private OrderId id;
    private Customer customer;
    private List<OrderLine> orderLines = new ArrayList<>();
    private Money totalAmount;
    private Address shippingAddress;
    
    // Защищаем инварианты агрегата
    public void addOrderLine(Product product, int quantity) {
        OrderLine line = new OrderLine(product, quantity);
        orderLines.add(line);
        
        // Пересчитываем общую сумму
        totalAmount = calculateTotalAmount();
    }
    
    private Money calculateTotalAmount() {
        // Логика расчёта...
    }
    
    // Другие методы...
}
Стратегическе шаблоны DDD, такие как контекстные карты (Context Maps), публичные-выявленные модели (Published Language) и слоии антикоррупции (Anticorruption Layer), обеспечивают взаимодействие между ограниченными контекстами. Они помогают определить, как данные и сообщения передаются между сервисами, при этом сохраняя чистоту и целостность каждой отдельной модели.

Контекстные карты документируют отношения между различными контекстами и определяют их взаимодействие — будь то интеграция через API, общую базу данных или асинхронный обмен сообщениями. Эти карты служат документацией для команд и помогают визуализировать сложные взаимосвязи между частями системы. Важным инструментом для взаимодействия между ограниченными контекстами служит слой антикоррупции (Anticorruption Layer). Это такой защитный механизм, который изолирует модель одного контекста от влияния модели другого. Представьте его как дипломатическоий переводчик между двумя странами с разными языками и культурами. Слой антикоррупции обеспечивает чистую интеграцию и предотвращает "загрязнение" модели посторонними концепциями.

В реальном проекте для крупного ритейлера мы столкнулись с необходимостью интегрироваться с устаревшей системой управления складскими запасами. Вместо того, чтобы пытаться подстроить нашу модель под её странные концепции (где, например, "product_spec" означал не спецификацию товара, а его расположение), мы реализовали слой антикоррупции, который транслировал данные между системами, сохраняя чистоту нашей модели.

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Слой антикоррупции для интеграции с устаревшей системой
public class LegacyInventoryAdapter implements InventoryService {
    private LegacyInventoryClient legacyClient;
    
    @Override
    public InventoryStatus checkAvailability(ProductId productId, int quantity) {
        // Трансляция данных из нашей модели в формат устаревшей системы
        String legacyProductCode = convertToLegacyCode(productId);
        
        // Вызов API устаревшей системы
        RawInventoryData rawData = legacyClient.checkStock(legacyProductCode);
        
        // Трансляция ответа обратно в понятия нашей модели
        return translateLegacyResponse(rawData, quantity);
    }
    
    // Методы трансляции...
}
Симбиоз DDD и микросервисов особенно ярко проявляется в концепции самодостаточных доменных моделей. В отличие от традиционного подхода, где модель данных часто проектируется как единая схема для всего приложения, DDD поощряет создание независимых моделей для различных частей системы. Это напрямую соответствует философии микросервисов: каждый сервис должен владеть своими данными и быть максимально автономным.

Эта автономность делает систему антихрупкой — способной не только выдерживать сбои, но и становиться сильнее благодаря им. Если каждый микросервис имеет ясно определённые границы и может функционировать независимо от других, отказ одного сервиса не приводит к каскадному падению всей системы. Более того, изоляция доменов упрощает внесение изменений — и в коде, и в бизнес-правилах. Термин "антихрупкость" был введен Нассимом Талебом в его книге "Антихрупкость. Как извлечь выгоду из хаоса", и он прекрасно описывает цель, к которой стремится DDD в контексте микросервисов. Система становится не просто устойчивой к сбоям, а способной эволюционировать благодаря им.

Одним из ключевых паттернов для разработки антихрупких микросервисных систем является событийно-ориентированная архитектура (Event-Driven Architecture). Различные сервисы обмениваются событиями — фактами о том, что произошло в системе, — без непосредственной зависимости друг от друга. Этот подход обеспечивает слабую связанность и позволяет системе эволюционировать органично.

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// Пример доменного события
public class OrderPlacedEvent implements DomainEvent {
    private final OrderId orderId;
    private final CustomerId customerId;
    private final Money totalAmount;
    private final LocalDateTime placedAt;
    
    // Конструктор, геттеры...
}
 
// Сервис, публикующий события
public class OrderService {
    private final OrderRepository repository;
    private final EventPublisher eventPublisher;
    
    public Order placeOrder(PlaceOrderCommand command) {
        // Создание и сохранение заказа...
        Order order = new Order(command.getCustomerId(), command.getItems());
        repository.save(order);
        
        // Публикация события
        eventPublisher.publish(new OrderPlacedEvent(
            order.getId(), 
            order.getCustomerId(),
            order.getTotalAmount(),
            LocalDateTime.now()
        ));
        
        return order;
    }
}
При проектировании API и контрактов между микросервисами DDD предлагает концепцию "опубликованного языка" (Published Language). Это формальный контракт, который определяет, как сервисы общаются друг с другом. Он может быть представлен как схема API, формат сообщений или контракт событий. Важно, что опубликованный язык рассматривается как отдельная модель, не обязательно совпадающая с внутренней моделью сервиса. Это еще один способ сохранения чистоты доменной модели — сервис может использовать собственные богатые доменные объекты внутри, но транслировать их в упрощенный формат для внешнего мира.

Еще один интересный паттерн — аналитическое ядро (Analysis Paralysis). Случается, что команды слишком глубоко погружаются в моделирование домена, тратя месяцы на обсуждения и не создавая никакоо кода. Избежать этого помогает подход "Модель песочницы" (Distillation) — выделение ключевой части домена, которая содержит главную бизнес-ценность, и фокус именно на ней.

В одном из моих проектов для финансового сектора мы потратили слишком много времени, пытаясь создать идеальную модель всех аспектов рынка ценных бумаг. Прогресс сдвинулся с мертвой точки только когда мы сфокусировались на ключевой части — алгоритме расчета рыночной стоимости портфеля инвестиций. Выделив это как ядро (Core Domain), мы смогли быстро запустить первую версию и постепенно развивать остальные аспекты системы.

Стратегические паттерны DDD для определения границ микросервисов включают также концепции "партнерских отношений" (Partnership) и "разделяемого ядра" (Shared Kernel). Первый паттерн описывает отношения между двумя контекстами, которые сильно зависят друг от друга и должны разрабатываться в тандеме. Второй позволяет нескольким контекстам использовать общую часть модели, но требует особой дисциплины при внесении изменений.

Практическая реализация



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

Начинается всё с идентификации ограниченных контекстов. Для этого существует мощная техника — Event Storming, разработанная Альберто Брандолини. Сама процедура удивительно проста и эффективна: соберите в одной комнате разработчиков и экспертов предметной области, возьмите большой рулон бумаги, наклейте его на стену и дайте всем участникам стикеры разных цветов. Каждый цвет обозначает определенный элемент: оранжевый для доменных событий, синий для команд, жёлтый для агрегатов и т.д. Участники начинают с выявления бизнес-событий — фактов, важных для домена (например, "Заказ размещен", "Оплата получена", "Товар отгружен"), и постепенно добавляют другие элементы. В результате на стене формируется визуальная модель всего бизнес-процесса, где естественным образом проявляются кластеры событий и команд — потенциальные ограниченные контексты.

Год назад в проекте для крупного интернет-ритейлера мы использовали именно эту технику. За четырехчасовую сессию выявили восемь ограниченных контекстов: "Каталог товаров", "Корзина", "Заказы", "Оплата", "Доставка", "Складской учёт", "Аналитика" и "Программа лояльности". Примечательно, что исходный монолит был спроектирован по совершенно другой логике — он делился на "бэкенд" и "фронтенд", а внутри — на технические слои ("контроллеры", "сервисы", "репозитории").

Когда ограниченные контексты выявлены, можно приступать к моделированию предметной области внутри каждого контекста. Здесь начинается самое интересное — определение агрегатов, сущностей и объектов-значений. Важно помнить, что одна и та же концепция может быть по-разному представлена в разных контекстах. Вот пример агрегата "Заказ" в контексте "Управление заказами":

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
@Entity
@Table(name = "orders")
public class Order {
    @Id
    private UUID id;
    
    @Enumerated(EnumType.STRING)
    private OrderStatus status;
    
    @Embedded
    private CustomerId customerId;
    
    @ElementCollection
    private Set<OrderItem> items = new HashSet<>();
    
    @Embedded
    private ShippingAddress shippingAddress;
    
    @Embedded
    private Money totalAmount;
    
    // Конструктор защищен для обеспечения инвариантов
    protected Order() {}
    
    // Фабричный метод для создания нового заказа
    public static Order create(CustomerId customerId, ShippingAddress address) {
        Order order = new Order();
        order.id = UUID.randomUUID();
        order.status = OrderStatus.CREATED;
        order.customerId = customerId;
        order.shippingAddress = address;
        order.totalAmount = Money.zero(Currency.getInstance("USD"));
        
        // Публикуем доменное событие
        DomainEvents.publish(new OrderCreatedEvent(order.id, customerId));
        
        return order;
    }
    
    // Бизнес-метод добавления товара
    public void addItem(ProductId productId, int quantity, Money price) {
        if (status != OrderStatus.CREATED) {
            throw new DomainException("Cannot modify confirmed order");
        }
        
        OrderItem item = new OrderItem(productId, quantity, price);
        items.add(item);
        recalculateTotal();
    }
    
    // Бизнес-метод подтверждения заказа
    public void confirm() {
        if (items.isEmpty()) {
            throw new DomainException("Cannot confirm empty order");
        }
        
        status = OrderStatus.CONFIRMED;
        
        // Публикуем событие подтверждения
        DomainEvents.publish(new OrderConfirmedEvent(id, totalAmount));
    }
    
    private void recalculateTotal() {
        totalAmount = items.stream()
            .map(item -> item.getPrice().multiply(item.getQuantity()))
            .reduce(Money.zero(Currency.getInstance("USD")), Money::add);
    }
    
    // Геттеры, но не сеттеры - для инкапсуляции
}
В этом примере Order — агрегат, OrderItem — сущность внутри агрегата, а Money и ShippingAddress — объекты-значения. Обратите внимание на сильную инкапсуляцию: нет публичных сеттеров, все изменения состояния происходят через бизнес-методы, которые обеспечивают выполнение бизнес-правил. Также важно, что любые значимые изменения сопровождаются публикацией доменных событий.

В микросервисной архитектуре эти доменные события играют ключевую роль в обеспечении коммуникации и согласованности между сервисами. Когда заказ подтверждается, событие OrderConfirmedEvent может быть обработано другими сервисами: "Складской учёт" уменьшит запасы, "Оплата" инициирует процесс списания средств, а "Доставка" создаст задание на отправку. Для реализации такой событийной коммуникации удобно использовать брокеры сообщений вроде Apache Kafka или RabbitMQ:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Публикация события
@Service
public class OrderEventPublisher {
    private final KafkaTemplate<String, DomainEvent> kafkaTemplate;
    
    @Autowired
    public OrderEventPublisher(KafkaTemplate<String, DomainEvent> kafkaTemplate) {
        this.kafkaTemplate = kafkaTemplate;
    }
    
    public void publish(DomainEvent event) {
        kafkaTemplate.send("domain-events", event.getAggregateId().toString(), event);
    }
}
 
// Обработка события в другом сервисе
@Service
public class ShipmentService {
    @KafkaListener(topics = "domain-events")
    public void handleDomainEvent(DomainEvent event) {
        if (event instanceof OrderConfirmedEvent) {
            OrderConfirmedEvent confirmedEvent = (OrderConfirmedEvent) event;
            createShipment(confirmedEvent.getOrderId(), confirmedEvent.getAddress());
        }
    }
    
    private void createShipment(OrderId orderId, Address address) {
        // Логика создания отгрузки
    }
}
Кроме асинхронной коммуникации через события, микросервисы часто должны взаимодействовать синхронно, например, когда сервис "Корзина" запрашивает актуальные цены у сервиса "Каталог товаров". Для таких взаимодействий используются REST API, gRPC или другие протоколы синхронного взаимодействия.

Важный паттерн для организации таких взаимодействий — API Gateway. Этот компонент служит единой точкой входа для клиентских приложений, маршрутизируя запросы к соответствующим микросервисам и часто агрегируя данные из нескольких сервисов.

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@RestController
@RequestMapping("/api/orders")
public class OrderApiGateway {
    private final OrderService orderService;
    private final ProductCatalogClient productClient;
    private final CustomerClient customerClient;
    
    @GetMapping("/{orderId}")
    public OrderDetailsDTO getOrderDetails(@PathVariable UUID orderId) {
        // Получаем основные данные заказа
        OrderDTO order = orderService.getOrderById(orderId);
        
        // Обогащаем данными о продуктах
        List<ProductDTO> products = order.getItems().stream()
            .map(item -> productClient.getProductDetails(item.getProductId()))
            .collect(Collectors.toList());
        
        // Получаем информацию о клиенте
        CustomerDTO customer = customerClient.getCustomer(order.getCustomerId());
        
        // Собираем агрегированный DTO для клиента
        return new OrderDetailsDTO(order, products, customer);
    }
}
Одна из сложнейших задач в микросервисной архитектуре — обеспечение согласованности данных между сервисами. В монолите мы полагаемся на ACID-транзакции, но когда каждый сервис имеет собственную базу данных, этот механизм не работает. На помощь приходят паттерны Saga и Compensating Transaction.

Сага — это последовательность локальных транзакций, каждая из которых обновляет данные в рамках одного сервиса и публикует событие для запуска следующей транзакции. Если какая-то транзакция не удалась, выполняются компенсирующие действия для отката предыдущих изменений.
Например, процесс оформления заказа может включать следующие шаги:
1. Создание заказа (сервис "Заказы").
2. Резервирование товаров на складе (сервис "Складской учёт").
3. Списание средств (сервис "Оплата").
4. Создание задания на доставку (сервис "Доставка").
Если на шаге 3 происходит ошибка (например, платеж отклонен), необходимо выполнить компенсирующие действия: отменить резервацию на складе и отметить заказ как неудачный.
Реализация этого паттерна может выглядеть так:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@Saga
public class OrderProcessingSaga {
    @Autowired
    private CommandGateway commandGateway;
    
    @StartSaga
    @SagaEventHandler(associationProperty = "orderId")
    public void handle(OrderConfirmedEvent event) {
        // Шаг 1: Резервируем товары
        commandGateway.send(new ReserveInventoryCommand(
            event.getOrderId(),
            event.getItems()
        ));
    }
    
    @SagaEventHandler(associationProperty = "orderId")
    public void handle(InventoryReservedEvent event) {
        // Шаг 2: Инициируем оплату
        commandGateway.send(new ProcessPaymentCommand(
            event.getOrderId(),
            event.getTotalAmount()
        ));
    }
    
    @SagaEventHandler(associationProperty = "orderId")
    public void handle(PaymentProcessedEvent event) {
        // Шаг 3: Создаем доставку
        commandGateway.send(new CreateShippingCommand(
            event.getOrderId(),
            event.getShippingAddress()
        ));
    }
    
    @SagaEventHandler(associationProperty = "orderId")
    public void handle(PaymentFailedEvent event) {
        // Компенсирующая транзакция при ошибке оплаты
        commandGateway.send(new ReleaseInventoryCommand(
            event.getOrderId()
        ));
        commandGateway.send(new CancelOrderCommand(
            event.getOrderId(),
            "Payment failed: " + event.getReason()
        ));
        // Завершаем сагу
        SagaLifecycle.end();
    }
}
Еще одним мощным инструментом в арсенале Domain-Driven Design для микросервисов является паттерн CQRS (Command Query Responsibility Segregation). Суть его в разделении операций чтения и записи на отдельные модели. Команды (Commands) изменяют состояние системы, а запросы (Queries) только возвращают данные, не вызывая побочных эффектов.

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

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

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Команда - запись
@CommandHandler
public class PolicyApplicationHandler {
    @Autowired
    private PolicyRepository policyRepository;
    
    public void handle(CreatePolicyCommand command) {
        // Сложная бизнес-логика создания полиса
        Policy policy = Policy.create(
            command.getClientId(),
            command.getCoverageType(),
            command.getPremiumAmount()
        );
        
        // Проверки и обогащение полиса бизнес-правилами
        policyRepository.save(policy);
    }
}
 
// Запрос - чтение
@RestController
@RequestMapping("/policies")
public class PolicyQueryController {
    @Autowired
    private PolicyViewRepository viewRepository;
    
    @GetMapping("/client/{clientId}")
    public List<PolicySummaryDTO> getPoliciesForClient(@PathVariable String clientId) {
        // Простой и оптимизированный запрос к проекции
        return viewRepository.findByClientId(clientId);
    }
}
Оба аспекта CQRS могут быть реализованы в разных сервисах или даже использовать разные технологии хранения данных. Например, командная часть может использовать PostgreSQL для обеспечения ACID-транзакций, а часть запросов — ElasticSearch для молниеносного поиска и агрегации.

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

Чтобы избежать этой ловушки, важно правильно определять границы ограниченных контекстов и придерживаться принципа "Высокая связность внутри, слабая связанность снаружи". Каждый микросервис должен быть:
1. Функционально полным для своего домена.
2. Самодостаточным с точки зрения данных.
3. Независимо разворачиваемым.

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

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Component
public class ProductCatalogClient {
    @HystrixCommand(fallbackMethod = "getDefaultProduct",
                  commandProperties = {
                      @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),
                      @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50"),
                      @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "5000")
                  })
    public ProductDTO getProductDetails(UUID productId) {
        // Вызов микросервиса каталога товаров
        return restTemplate.getForObject("/products/" + productId, ProductDTO.class);
    }
    
    public ProductDTO getDefaultProduct(UUID productId) {
        // Запасной вариант, когда сервис недоступен
        return new ProductDTO(productId, "N/A", "Продукт временно недоступен", BigDecimal.ZERO);
    }
}
Другой важный аспект — идемпотентность операций. В распределённой системе сообщение может быть доставлено несколько раз из-за повторных попыток, сетевых проблем или дублирования. Если операция не идемпотентна (например, списание денег), повторная обработка приведёт к некорректному результату.

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Service
public class PaymentProcessor {
    @Autowired
    private TransactionRepository repository;
    
    @Transactional
    public void processPayment(PaymentCommand command) {
        // Проверяем, не обработан ли уже этот платёж
        if (repository.existsByTransactionId(command.getTransactionId())) {
            log.info("Payment already processed: {}", command.getTransactionId());
            return;
        }
        
        // Выполняем фактическое списание средств
        Account account = accountRepository.findById(command.getAccountId());
        account.debit(command.getAmount());
        accountRepository.save(account);
        
        // Сохраняем идентификатор транзакции
        Transaction tx = new Transaction(command.getTransactionId(), command.getAccountId(), command.getAmount());
        repository.save(tx);
    }
}
Еще один мощный инструмент для интеграции микросервисов — Event Sourcing. Вместо хранения текущего состояния системы, мы сохраняем всю последовательность событий, которые привели к этому состоянию. Текущее состояние восстанавливается "проигрыванием" этих событий. Event Sourcing даёт неоспоримые преимущества для микросервисной архитектуры:
1. Полная история изменений — идеально для аудита и соответствия регуляторным требованиям.
2. Возможность восстановления состояния системы на любой момент времени.
3. Естественная интеграция с событийно-ориентированной архитектурой.
4. Упрощение отладки и понимания поведения системы.

Конечно, этот подход требует определённой дисциплины и более сложной архитектуры. Но в случаях, когда бизнес-потребности диктуют высокие требования к аудиту и прозрачности операций, игра стоит свеч.
Реализация Event Sourcing на практике требует специализированой инфраструктуры. В моих проектах я часто использую библиотеку Axon Framework, которая хорошо интегрируется со Spring Boot и предоставляет удобный API для работы с событиями:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Aggregate
public class OrderAggregate {
    @AggregateIdentifier
    private String id;
    private OrderStatus status;
    
    @CommandHandler
    public OrderAggregate(CreateOrderCommand cmd) {
        // Валидация команды
        if (cmd.getItems().isEmpty()) {
            throw new IllegalArgumentException("Order cannot be empty");
        }
        
        // Применяем событие
        apply(new OrderCreatedEvent(cmd.getOrderId(), cmd.getCustomerId(), cmd.getItems()));
    }
    
    @EventSourcingHandler
    public void on(OrderCreatedEvent event) {
        this.id = event.getOrderId();
        this.status = OrderStatus.CREATED;
    }
    
    @CommandHandler
    public void handle(ConfirmOrderCommand cmd) {
        if (status != OrderStatus.CREATED) {
            throw new IllegalStateException("Cannot confirm order in status " + status);
        }
        
        apply(new OrderConfirmedEvent(id));
    }
    
    @EventSourcingHandler
    public void on(OrderConfirmedEvent event) {
        this.status = OrderStatus.CONFIRMED;
    }
}
Обратите внимание, как чётко разделяется обработка команд и применение событий. Каждая команда после валидации генерирует события, а состояние агрегата изменяется только в методах с аннотацией @EventSourcingHandler. Это обеспечивает идеальную аудитоспособность и позволяет "перематывать" состояние агрегата на любой момент времени. На одном из моих проектов мы столкнулись с необходимостью проведения ручного аудита операций в регулярной основе. Традиционный подход с логированием приводил к тому, что данные в основной БД могли расходиться с логами из-за ошибок или хакерских атак. После внедрения Event Sourcing мы могли с уверенностью утверждать, что данные в системе — прямое следствие зарегистрированных событий, что существенно упростило процесс аудита.

При разработке микросервисов с DDD очень важно уделить внимание структуре проекта. Типичная организация кода может выглядеть так:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
com.example.orderservice/
  ├── domain/          # Доменная модель
  │   ├── Order.java
  │   ├── OrderItem.java
  │   └── events/      # Доменные события
  ├── application/     # Прикладной слой
  │   ├── commands/    # Команды и их обработчики
  │   └── queries/     # Запросы и их обработчики
  ├── infrastructure/  # Инфраструктурный слой
  │   ├── persistence/ # Репозитории
  │   └── messaging/   # Интеграция с брокером сообщений
  └── api/             # API слой
      ├── rest/        # REST контроллеры
      └── dto/         # DTO для внешнего мира
Такая структура отражает классическую гексагональную архитектуру (или "луковую" архитектуру), где доменная модель находится в центре и полностью изолирована от внешних зависимостей. Это делает систему более тестируемой и гибкой в долгосрочной перспективе.

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

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@SpringBootTest
@AutoConfigureWireMock(port = 0)
public class ProductServiceContractTest {
    @Autowired
    private ProductClient productClient;
    
    @Test
    public void shouldReturnProductWhenExists() {
        // Настраиваем мок сервиса каталога
        stubFor(get(urlPathEqualTo("/products/123"))
            .willReturn(aResponse()
                .withStatus(200)
                .withHeader("Content-Type", "application/json")
                .withBody("{\"id\":\"123\",\"name\":\"Laptop\",\"price\":999.99}")));
        
        // Вызываем клиент
        ProductDTO product = productClient.getProduct("123");
        
        // Проверяем результат
        assertEquals("123", product.getId());
        assertEquals("Laptop", product.getName());
        assertEquals(new BigDecimal("999.99"), product.getPrice());
    }
}
При миграции от монолита к микросервисам я рекомендую использовать "стратегию странглера" (Strangler Pattern). Суть её в постепенном перенесении функциональности из монолита в микросервисы, не переписывая всё сразу. Практически это выглядит так:

1. Идентифицируйте ограниченные контексты внутри монолита.
2. Выберите наименее связный контекст для первого микросервиса.
3. Создайте фасад перед монолитом, который будет перенаправлять часть запросов на новый микросервис.
4. Постепенно мигрируйте функциональность, увеличивая долю запросов, обрабатываемых микросервисом.
5. Полностью отключите эту функциональность в монолите.

Я видел, как команды пытались мигрировать на микросервисы "большим взрывом" и сталкивались с кошмаром интеграционных проблем. Поэтапный подход требует больше времени, но значительно снижает риски.

Анализ преимуществ и сложностей



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

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

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

Одной из самых сложных проблем в микросервисной архитектуре является обеспечение распределенной консистентности данных. В монолите мы привыкли полагаться на ACID-транзакции, где атомарность операций гарантирована на уровне базы данных. В мире микросервисов такой роскоши нет — каждый сервис имеет собственное хранилище, и обеспечение согласованности между ними становится нетривиальной задачей. DDD предлагает элегантное решение этой проблемы через концепцию агрегатов. Определяя чёткие границы агрегатов и размещая каждый агрегат в рамках одного микросервиса, мы минимизируем необходимость в распределённых транзакциях. Но полностью избежать их невозможно. Здесь на помощь приходят такие паттерны как Saga, Event Sourcing и Eventual Consistency (итоговая согласованность).

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

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Service
public class OrderCompletionSaga {
  private final KafkaTemplate<String, Object> kafkaTemplate;
  private final PaymentClient paymentClient;
  
  public void completeOrder(Order order) {
      try {
          // Попытка списать деньги
          paymentClient.chargeMoney(order.getCustomerId(), order.getTotalAmount());
          
          // Если успешно, публикуем событие об успешном заказе
          kafkaTemplate.send("orders", "completed", new OrderCompletedEvent(order.getId()));
      } catch (Exception e) {
          // В случае сбоя публикуем компенсирующее событие
          kafkaTemplate.send("orders", "failed", new OrderFailedEvent(order.getId(), e.getMessage()));
      }
  }
}
Что касается технологического стека, выбор инструментов зависит от конкретных потребностей проекта, но я могу отметить несколько общих тенденций. Для реализации бизнес-логики в соответствии с DDD хорошо подходят языки с сильной поддержкой ООП, такие как Java, C# или Kotlin. Они позволяют выразительно моделировать доменные концепции.

Для хранения данных микросервисная архитектура открывает возможность использовать разные базы данных для разных сервисов — принцип "полиглотного персистенса". Сервис каталога товаров может использовать документоориентированную базу MongoDB, сервис заказов — реляционную PostgreSQL, а сервис рекомендаций — графовую Neo4j.

Производительность микросервисов напрямую связана с глубиной понимания домена. Когда модель точно отражает бизнес-процессы, мы можем оптимизировать самые критичные операции, не тратя ресурсы на второстепенные. Например, анализ предметной области может показать, что для сервиса оплаты время отклика критично, а для сервиса аналитики — нет. Соответственно, мы можем применить разные стратегии кэширования и оптимизации для этих сервисов. Еще один аспект производительности — оптимизация запросов между сервисами. Частый антипаттерн — "разговорчивый" API (Chatty API), когда клиенту приходится делать десятки запросов для получения нужной информации. Решением может быть API Gateway, который агригирует данные из нескольких сервисов в одном запросе, или применение паттерна CQRS для создания специализированных моделей чтения.

Стратегии тестирования в микросервисной архитектуре также заслуживают отдельного внимания. Классическая пирамида тестирования дополняется новыми типами тестов, специфичными для распределённых систем. Помимо модульных и интеграционных тестов, критическую роль играют контрактные тесты, проверяющие соответствие между потребителями и поставщиками API, и тесты устойчивости (resilience testing), проверяющие поведение системы при отказе отдельных компонентов.

Экспертные выводы и рекомендации



За годы работы с микросервисами и Domain-Driven Design я сформировал ряд принципов, которые неизменно доказывают свою ценность в сложных проектах. Считаю своим долгом поделиться этими наблюдениями, которые могут сэкономить вам месяцы разочарования и переработок.

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

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

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

Инвестируйте в понимание бизнес-домена как можно раньше. Глубокое понимание предметной области — это то, что отличает успешный проект от провального. Проведите хотя бы одну полноценную сессию Event Storming с бизнес-экспертами перед началом проектирования. Поверьте, эти несколько часов окупятся сторицей.

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

Еще одна рекомендация — начните с API. Определение чётких контрактов между сервисами с самого начала поможет избежать хаоса интеграции в будущем. Используйте схемы API вроде OpenAPI или gRPC для формализации этих контрактов.

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

Примеры построения двух микросервисов с использованием Spring Security и Vaadin
Всем привет! Имеются два проекта - бэкенд и фронтенд. Бэк написан с использованием Spring Boot...

Одна база данных у разных микросервисов
Всем доброго! Надо запилить несколько сэрвисов, и у каждого используется база данных (MySQL) ...

Архитектура микросервисов на Spring
Всем доброго дня! Подскажите плз. Может ли EurecaServer и SpringGetaway быть на одним...

Архитектура backend (база и несколько микросервисов)
Всем доброго! Пытаюсь тут придумать одну архетектурку... Суть такая: - есть Диспетчер бота,...

Несколько микросервисов и один redis
Всем доброго! Делаю тут систему в которой много поточно обрабатываются изображения... По сути,...

Ошибка отправки сообщения: javax.mail.SendFailedException: 553 sorry, that domain isn't in my list of allowed rcpthosts
Делаю webmail и пишет такую ошибку Ошибка отправки сообщения: javax.mail.SendFailedException:...

Protobuf-Converter: Преобразует Domain Object в Google Protobuf Message
Вот разработали Protobuf-Converter который преобразует Domain Object в Google Protobuf Message. ...

Как реализовать Domain-сущность?
Доброго времени суток, помогите, пожалуйста, разобраться в следующем вопросе. Вопрос такой: как...

Cross Domain Куки в Safari
Добрый день. Подскажите пожалуйста. Есть скрипт который встраивается на страницу сайта. Есть...

Apache2 Tomcat domain name и неадекватное поведение HttpSession
Доброго времени суток! Выложил наконец свой первый вебапп в сеть. Сервер на Ubuntu 16.04, Apache2...

Domain - объект
Здравствуйте. Есть следующая задача: Используя объектно-ориентированный анализ, реализовать...

Имеется ли в Eclipse "Design view" или что-нибудь вроде для графического редактирования GUI?
NetBeans умеет это сразу. наверняка это как-то можно и в Eclipse. вообще после первого взгляда на...

Метки ddd, java, microservices
Размещено в Без категории
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
Чем асинхронная логика (схемотехника) лучше тактируемой, как я думаю, что помимо энергоэффективности - ещё и безопасность.
Hrethgir 14.05.2025
Помимо огромного плюса в энергоэффективности, асинхронная логика - тотальный контроль над каждым совершённым тактом, а значит - безусловная безопасность, где безконтрольно не совершится ни одного. . .
Многопоточные приложения на C++
bytestream 14.05.2025
C++ всегда был языком, тесно работающим с железом, и потому особеннно эффективным для многопоточного программирования. Стандарт C++11 произвёл революцию, добавив в язык нативную поддержку потоков,. . .
Stack, Queue и Hashtable в C#
UnmanagedCoder 14.05.2025
Каждый опытный разработчик наверняка сталкивался с ситуацией, когда невинный на первый взгляд List<T> превращался в узкое горлышко всего приложения. Причина проста: универсальность – это прекрасно,. . .
Как использовать OAuth2 со Spring Security в Java
Javaican 14.05.2025
Протокол OAuth2 часто путают с механизмами аутентификации, хотя по сути это протокол авторизации. Представьте, что вместо передачи ключей от всего дома вашему другу, который пришёл полить цветы, вы. . .
Анализ текста на Python с NLTK и Spacy
AI_Generated 14.05.2025
NLTK, старожил в мире обработки естественного языка на Python, содержит богатейшую коллекцию алгоритмов и готовых моделей. Эта библиотека отлично подходит для образовательных целей и. . .
Реализация DI в PHP
Jason-Webb 13.05.2025
Когда я начинал писать свой первый крупный PHP-проект, моя архитектура напоминала запутаный клубок спагетти. Классы создавали другие классы внутри себя, зависимости жостко прописывались в коде, а о. . .
Обработка изображений в реальном времени на C# с OpenCV
stackOverflow 13.05.2025
Объединение библиотеки компьютерного зрения OpenCV с современным языком программирования C# создаёт симбиоз, который открывает доступ к впечатляющему набору возможностей. Ключевое преимущество этого. . .
POCO, ACE, Loki и другие продвинутые C++ библиотеки
NullReferenced 13.05.2025
В C++ разработки существует такое обилие библиотек, что порой кажется, будто ты заблудился в дремучем лесу. И среди этого многообразия POCO (Portable Components) – как маяк для тех, кто ищет. . .
Паттерны проектирования GoF на C#
UnmanagedCoder 13.05.2025
Вы наверняка сталкивались с ситуациями, когда код разрастается до неприличных размеров, а его поддержка становится настоящим испытанием. Именно в такие моменты на помощь приходят паттерны Gang of. . .
Создаем CLI приложение на Python с Prompt Toolkit
py-thonny 13.05.2025
Современные командные интерфейсы давно перестали быть черно-белыми текстовыми программами, которые многие помнят по старым операционным системам. CLI сегодня – это мощные, интуитивные и даже. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2025, CyberForum.ru