- Added comprehensive course structure with 11 modules - Included theoretical content, practical examples, and tasks - Multi-language code examples (Java, Rust, Node.js, Python) - Detailed analysis of architectural approaches and trade-offs - Complete caching strategies documentation - Architecture examples from real-world systems (Twitter, Netflix, Uber) - Structured navigation and learning path - Ready for educational use
198 KiB
Модуль 1: Основы архитектуры программного обеспечения
Содержание модуля
- Роль и обязанности Solution Architect
- Введение в архитектуру ПО
- Доменная модель и Domain-Driven Design
- Принципы проектирования
- Архитектурные стили и паттерны
- Качественные атрибуты
- Роль архитектора в команде
Роль и обязанности Solution Architect
Кто такой Solution Architect?
Solution Architect — это специалист, который проектирует комплексные технические решения для решения бизнес-задач. Он является мостом между бизнес-требованиями и технической реализацией.
Ключевые обязанности Solution Architect
1. Анализ и проектирование решений
- Анализ бизнес-требований и перевод их в технические решения
- Проектирование архитектуры с учетом всех ограничений и требований
- Выбор технологий и инструментов для реализации
- Создание технических спецификаций и архитектурной документации
2. Управление техническими решениями
- Принятие архитектурных решений (Architecture Decision Records - ADR)
- Оценка рисков и компромиссов технических решений
- Управление техническим долгом и эволюцией системы
- Обеспечение соответствия стандартам и лучшим практикам
3. Коммуникация и координация
- Взаимодействие со стейкхолдерами (бизнес, разработчики, DevOps, QA)
- Презентация архитектурных решений на разных уровнях
- Техническое лидерство и менторство команды
- Координация между командами разработки
Отличия от других типов архитекторов
Solution Architect против Enterprise Architect
| Аспект | Solution Architect | Enterprise Architect |
|---|---|---|
| Фокус | Конкретное решение/продукт | Вся организация |
| Масштаб | Один проект/система | Множество систем |
| Временной горизонт | 6-18 месяцев | 2-5 лет |
| Детализация | Высокая техническая детализация | Высокий уровень абстракции |
| Взаимодействие | Команды разработки | C-level, IT-директора |
Пример различий:
// Solution Architect - детальное техническое решение
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@Autowired
private OrderService orderService;
@Autowired
private PaymentService paymentService;
@PostMapping
public ResponseEntity<OrderResponse> createOrder(@RequestBody CreateOrderRequest request) {
// Детальная реализация бизнес-логики
Order order = orderService.createOrder(request);
PaymentResult payment = paymentService.processPayment(order);
if (payment.isSuccess()) {
return ResponseEntity.ok(OrderResponse.fromOrder(order));
} else {
throw new PaymentException("Payment failed");
}
}
}
// Enterprise Architect - высокоуровневая диаграмма
/*
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ E-commerce │ │ Payment │ │ Inventory │
│ Platform │◄──►│ Gateway │◄──►│ Management │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Customer │ │ Order │ │ Fulfillment │
│ Management │ │ Processing │ │ System │
└─────────────────┘ └─────────────────┘ └─────────────────┘
*/
Solution Architect против Technical Architect
| Аспект | Solution Architect | Technical Architect |
|---|---|---|
| Фокус | Бизнес-решение | Техническая реализация |
| Уровень | Системный | Компонентный |
| Взаимодействие | Бизнес + Технические команды | Только технические команды |
| Решения | Архитектурные паттерны, технологии | Алгоритмы, структуры данных |
Жизненный цикл проектирования архитектуры
1. Анализ требований (Requirements Analysis)
Входные данные:
- Бизнес-требования (Business Requirements)
- Функциональные требования (Functional Requirements)
- Нефункциональные требования (Non-Functional Requirements)
- Ограничения (Constraints)
Процесс анализа:
// Пример анализа требований
public class RequirementsAnalyzer {
public ArchitectureRequirements analyzeRequirements(BusinessRequirements businessReq) {
ArchitectureRequirements archReq = new ArchitectureRequirements();
// Анализ функциональных требований
analyzeFunctionalRequirements(businessReq, archReq);
// Анализ нефункциональных требований
analyzeNonFunctionalRequirements(businessReq, archReq);
// Анализ ограничений
analyzeConstraints(businessReq, archReq);
return archReq;
}
private void analyzeNonFunctionalRequirements(BusinessRequirements businessReq,
ArchitectureRequirements archReq) {
// Производительность
if (businessReq.getExpectedUsers() > 10000) {
archReq.addRequirement("HIGH_PERFORMANCE", "System must handle 10k+ concurrent users");
}
// Масштабируемость
if (businessReq.isGlobalDeployment()) {
archReq.addRequirement("SCALABILITY", "System must scale globally");
}
// Безопасность
if (businessReq.isFinancialData()) {
archReq.addRequirement("SECURITY", "Financial data must be encrypted");
}
}
}
2. Проектирование архитектуры (Architecture Design)
Этапы проектирования:
-
Высокоуровневое проектирование
- Определение основных компонентов
- Выбор архитектурного стиля
- Определение интерфейсов
-
Детальное проектирование
- Проектирование компонентов
- Выбор технологий
- Определение паттернов взаимодействия
-
Валидация архитектуры
- Проверка соответствия требованиям
- Анализ рисков
- Прототипирование критических компонентов
3. Документирование (Documentation)
Типы архитектурной документации:
# Solution Architecture Document
## 1. Executive Summary
- Краткое описание решения
- Ключевые преимущества
- Основные риски
## 2. Business Context
- Бизнес-цели
- Пользователи системы
- Ключевые процессы
## 3. Architecture Overview
- Высокоуровневая диаграмма
- Основные компоненты
- Технологический стек
## 4. Detailed Design
- Детальные диаграммы компонентов
- API спецификации
- Схемы данных
## 5. Non-Functional Requirements
- Производительность
- Безопасность
- Масштабируемость
- Надежность
## 6. Implementation Plan
- Этапы реализации
- Ресурсы
- Временные рамки
## 7. Risks and Mitigation
- Технические риски
- Планы по их устранению
4. Реализация и контроль (Implementation & Governance)
Роль Solution Architect в реализации:
- Техническое лидерство команды разработки
- Контроль качества архитектурных решений
- Управление изменениями в архитектуре
- Решение технических проблем
Нефункциональные требования (NFR) в фокусе Solution Architect
1. Масштабируемость (Scalability)
Определение: Способность системы обрабатывать увеличение нагрузки.
Метрики:
- Количество одновременных пользователей
- Объем обрабатываемых данных
- Количество транзакций в секунду
Примеры требований:
scalability:
concurrent_users: 10000
data_volume: "1TB per day"
transactions_per_second: 1000
response_time: "< 200ms for 95% of requests"
availability: "99.9%"
2. Надежность (Reliability)
Определение: Способность системы выполнять требуемые функции в заданных условиях.
Аспекты:
- Отказоустойчивость (Fault Tolerance)
- Восстановление после сбоев (Recovery)
- Предотвращение потери данных (Data Integrity)
3. Безопасность (Security)
Определение: Защита системы от угроз и несанкционированного доступа.
Компоненты:
- Аутентификация и авторизация
- Шифрование данных
- Защита от атак
- Соответствие стандартам
4. Производительность (Performance)
Определение: Скорость и эффективность выполнения операций.
Метрики:
- Время отклика (Response Time)
- Пропускная способность (Throughput)
- Использование ресурсов (Resource Utilization)
Ключевые навыки Solution Architect
Технические навыки
-
Архитектурные паттерны и стили
- Микросервисы, SOA, Event-driven
- CQRS, Event Sourcing, Saga
- API Gateway, Service Mesh
-
Облачные технологии
- AWS, Azure, GCP
- Containerization (Docker, Kubernetes)
- Serverless архитектура
-
Интеграционные технологии
- REST, GraphQL, gRPC
- Message Queues (RabbitMQ, Kafka)
- API Management
Мягкие навыки
-
Коммуникация
- Презентация технических решений
- Взаимодействие с бизнесом
- Техническое письмо
-
Лидерство
- Техническое лидерство
- Менторство
- Управление конфликтами
-
Аналитическое мышление
- Анализ требований
- Принятие решений
- Управление рисками
Карьерный путь Solution Architect
Уровни развития
-
Junior Solution Architect (2-3 года опыта)
- Помощь в проектировании решений
- Изучение архитектурных паттернов
- Работа под руководством Senior Architect
-
Solution Architect (3-7 лет опыта)
- Независимое проектирование решений
- Взаимодействие с бизнесом
- Техническое лидерство команды
-
Senior Solution Architect (7+ лет опыта)
- Сложные enterprise решения
- Архитектурная стратегия
- Менторство других архитекторов
-
Principal/Lead Solution Architect (10+ лет опыта)
- Архитектурная стратегия компании
- Управление архитектурной командой
- Влияние на технологическую стратегию
Введение в архитектуру ПО
Определение архитектуры ПО
Архитектура программного обеспечения — это структура системы, включающая:
- Компоненты системы и их взаимодействие
- Внешние интерфейсы
- Ограничения и требования
- Принципы проектирования
Ключевые характеристики архитектуры
- Структурность — архитектура определяет структуру системы
- Абстрактность — скрывает детали реализации
- Системность — рассматривает систему как целое
- Эволюционность — может изменяться со временем
Зачем нужна архитектура?
- Управление сложностью — разбиение сложной системы на понятные части
- Коммуникация — общий язык для обсуждения системы
- Планирование — основа для принятия технических решений
- Переиспользование — возможность использования компонентов в других проектах
Архитектурные решения
Архитектурное решение — это выбор, который:
- Влияет на структуру системы
- Имеет долгосрочные последствия
- Трудно изменить в будущем
- Влияет на качественные атрибуты
Примеры архитектурных решений:
1. Выбор между монолитом и микросервисами
// Монолитная архитектура
@SpringBootApplication
public class EcommerceApplication {
public static void main(String[] args) {
SpringApplication.run(EcommerceApplication.class, args);
}
}
// Все сервисы в одном приложении
@RestController
@RequestMapping("/api")
public class EcommerceController {
@Autowired
private UserService userService;
@Autowired
private OrderService orderService;
@Autowired
private PaymentService paymentService;
@PostMapping("/orders")
public ResponseEntity<Order> createOrder(@RequestBody CreateOrderRequest request) {
// Вся логика в одном месте
User user = userService.findById(request.getUserId());
Order order = orderService.createOrder(request);
PaymentResult payment = paymentService.processPayment(order);
return ResponseEntity.ok(order);
}
}
// Микросервисная архитектура
// User Service
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
return ResponseEntity.ok(userService.findById(id));
}
}
// Order Service
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@Autowired
private OrderService orderService;
@Autowired
private UserServiceClient userServiceClient;
@PostMapping
public ResponseEntity<Order> createOrder(@RequestBody CreateOrderRequest request) {
// Вызов внешнего сервиса
User user = userServiceClient.getUser(request.getUserId());
Order order = orderService.createOrder(request);
return ResponseEntity.ok(order);
}
}
2. Выбор базы данных (SQL vs NoSQL)
// SQL подход (PostgreSQL)
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name", nullable = false)
private String name;
@Column(name = "email", unique = true, nullable = false)
private String email;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
private List<Order> orders = new ArrayList<>();
// Сложные запросы с JOIN
@Query("SELECT u FROM User u JOIN u.orders o WHERE o.status = :status")
List<User> findUsersWithOrdersByStatus(@Param("status") OrderStatus status);
}
// NoSQL подход (MongoDB)
@Document(collection = "users")
public class User {
@Id
private String id;
private String name;
private String email;
private List<Order> orders = new ArrayList<>();
// Гибкая схема данных
private Map<String, Object> additionalProperties = new HashMap<>();
}
// Пример использования
@Service
public class UserService {
// SQL: ACID транзакции
@Transactional
public User createUserWithOrder(CreateUserRequest request) {
User user = new User(request.getName(), request.getEmail());
user = userRepository.save(user);
Order order = new Order(user.getId(), request.getOrderItems());
order = orderRepository.save(order);
return user;
}
// NoSQL: Документная модель
public User createUserWithOrder(CreateUserRequest request) {
User user = new User();
user.setName(request.getName());
user.setEmail(request.getEmail());
// Встраивание заказа в документ пользователя
Order order = new Order(request.getOrderItems());
user.getOrders().add(order);
return userRepository.save(user);
}
}
3. Выбор протокола коммуникации (REST vs GraphQL)
// REST API
@RestController
@RequestMapping("/api/users")
public class UserRestController {
@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
User user = userService.findById(id);
return ResponseEntity.ok(user);
}
@GetMapping("/{id}/orders")
public ResponseEntity<List<Order>> getUserOrders(@PathVariable Long id) {
List<Order> orders = orderService.findByUserId(id);
return ResponseEntity.ok(orders);
}
// Проблема: N+1 запросов
@GetMapping
public ResponseEntity<List<User>> getAllUsers() {
List<User> users = userService.findAll();
// Для каждого пользователя нужен отдельный запрос для заказов
users.forEach(user -> {
List<Order> orders = orderService.findByUserId(user.getId());
user.setOrders(orders);
});
return ResponseEntity.ok(users);
}
}
// GraphQL API
@Component
public class UserGraphQLResolver implements GraphQLQueryResolver {
public User getUser(Long id) {
return userService.findById(id);
}
public List<User> getAllUsers() {
return userService.findAll();
}
}
@Component
public class UserResolver implements GraphQLResolver<User> {
@Autowired
private OrderService orderService;
// Автоматическое разрешение заказов
public List<Order> orders(User user) {
return orderService.findByUserId(user.getId());
}
}
// GraphQL Schema
/*
type User {
id: ID!
name: String!
email: String!
orders: [Order!]!
}
type Query {
user(id: ID!): User
users: [User!]!
}
# Клиент может запросить только нужные поля
query {
users {
id
name
orders {
id
total
}
}
}
*/
4. Выбор стратегии кэширования
// Cache-Aside Pattern
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private RedisTemplate<String, User> redisTemplate;
private static final String CACHE_KEY_PREFIX = "user:";
private static final Duration CACHE_TTL = Duration.ofMinutes(30);
public User findById(Long id) {
String cacheKey = CACHE_KEY_PREFIX + id;
// 1. Проверяем кэш
User user = redisTemplate.opsForValue().get(cacheKey);
if (user != null) {
return user;
}
// 2. Загружаем из БД
user = userRepository.findById(id);
if (user != null) {
// 3. Сохраняем в кэш
redisTemplate.opsForValue().set(cacheKey, user, CACHE_TTL);
}
return user;
}
}
// Write-Through Pattern
@Service
public class UserService {
public User save(User user) {
// 1. Сохраняем в БД
user = userRepository.save(user);
// 2. Обновляем кэш
String cacheKey = CACHE_KEY_PREFIX + user.getId();
redisTemplate.opsForValue().set(cacheKey, user, CACHE_TTL);
return user;
}
}
// Write-Behind Pattern
@Service
public class UserService {
@Async
public CompletableFuture<User> saveAsync(User user) {
// 1. Обновляем кэш сразу
String cacheKey = CACHE_KEY_PREFIX + user.getId();
redisTemplate.opsForValue().set(cacheKey, user, CACHE_TTL);
// 2. Сохраняем в БД асинхронно
return CompletableFuture.supplyAsync(() -> {
return userRepository.save(user);
});
}
}
Доменная модель и Domain-Driven Design
Что такое домен?
Домен (Domain) — это предметная область, в которой работает приложение. Это сфера знаний, правил и процессов, которые определяют, как работает бизнес.
Ключевые характеристики домена:
- Бизнес-логика — правила и процессы, специфичные для предметной области
- Терминология — язык, используемый экспертами в данной области
- Ограничения — правила и ограничения, которые должны соблюдаться
- Процессы — последовательности действий, выполняемых в домене
Domain-Driven Design (DDD)
Domain-Driven Design — подход к разработке программного обеспечения, который фокусируется на моделировании предметной области.
Основные принципы DDD:
- Фокус на домене — архитектура отражает структуру домена
- Единый язык — общий язык между разработчиками и экспертами
- Модель-код соответствие — код отражает доменную модель
- Итеративное уточнение — постоянное улучшение понимания домена
Строительные блоки DDD
1. Entity (Сущность)
Entity — объект, который имеет уникальную идентичность и жизненный цикл.
// Пример Entity - Заказ
@Entity
public class Order {
@Id
private OrderId id;
private CustomerId customerId;
private OrderStatus status;
private Money totalAmount;
private List<OrderItem> items;
// Конструктор
public Order(OrderId id, CustomerId customerId) {
this.id = id;
this.customerId = customerId;
this.status = OrderStatus.DRAFT;
this.items = new ArrayList<>();
}
// Бизнес-методы
public void addItem(ProductId productId, Quantity quantity, Money unitPrice) {
if (status != OrderStatus.DRAFT) {
throw new IllegalStateException("Cannot modify confirmed order");
}
OrderItem item = new OrderItem(productId, quantity, unitPrice);
items.add(item);
recalculateTotal();
}
public void confirm() {
if (items.isEmpty()) {
throw new IllegalStateException("Cannot confirm empty order");
}
this.status = OrderStatus.CONFIRMED;
// Доменное событие
DomainEvents.raise(new OrderConfirmed(this.id));
}
private void recalculateTotal() {
this.totalAmount = items.stream()
.map(OrderItem::getSubtotal)
.reduce(Money.ZERO, Money::add);
}
// Геттеры
public OrderId getId() { return id; }
public CustomerId getCustomerId() { return customerId; }
public OrderStatus getStatus() { return status; }
public Money getTotalAmount() { return totalAmount; }
public List<OrderItem> getItems() { return Collections.unmodifiableList(items); }
}
2. Value Object (Объект-значение)
Value Object — объект, который определяется только своими атрибутами, без идентичности.
// Пример Value Object - Деньги
public class Money {
private final BigDecimal amount;
private final Currency currency;
public Money(BigDecimal amount, Currency currency) {
if (amount == null || amount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("Amount must be non-negative");
}
if (currency == null) {
throw new IllegalArgumentException("Currency cannot be null");
}
this.amount = amount.setScale(2, RoundingMode.HALF_UP);
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);
}
public Money multiply(BigDecimal factor) {
return new Money(this.amount.multiply(factor), this.currency);
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Money money = (Money) obj;
return Objects.equals(amount, money.amount) &&
Objects.equals(currency, money.currency);
}
@Override
public int hashCode() {
return Objects.hash(amount, currency);
}
// Геттеры
public BigDecimal getAmount() { return amount; }
public Currency getCurrency() { return currency; }
public static final Money ZERO = new Money(BigDecimal.ZERO, Currency.getInstance("USD"));
}
3. Aggregate (Агрегат)
Aggregate — кластер связанных объектов, которые рассматриваются как единое целое.
// Пример Aggregate - Заказ
public class Order {
private OrderId id;
private CustomerId customerId;
private OrderStatus status;
private Money totalAmount;
private List<OrderItem> items;
private List<DomainEvent> domainEvents;
// Конструктор и методы...
// Методы для работы с доменными событиями
public List<DomainEvent> getUncommittedEvents() {
return new ArrayList<>(domainEvents);
}
public void markEventsAsCommitted() {
domainEvents.clear();
}
private void raiseEvent(DomainEvent event) {
domainEvents.add(event);
}
}
// Корневой объект агрегата
public class OrderAggregate {
private Order order;
public OrderAggregate(OrderId id, CustomerId customerId) {
this.order = new Order(id, customerId);
}
public void addItem(ProductId productId, Quantity quantity, Money unitPrice) {
order.addItem(productId, quantity, unitPrice);
}
public void confirm() {
order.confirm();
}
public Order getOrder() {
return order;
}
}
4. Domain Service (Доменный сервис)
Domain Service — сервис, который содержит бизнес-логику, не принадлежащую ни одной сущности.
// Пример Domain Service - Расчет скидки
@Service
public class DiscountCalculationService {
public Money calculateDiscount(Order order, Customer customer) {
Money discount = Money.ZERO;
// Скидка за количество товаров
int totalItems = order.getItems().stream()
.mapToInt(item -> item.getQuantity().getValue())
.sum();
if (totalItems >= 10) {
discount = discount.add(order.getTotalAmount().multiply(new BigDecimal("0.1")));
}
// Скидка для VIP клиентов
if (customer.isVip()) {
discount = discount.add(order.getTotalAmount().multiply(new BigDecimal("0.05")));
}
// Скидка за праздники
if (isHolidayPeriod()) {
discount = discount.add(order.getTotalAmount().multiply(new BigDecimal("0.15")));
}
return discount;
}
private boolean isHolidayPeriod() {
LocalDate now = LocalDate.now();
return now.getMonth() == Month.DECEMBER && now.getDayOfMonth() >= 20;
}
}
5. Repository (Репозиторий)
Repository — интерфейс для доступа к агрегатам.
// Интерфейс репозитория
public interface OrderRepository {
void save(Order order);
Optional<Order> findById(OrderId id);
List<Order> findByCustomerId(CustomerId customerId);
List<Order> findByStatus(OrderStatus status);
void delete(OrderId id);
}
// Реализация репозитория
@Repository
public class JpaOrderRepository implements OrderRepository {
@PersistenceContext
private EntityManager entityManager;
@Override
public void save(Order order) {
entityManager.persist(order);
}
@Override
public Optional<Order> findById(OrderId id) {
Order order = entityManager.find(Order.class, id);
return Optional.ofNullable(order);
}
@Override
public List<Order> findByCustomerId(CustomerId customerId) {
return entityManager.createQuery(
"SELECT o FROM Order o WHERE o.customerId = :customerId", Order.class)
.setParameter("customerId", customerId)
.getResultList();
}
@Override
public List<Order> findByStatus(OrderStatus status) {
return entityManager.createQuery(
"SELECT o FROM Order o WHERE o.status = :status", Order.class)
.setParameter("status", status)
.getResultList();
}
@Override
public void delete(OrderId id) {
Order order = entityManager.find(Order.class, id);
if (order != null) {
entityManager.remove(order);
}
}
}
Bounded Context (Ограниченный контекст)
Bounded Context — граница, в которой определенная доменная модель является валидной.
Примеры Bounded Context:
- Order Management — управление заказами
- Customer Management — управление клиентами
- Inventory Management — управление складом
- Payment Processing — обработка платежей
// Bounded Context: Order Management
@BoundedContext("OrderManagement")
public class OrderContext {
// Агрегаты
public class Order { /* ... */ }
public class OrderItem { /* ... */ }
// Сервисы
public class OrderService { /* ... */ }
public class OrderValidationService { /* ... */ }
// Репозитории
public interface OrderRepository { /* ... */ }
}
// Bounded Context: Customer Management
@BoundedContext("CustomerManagement")
public class CustomerContext {
// Агрегаты
public class Customer { /* ... */ }
public class CustomerProfile { /* ... */ }
// Сервисы
public class CustomerService { /* ... */ }
public class CustomerRegistrationService { /* ... */ }
// Репозитории
public interface CustomerRepository { /* ... */ }
}
Ubiquitous Language (Единый язык)
Ubiquitous Language — общий язык между разработчиками и экспертами предметной области.
Пример единого языка для e-commerce:
// Термины домена
public enum OrderStatus {
DRAFT("Черновик"),
CONFIRMED("Подтвержден"),
SHIPPED("Отправлен"),
DELIVERED("Доставлен"),
CANCELLED("Отменен");
private final String description;
OrderStatus(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
// Доменные события
public class OrderConfirmed implements DomainEvent {
private final OrderId orderId;
private final LocalDateTime occurredOn;
public OrderConfirmed(OrderId orderId) {
this.orderId = orderId;
this.occurredOn = LocalDateTime.now();
}
public OrderId getOrderId() {
return orderId;
}
public LocalDateTime getOccurredOn() {
return occurredOn;
}
}
Event Storming
Event Storming — методология для изучения домена через события.
Процесс Event Storming:
- Доменные события — что происходит в системе
- Команды — что вызывает события
- Агрегаты — что обрабатывает команды
- Политики — бизнес-правила
- Внешние системы — интеграции
# Event Storming для E-commerce
## Доменные события
- OrderCreated (Заказ создан)
- OrderConfirmed (Заказ подтвержден)
- PaymentProcessed (Платеж обработан)
- OrderShipped (Заказ отправлен)
- OrderDelivered (Заказ доставлен)
## Команды
- CreateOrder (Создать заказ)
- ConfirmOrder (Подтвердить заказ)
- ProcessPayment (Обработать платеж)
- ShipOrder (Отправить заказ)
## Агрегаты
- Order (Заказ)
- Customer (Клиент)
- Product (Товар)
- Payment (Платеж)
## Политики
- Скидка за количество товаров
- VIP статус клиента
- Праздничные скидки
Антипаттерны DDD
1. Anemic Domain Model (Анемичная доменная модель)
Проблема: Доменные объекты содержат только данные без поведения.
// Плохо: Анемичная модель
public class Order {
private Long id;
private String customerId;
private String status;
private BigDecimal totalAmount;
// Только геттеры и сеттеры
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
// ...
}
// Хорошо: Богатая доменная модель
public class Order {
private OrderId id;
private CustomerId customerId;
private OrderStatus status;
private Money totalAmount;
// Бизнес-методы
public void addItem(ProductId productId, Quantity quantity, Money unitPrice) {
// Бизнес-логика
}
public void confirm() {
// Бизнес-логика
}
}
2. God Object (Божественный объект)
Проблема: Один объект содержит слишком много ответственности.
// Плохо: God Object
public class Order {
// Слишком много ответственности
public void addItem() { /* ... */ }
public void calculateTax() { /* ... */ }
public void sendEmail() { /* ... */ }
public void updateInventory() { /* ... */ }
public void processPayment() { /* ... */ }
}
// Хорошо: Разделение ответственности
public class Order {
public void addItem() { /* ... */ }
}
public class TaxCalculationService {
public Money calculateTax(Order order) { /* ... */ }
}
public class EmailService {
public void sendOrderConfirmation(Order order) { /* ... */ }
}
Интеграция DDD с архитектурой
1. Layered Architecture + DDD
// Presentation Layer
@RestController
public class OrderController {
@Autowired
private OrderApplicationService orderService;
@PostMapping("/orders")
public ResponseEntity<OrderResponse> createOrder(@RequestBody CreateOrderRequest request) {
OrderId orderId = orderService.createOrder(request);
return ResponseEntity.ok(new OrderResponse(orderId));
}
}
// Application Layer
@Service
public class OrderApplicationService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private OrderDomainService orderDomainService;
public OrderId createOrder(CreateOrderRequest request) {
Order order = new Order(
new OrderId(UUID.randomUUID()),
new CustomerId(request.getCustomerId())
);
for (OrderItemRequest itemRequest : request.getItems()) {
order.addItem(
new ProductId(itemRequest.getProductId()),
new Quantity(itemRequest.getQuantity()),
new Money(itemRequest.getUnitPrice(), Currency.USD)
);
}
orderRepository.save(order);
return order.getId();
}
}
// Domain Layer
public class Order {
// Доменная логика
}
// Infrastructure Layer
@Repository
public class JpaOrderRepository implements OrderRepository {
// Реализация репозитория
}
2. Hexagonal Architecture + DDD
// Domain Layer (внутренний круг)
public class Order {
// Доменная логика
}
public interface OrderRepository {
void save(Order order);
Optional<Order> findById(OrderId id);
}
// Application Layer
@Service
public class OrderService {
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
public void createOrder(Order order) {
// Применение доменных правил
orderRepository.save(order);
}
}
// Infrastructure Layer (внешний круг)
@Repository
public class JpaOrderRepository implements OrderRepository {
// Реализация через JPA
}
// Adapters
@RestController
public class OrderController {
// REST API
}
@Component
public class OrderEventHandler {
// Обработка событий
}
Практические рекомендации
1. Начните с доменных событий
// Сначала определите события
public class OrderCreated implements DomainEvent {
private final OrderId orderId;
private final CustomerId customerId;
private final LocalDateTime occurredOn;
// Конструктор, геттеры
}
// Затем создайте агрегаты
public class Order {
public void confirm() {
// Бизнес-логика
DomainEvents.raise(new OrderConfirmed(this.id));
}
}
2. Используйте Value Objects для примитивов
// Вместо String
public class CustomerId {
private final String value;
public CustomerId(String value) {
if (value == null || value.trim().isEmpty()) {
throw new IllegalArgumentException("Customer ID cannot be empty");
}
this.value = value;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
CustomerId that = (CustomerId) obj;
return Objects.equals(value, that.value);
}
@Override
public int hashCode() {
return Objects.hash(value);
}
}
3. Следите за границами агрегатов
// Правильно: Агрегат управляет только своими данными
public class Order {
private OrderId id;
private List<OrderItem> items;
public void addItem(ProductId productId, Quantity quantity, Money unitPrice) {
// Логика добавления товара в заказ
}
}
// Неправильно: Агрегат управляет чужими данными
public class Order {
public void updateProductInventory(ProductId productId, Quantity quantity) {
// Это не должно быть в Order агрегате
}
}
Заключение
Domain-Driven Design помогает создавать архитектуру, которая:
- Отражает бизнес-логику — код соответствует домену
- Улучшает коммуникацию — единый язык между командами
- Повышает качество — четкие границы и ответственности
- Упрощает тестирование — изолированная доменная логика
- Обеспечивает эволюцию — гибкая архитектура для изменений
DDD особенно эффективен для сложных бизнес-доменов, где важно точное понимание предметной области и создание архитектуры, которая может эволюционировать вместе с бизнесом.
Принципы проектирования
SOLID принципы
1. Single Responsibility Principle (SRP)
Принцип единственной ответственности
Класс должен иметь только одну причину для изменения
Пример нарушения:
// Нарушение SRP - класс делает слишком много
class UserManager {
private UserRepository userRepository;
private EmailService emailService;
private PasswordEncoder passwordEncoder;
private AuditService auditService;
public User createUser(CreateUserRequest request) {
// 1. Валидация данных
if (request.getEmail() == null || !isValidEmail(request.getEmail())) {
throw new ValidationException("Invalid email");
}
// 2. Хеширование пароля
String hashedPassword = passwordEncoder.encode(request.getPassword());
// 3. Создание пользователя
User user = new User(request.getName(), request.getEmail(), hashedPassword);
user = userRepository.save(user);
// 4. Отправка приветственного email
emailService.sendWelcomeEmail(user.getEmail());
// 5. Аудит
auditService.logUserCreation(user.getId());
return user;
}
public void updateUser(Long userId, UpdateUserRequest request) {
// Аналогично много ответственностей...
}
public void deleteUser(Long userId) {
// Аналогично много ответственностей...
}
}
Правильная реализация:
// Разделение ответственностей
class User {
private Long id;
private String name;
private String email;
private String passwordHash;
// Только данные пользователя и бизнес-логика
public void changePassword(String newPassword, PasswordEncoder encoder) {
this.passwordHash = encoder.encode(newPassword);
}
public boolean isEmailValid() {
return email != null && email.contains("@");
}
}
class UserValidator {
public void validateCreateRequest(CreateUserRequest request) {
if (request.getName() == null || request.getName().trim().isEmpty()) {
throw new ValidationException("Name is required");
}
if (request.getEmail() == null || !isValidEmail(request.getEmail())) {
throw new ValidationException("Invalid email");
}
if (request.getPassword() == null || request.getPassword().length() < 8) {
throw new ValidationException("Password must be at least 8 characters");
}
}
private boolean isValidEmail(String email) {
return email.matches("^[A-Za-z0-9+_.-]+@(.+)$");
}
}
class UserRepository {
public User save(User user) {
// Только операции с БД
return userRepository.save(user);
}
public Optional<User> findById(Long id) {
return userRepository.findById(id);
}
}
class UserNotificationService {
public void sendWelcomeEmail(String email) {
// Только отправка уведомлений
emailService.sendWelcomeEmail(email);
}
}
class UserAuditService {
public void logUserCreation(Long userId) {
// Только аудит
auditService.logUserCreation(userId);
}
}
// Координатор, который использует все сервисы
class UserService {
private UserValidator validator;
private UserRepository repository;
private UserNotificationService notificationService;
private UserAuditService auditService;
private PasswordEncoder passwordEncoder;
public User createUser(CreateUserRequest request) {
// 1. Валидация
validator.validateCreateRequest(request);
// 2. Создание пользователя
String hashedPassword = passwordEncoder.encode(request.getPassword());
User user = new User(request.getName(), request.getEmail(), hashedPassword);
// 3. Сохранение
user = repository.save(user);
// 4. Уведомления
notificationService.sendWelcomeEmail(user.getEmail());
// 5. Аудит
auditService.logUserCreation(user.getId());
return user;
}
}
2. Open/Closed Principle (OCP)
Принцип открытости/закрытости
Программные сущности должны быть открыты для расширения, но закрыты для модификации
Пример нарушения:
// Нарушение OCP - нужно модифицировать класс для добавления новых типов
class OrderProcessor {
public void processOrder(Order order) {
switch (order.getPaymentType()) {
case CREDIT_CARD:
processCreditCardPayment(order);
break;
case PAYPAL:
processPayPalPayment(order);
break;
case BANK_TRANSFER:
processBankTransferPayment(order);
break;
// При добавлении нового типа платежа нужно модифицировать этот класс
}
}
private void processCreditCardPayment(Order order) {
// логика обработки кредитной карты
}
private void processPayPalPayment(Order order) {
// логика обработки PayPal
}
private void processBankTransferPayment(Order order) {
// логика обработки банковского перевода
}
}
Правильная реализация:
// Соблюдение OCP - открыто для расширения, закрыто для модификации
interface PaymentProcessor {
boolean canProcess(PaymentType paymentType);
PaymentResult processPayment(PaymentRequest request);
}
class CreditCardProcessor implements PaymentProcessor {
@Override
public boolean canProcess(PaymentType paymentType) {
return paymentType == PaymentType.CREDIT_CARD;
}
@Override
public PaymentResult processPayment(PaymentRequest request) {
// логика обработки кредитной карты
return new PaymentResult(true, "Payment processed via credit card");
}
}
class PayPalProcessor implements PaymentProcessor {
@Override
public boolean canProcess(PaymentType paymentType) {
return paymentType == PaymentType.PAYPAL;
}
@Override
public PaymentResult processPayment(PaymentRequest request) {
// логика обработки PayPal
return new PaymentResult(true, "Payment processed via PayPal");
}
}
class CryptocurrencyProcessor implements PaymentProcessor {
@Override
public boolean canProcess(PaymentType paymentType) {
return paymentType == PaymentType.CRYPTOCURRENCY;
}
@Override
public PaymentResult processPayment(PaymentRequest request) {
// логика обработки криптовалюты
return new PaymentResult(true, "Payment processed via cryptocurrency");
}
}
// Основной класс не нужно модифицировать для добавления новых процессоров
class OrderProcessor {
private List<PaymentProcessor> processors;
public OrderProcessor(List<PaymentProcessor> processors) {
this.processors = processors;
}
public PaymentResult processOrder(Order order) {
PaymentProcessor processor = processors.stream()
.filter(p -> p.canProcess(order.getPaymentType()))
.findFirst()
.orElseThrow(() -> new UnsupportedPaymentTypeException(
"No processor found for payment type: " + order.getPaymentType()));
PaymentRequest request = new PaymentRequest(order.getAmount(), order.getPaymentDetails());
return processor.processPayment(request);
}
}
// Конфигурация процессоров
@Configuration
public class PaymentProcessorConfig {
@Bean
public List<PaymentProcessor> paymentProcessors() {
return Arrays.asList(
new CreditCardProcessor(),
new PayPalProcessor(),
new CryptocurrencyProcessor() // Новый процессор добавляется без модификации существующего кода
);
}
}
3. Liskov Substitution Principle (LSP)
Принцип подстановки Лисков
Объекты производного класса должны быть заменяемы объектами базового класса
Пример нарушения:
// Нарушение LSP
class Rectangle {
protected int width;
protected int height;
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
public int getArea() {
return width * height;
}
}
class Square extends Rectangle {
@Override
public void setWidth(int width) {
this.width = width;
this.height = width; // Нарушение LSP - изменяет поведение
}
@Override
public void setHeight(int height) {
this.height = height;
this.width = height; // Нарушение LSP - изменяет поведение
}
}
// Проблема: код, работающий с Rectangle, может сломаться с Square
public void testRectangle(Rectangle rectangle) {
rectangle.setWidth(5);
rectangle.setHeight(4);
assert rectangle.getArea() == 20; // Для Square это будет 16!
}
Правильная реализация:
// Соблюдение LSP
abstract class Shape {
public abstract int getArea();
}
class Rectangle extends Shape {
private int width;
private int height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
@Override
public int getArea() {
return width * height;
}
}
class Square extends Shape {
private int side;
public Square(int side) {
this.side = side;
}
public void setSide(int side) {
this.side = side;
}
@Override
public int getArea() {
return side * side;
}
}
// Теперь любой Shape можно заменить другим Shape
public void testShape(Shape shape) {
int area = shape.getArea();
// Логика работает корректно для любого типа Shape
}
4. Interface Segregation Principle (ISP)
Принцип разделения интерфейсов
Клиенты не должны зависеть от интерфейсов, которые они не используют
Пример нарушения:
// Нарушение ISP - слишком большой интерфейс
interface Worker {
void work();
void eat();
void sleep();
void code();
void design();
void test();
void deploy();
}
class Developer implements Worker {
@Override
public void work() { /* разработка */ }
@Override
public void eat() { /* еда */ }
@Override
public void sleep() { /* сон */ }
@Override
public void code() { /* кодирование */ }
@Override
public void design() { /* не используется */ }
@Override
public void test() { /* не используется */ }
@Override
public void deploy() { /* не используется */ }
}
class Designer implements Worker {
@Override
public void work() { /* работа */ }
@Override
public void eat() { /* еда */ }
@Override
public void sleep() { /* сон */ }
@Override
public void code() { /* не используется */ }
@Override
public void design() { /* дизайн */ }
@Override
public void test() { /* не используется */ }
@Override
public void deploy() { /* не используется */ }
}
Правильная реализация:
// Соблюдение ISP - разделение интерфейсов
interface Human {
void eat();
void sleep();
}
interface Worker extends Human {
void work();
}
interface Developer extends Worker {
void code();
}
interface Designer extends Worker {
void design();
}
interface Tester extends Worker {
void test();
}
interface DevOps extends Worker {
void deploy();
}
// Теперь каждый класс реализует только нужные ему интерфейсы
class JavaDeveloper implements Developer {
@Override
public void eat() { /* еда */ }
@Override
public void sleep() { /* сон */ }
@Override
public void work() { /* работа */ }
@Override
public void code() { /* Java кодирование */ }
}
class UIDesigner implements Designer {
@Override
public void eat() { /* еда */ }
@Override
public void sleep() { /* сон */ }
@Override
public void work() { /* работа */ }
@Override
public void design() { /* UI дизайн */ }
}
// Можно комбинировать интерфейсы
class FullStackDeveloper implements Developer, Designer {
@Override
public void eat() { /* еда */ }
@Override
public void sleep() { /* сон */ }
@Override
public void work() { /* работа */ }
@Override
public void code() { /* кодирование */ }
@Override
public void design() { /* дизайн */ }
}
5. Dependency Inversion Principle (DIP)
Принцип инверсии зависимостей
Модули высокого уровня не должны зависеть от модулей низкого уровня
Пример нарушения:
// Нарушение DIP - высокоуровневый модуль зависит от низкоуровневого
class EmailService {
public void sendEmail(String to, String subject, String body) {
// Прямая зависимость от конкретной реализации
SMTPClient smtpClient = new SMTPClient();
smtpClient.connect("smtp.gmail.com", 587);
smtpClient.send(to, subject, body);
smtpClient.disconnect();
}
}
class OrderService {
private EmailService emailService;
public OrderService() {
this.emailService = new EmailService(); // Жесткая зависимость
}
public void processOrder(Order order) {
// Обработка заказа
order.setStatus(OrderStatus.PROCESSED);
// Отправка уведомления
emailService.sendEmail(
order.getCustomerEmail(),
"Order Confirmation",
"Your order has been processed"
);
}
}
Правильная реализация:
// Соблюдение DIP - зависимость от абстракций
interface NotificationService {
void sendNotification(String recipient, String subject, String message);
}
interface EmailProvider {
void sendEmail(String to, String subject, String body);
}
class SMTPEmailProvider implements EmailProvider {
@Override
public void sendEmail(String to, String subject, String body) {
SMTPClient smtpClient = new SMTPClient();
smtpClient.connect("smtp.gmail.com", 587);
smtpClient.send(to, subject, body);
smtpClient.disconnect();
}
}
class SendGridEmailProvider implements EmailProvider {
@Override
public void sendEmail(String to, String subject, String body) {
SendGridClient client = new SendGridClient();
client.send(to, subject, body);
}
}
class EmailNotificationService implements NotificationService {
private EmailProvider emailProvider;
public EmailNotificationService(EmailProvider emailProvider) {
this.emailProvider = emailProvider; // Инъекция зависимости
}
@Override
public void sendNotification(String recipient, String subject, String message) {
emailProvider.sendEmail(recipient, subject, message);
}
}
class SMSNotificationService implements NotificationService {
private SMSProvider smsProvider;
public SMSNotificationService(SMSProvider smsProvider) {
this.smsProvider = smsProvider;
}
@Override
public void sendNotification(String recipient, String subject, String message) {
smsProvider.sendSMS(recipient, message);
}
}
// Высокоуровневый модуль зависит от абстракции
class OrderService {
private NotificationService notificationService;
public OrderService(NotificationService notificationService) {
this.notificationService = notificationService; // Инъекция зависимости
}
public void processOrder(Order order) {
// Обработка заказа
order.setStatus(OrderStatus.PROCESSED);
// Отправка уведомления через абстракцию
notificationService.sendNotification(
order.getCustomerEmail(),
"Order Confirmation",
"Your order has been processed"
);
}
}
// Конфигурация зависимостей
@Configuration
public class AppConfig {
@Bean
public EmailProvider emailProvider() {
return new SMTPEmailProvider(); // Можно легко заменить на SendGridEmailProvider
}
@Bean
public NotificationService notificationService(EmailProvider emailProvider) {
return new EmailNotificationService(emailProvider);
}
@Bean
public OrderService orderService(NotificationService notificationService) {
return new OrderService(notificationService);
}
}
Дополнительные принципы
DRY (Не повторяйся)
Не повторяйся
- Избегайте дублирования кода
- Выносите общую логику в отдельные функции/классы
- Используйте абстракции для переиспользования
Пример нарушения:
// Нарушение DRY - дублирование кода
class UserService {
public void createUser(String email, String password) {
if (email == null || email.isEmpty()) {
throw new IllegalArgumentException("Email cannot be null or empty");
}
if (!email.contains("@")) {
throw new IllegalArgumentException("Invalid email format");
}
if (password == null || password.length() < 8) {
throw new IllegalArgumentException("Password must be at least 8 characters");
}
// создание пользователя
}
public void updateUser(String email, String password) {
if (email == null || email.isEmpty()) {
throw new IllegalArgumentException("Email cannot be null or empty");
}
if (!email.contains("@")) {
throw new IllegalArgumentException("Invalid email format");
}
if (password == null || password.length() < 8) {
throw new IllegalArgumentException("Password must be at least 8 characters");
}
// обновление пользователя
}
}
Правильная реализация:
// Соблюдение DRY - выделение общей логики
class ValidationUtils {
public static void validateEmail(String email) {
if (email == null || email.isEmpty()) {
throw new IllegalArgumentException("Email cannot be null or empty");
}
if (!email.contains("@")) {
throw new IllegalArgumentException("Invalid email format");
}
}
public static void validatePassword(String password) {
if (password == null || password.length() < 8) {
throw new IllegalArgumentException("Password must be at least 8 characters");
}
}
}
class UserService {
public void createUser(String email, String password) {
ValidationUtils.validateEmail(email);
ValidationUtils.validatePassword(password);
// создание пользователя
}
public void updateUser(String email, String password) {
ValidationUtils.validateEmail(email);
ValidationUtils.validatePassword(password);
// обновление пользователя
}
}
KISS (Будь проще)
Делай проще
- Простые решения часто лучше сложных
- Избегайте преждевременной оптимизации
- Читаемость кода важнее "умности"
Пример нарушения:
// Нарушение KISS - излишняя сложность
class ComplexCalculator {
public double calculate(String expression) {
if (expression == null || expression.trim().isEmpty()) {
throw new IllegalArgumentException("Expression cannot be null or empty");
}
String trimmedExpression = expression.trim();
if (!trimmedExpression.matches("^[0-9+\\-*/\\s()]+$")) {
throw new IllegalArgumentException("Invalid characters in expression");
}
// Сложная логика парсинга и вычисления...
return evaluateComplexExpression(trimmedExpression);
}
private double evaluateComplexExpression(String expr) {
// Очень сложная реализация
return 0.0;
}
}
Правильная реализация:
// Соблюдение KISS - простое решение
class SimpleCalculator {
public double add(double a, double b) {
return a + b;
}
public double subtract(double a, double b) {
return a - b;
}
public double multiply(double a, double b) {
return a * b;
}
public double divide(double a, double b) {
if (b == 0) {
throw new IllegalArgumentException("Division by zero");
}
return a / b;
}
}
// Использование
SimpleCalculator calc = new SimpleCalculator();
double result = calc.add(calc.multiply(2, 3), calc.divide(10, 2)); // 2*3 + 10/2 = 11
YAGNI (Вам это не понадобится)
Тебе это не понадобится
- Не реализуйте функциональность "на будущее"
- Добавляйте функции только когда они действительно нужны
- Избегайте over-engineering
Пример нарушения:
// Нарушение YAGNI - преждевременная оптимизация
class UserRepository {
private Map<String, User> cache = new ConcurrentHashMap<>();
private List<User> users = new ArrayList<>();
private UserIndexer indexer = new UserIndexer();
private UserValidator validator = new UserValidator();
private UserMetricsCollector metrics = new UserMetricsCollector();
public void saveUser(User user) {
// Валидация (может не понадобиться)
validator.validate(user);
// Индексация (может не понадобиться)
indexer.index(user);
// Кэширование (может не понадобиться)
cache.put(user.getId(), user);
// Сохранение
users.add(user);
// Метрики (может не понадобиться)
metrics.recordSave(user);
}
}
Правильная реализация:
// Соблюдение YAGNI - только необходимая функциональность
class UserRepository {
private List<User> users = new ArrayList<>();
public void saveUser(User user) {
if (user == null) {
throw new IllegalArgumentException("User cannot be null");
}
users.add(user);
}
public User findById(String id) {
if (id == null) {
throw new IllegalArgumentException("ID cannot be null");
}
return users.stream()
.filter(u -> u.getId().equals(id))
.findFirst()
.orElse(null);
}
}
// Когда понадобится кэширование - добавим
// Когда понадобится валидация - добавим
// Когда понадобится индексация - добавим
Принципы модульности
Слабая связанность (Loose Coupling)
- Компоненты минимально зависят друг от друга
- Изменения в одном компоненте не влияют на другие
- Легче тестировать и поддерживать
Пример нарушения:
// Нарушение - тесная связанность
class OrderService {
private MySQLDatabase database;
private SMTPEmailService emailService;
private FileLogger logger;
public OrderService() {
this.database = new MySQLDatabase("localhost", "orders_db");
this.emailService = new SMTPEmailService("smtp.gmail.com", 587);
this.logger = new FileLogger("/var/log/orders.log");
}
public void processOrder(Order order) {
// Прямая зависимость от конкретных реализаций
database.save(order);
emailService.sendEmail(order.getCustomerEmail(), "Order processed");
logger.log("Order processed: " + order.getId());
}
}
Правильная реализация:
// Соблюдение - слабая связанность
interface OrderRepository {
void save(Order order);
}
interface NotificationService {
void sendNotification(String recipient, String message);
}
interface LoggingService {
void log(String message);
}
class OrderService {
private OrderRepository repository;
private NotificationService notificationService;
private LoggingService loggingService;
public OrderService(OrderRepository repository,
NotificationService notificationService,
LoggingService loggingService) {
this.repository = repository;
this.notificationService = notificationService;
this.loggingService = loggingService;
}
public void processOrder(Order order) {
repository.save(order);
notificationService.sendNotification(
order.getCustomerEmail(),
"Order processed"
);
loggingService.log("Order processed: " + order.getId());
}
}
// Конкретные реализации
class MySQLOrderRepository implements OrderRepository {
@Override
public void save(Order order) {
// MySQL специфичная логика
}
}
class EmailNotificationService implements NotificationService {
@Override
public void sendNotification(String recipient, String message) {
// Email специфичная логика
}
}
class FileLoggingService implements LoggingService {
@Override
public void log(String message) {
// File logging специфичная логика
}
}
Высокая сплоченность (High Cohesion)
- Элементы внутри модуля тесно связаны
- Модуль имеет четко определенную ответственность
- Все функции модуля работают для достижения одной цели
Пример нарушения:
// Нарушение - низкая сплоченность
class UtilityClass {
public void sendEmail(String to, String subject, String body) {
// Отправка email
}
public void calculateTax(double amount, double rate) {
// Расчет налога
}
public void validatePassword(String password) {
// Валидация пароля
}
public void formatDate(Date date, String format) {
// Форматирование даты
}
public void compressFile(String filePath) {
// Сжатие файла
}
}
Правильная реализация:
// Соблюдение - высокая сплоченность
class EmailService {
public void sendEmail(String to, String subject, String body) {
validateEmail(to);
String formattedBody = formatEmailBody(body);
deliverEmail(to, subject, formattedBody);
}
private void validateEmail(String email) {
// Валидация email адреса
}
private String formatEmailBody(String body) {
// Форматирование тела письма
return body;
}
private void deliverEmail(String to, String subject, String body) {
// Доставка email
}
}
class TaxCalculator {
public double calculateTax(double amount, double rate) {
validateAmount(amount);
validateRate(rate);
return amount * rate;
}
private void validateAmount(double amount) {
if (amount < 0) {
throw new IllegalArgumentException("Amount cannot be negative");
}
}
private void validateRate(double rate) {
if (rate < 0 || rate > 1) {
throw new IllegalArgumentException("Rate must be between 0 and 1");
}
}
}
class PasswordValidator {
public boolean isValid(String password) {
return hasMinimumLength(password) &&
containsUppercase(password) &&
containsLowercase(password) &&
containsDigit(password);
}
private boolean hasMinimumLength(String password) {
return password.length() >= 8;
}
private boolean containsUppercase(String password) {
return password.chars().anyMatch(Character::isUpperCase);
}
private boolean containsLowercase(String password) {
return password.chars().anyMatch(Character::isLowerCase);
}
private boolean containsDigit(String password) {
return password.chars().anyMatch(Character::isDigit);
}
}
Архитектурные стили и паттерны
Архитектурные стили
1. Монолитная архитектура
Определение: Вся система реализована как единое приложение
Преимущества:
- Простота разработки и развертывания
- Легкое тестирование
- Простая отладка
- Низкая сложность сети
Недостатки:
- Сложность масштабирования
- Технологическая привязка
- Сложность обновлений
- Риск каскадных отказов
Когда использовать:
- Небольшие команды
- Простые приложения
- Быстрое прототипирование
Технический пример:
// Монолитное приложение - все в одном проекте
@SpringBootApplication
public class ECommerceApplication {
public static void main(String[] args) {
SpringApplication.run(ECommerceApplication.class, args);
}
}
// Все контроллеры в одном приложении
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;
@PostMapping
public ResponseEntity<User> createUser(@RequestBody User user) {
return ResponseEntity.ok(userService.createUser(user));
}
@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
return ResponseEntity.ok(userService.getUser(id));
}
}
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@Autowired
private OrderService orderService;
@PostMapping
public ResponseEntity<Order> createOrder(@RequestBody Order order) {
return ResponseEntity.ok(orderService.createOrder(order));
}
@GetMapping("/{id}")
public ResponseEntity<Order> getOrder(@PathVariable Long id) {
return ResponseEntity.ok(orderService.getOrder(id));
}
}
@RestController
@RequestMapping("/api/products")
public class ProductController {
@Autowired
private ProductService productService;
@PostMapping
public ResponseEntity<Product> createProduct(@RequestBody Product product) {
return ResponseEntity.ok(productService.createProduct(product));
}
@GetMapping("/{id}")
public ResponseEntity<Product> getProduct(@PathVariable Long id) {
return ResponseEntity.ok(productService.getProduct(id));
}
}
// Все сервисы в одном приложении
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public User createUser(User user) {
return userRepository.save(user);
}
public User getUser(Long id) {
return userRepository.findById(id).orElse(null);
}
}
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private UserService userService; // Прямая зависимость между сервисами
public Order createOrder(Order order) {
User user = userService.getUser(order.getUserId());
if (user == null) {
throw new IllegalArgumentException("User not found");
}
return orderRepository.save(order);
}
public Order getOrder(Long id) {
return orderRepository.findById(id).orElse(null);
}
}
// Единая база данных
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
// геттеры и сеттеры
}
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long userId;
private BigDecimal amount;
// геттеры и сеттеры
}
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private BigDecimal price;
// геттеры и сеттеры
}
2. Микросервисная архитектура
Определение: Система состоит из множества небольших независимых сервисов
Преимущества:
- Независимое развертывание
- Технологическое разнообразие
- Масштабируемость
- Отказоустойчивость
Недостатки:
- Сложность сети
- Проблемы с данными
- Сложность мониторинга
- Overhead коммуникации
Когда использовать:
- Большие команды
- Сложные домены
3. Слоистая архитектура (Layered Architecture)
Определение: Система организована в горизонтальные слои, где каждый слой может использовать только слои ниже себя.
Слои:
- Presentation Layer — пользовательский интерфейс
- Business Layer — бизнес-логика
- Data Access Layer — работа с данными
- Database Layer — хранение данных
Преимущества:
- Четкое разделение ответственности
- Легкость тестирования
- Переиспользование компонентов
- Простота понимания
Недостатки:
- Производительность (много слоев)
- Сложность при изменении требований
- Потенциальные проблемы с производительностью
Технические примеры:
Java (Spring Boot):
// Presentation Layer
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;
@PostMapping
public ResponseEntity<UserDto> createUser(@RequestBody CreateUserRequest request) {
UserDto user = userService.createUser(request);
return ResponseEntity.ok(user);
}
}
// Business Layer
@Service
@Transactional
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private EmailService emailService;
public UserDto createUser(CreateUserRequest request) {
// Бизнес-логика валидации
validateUserRequest(request);
// Создание пользователя
User user = new User();
user.setName(request.getName());
user.setEmail(request.getEmail());
user.setCreatedDate(LocalDateTime.now());
// Сохранение через Data Access Layer
User savedUser = userRepository.save(user);
// Бизнес-логика уведомления
emailService.sendWelcomeEmail(savedUser.getEmail());
return mapToDto(savedUser);
}
private void validateUserRequest(CreateUserRequest request) {
if (request.getName() == null || request.getName().trim().isEmpty()) {
throw new ValidationException("Name is required");
}
if (request.getEmail() == null || !isValidEmail(request.getEmail())) {
throw new ValidationException("Valid email is required");
}
}
}
// Data Access Layer
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
List<User> findByCreatedDateBetween(LocalDateTime start, LocalDateTime end);
}
// Database Layer (Entity)
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(unique = true, nullable = false)
private String email;
@CreationTimestamp
private LocalDateTime createdDate;
// геттеры и сеттеры
}
Rust (Actix Web + Diesel):
// Presentation Layer
use actix_web::{web, HttpResponse, Result};
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
pub struct CreateUserRequest {
pub name: String,
pub email: String,
}
#[derive(Serialize)]
pub struct UserDto {
pub id: i32,
pub name: String,
pub email: String,
pub created_date: chrono::DateTime<chrono::Utc>,
}
// Business Layer
pub struct UserService {
user_repository: Box<dyn UserRepository>,
email_service: Box<dyn EmailService>,
}
impl UserService {
pub fn new(user_repository: Box<dyn UserRepository>, email_service: Box<dyn EmailService>) -> Self {
Self {
user_repository,
email_service,
}
}
pub fn create_user(&self, request: CreateUserRequest) -> Result<UserDto, UserError> {
// Бизнес-логика валидации
self.validate_user_request(&request)?;
// Создание пользователя
let user = User {
id: 0, // Will be set by database
name: request.name,
email: request.email,
created_date: chrono::Utc::now(),
};
// Сохранение через Data Access Layer
let saved_user = self.user_repository.save(user)?;
// Бизнес-логика уведомления
self.email_service.send_welcome_email(&saved_user.email)?;
Ok(UserDto {
id: saved_user.id,
name: saved_user.name,
email: saved_user.email,
created_date: saved_user.created_date,
})
}
fn validate_user_request(&self, request: &CreateUserRequest) -> Result<(), UserError> {
if request.name.trim().is_empty() {
return Err(UserError::ValidationError("Name is required".to_string()));
}
if !self.is_valid_email(&request.email) {
return Err(UserError::ValidationError("Valid email is required".to_string()));
}
Ok(())
}
fn is_valid_email(&self, email: &str) -> bool {
email.contains('@') && email.contains('.')
}
}
// Data Access Layer
pub trait UserRepository {
fn save(&self, user: User) -> Result<User, UserError>;
fn find_by_email(&self, email: &str) -> Result<Option<User>, UserError>;
fn find_by_created_date_between(&self, start: chrono::DateTime<chrono::Utc>, end: chrono::DateTime<chrono::Utc>) -> Result<Vec<User>, UserError>;
}
pub struct DieselUserRepository {
connection: diesel::PgConnection,
}
impl UserRepository for DieselUserRepository {
fn save(&self, user: User) -> Result<User, UserError> {
use crate::schema::users::dsl::*;
use diesel::prelude::*;
diesel::insert_into(users)
.values(&user)
.get_result(&self.connection)
.map_err(|e| UserError::DatabaseError(e.to_string()))
}
fn find_by_email(&self, email: &str) -> Result<Option<User>, UserError> {
use crate::schema::users::dsl::*;
use diesel::prelude::*;
users
.filter(email.eq(email))
.first(&self.connection)
.optional()
.map_err(|e| UserError::DatabaseError(e.to_string()))
}
fn find_by_created_date_between(&self, start: chrono::DateTime<chrono::Utc>, end: chrono::DateTime<chrono::Utc>) -> Result<Vec<User>, UserError> {
use crate::schema::users::dsl::*;
use diesel::prelude::*;
users
.filter(created_date.between(start, end))
.load(&self.connection)
.map_err(|e| UserError::DatabaseError(e.to_string()))
}
}
// Database Layer (Model)
use diesel::prelude::*;
#[derive(Queryable, Insertable, Serialize, Deserialize)]
#[table_name = "users"]
pub struct User {
pub id: i32,
pub name: String,
pub email: String,
pub created_date: chrono::DateTime<chrono::Utc>,
}
// Error handling
#[derive(Debug)]
pub enum UserError {
ValidationError(String),
DatabaseError(String),
EmailError(String),
}
impl std::fmt::Display for UserError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
UserError::ValidationError(msg) => write!(f, "Validation error: {}", msg),
UserError::DatabaseError(msg) => write!(f, "Database error: {}", msg),
UserError::EmailError(msg) => write!(f, "Email error: {}", msg),
}
}
}
impl std::error::Error for UserError {}
// Email Service trait
pub trait EmailService {
fn send_welcome_email(&self, email: &str) -> Result<(), UserError>;
}
Python (FastAPI + SQLAlchemy):
# Presentation Layer
from fastapi import FastAPI, HTTPException, Depends
from pydantic import BaseModel, EmailStr
from typing import Optional
from datetime import datetime
app = FastAPI()
class CreateUserRequest(BaseModel):
name: str
email: EmailStr
class UserDto(BaseModel):
id: int
name: str
email: str
created_date: datetime
# Business Layer
class UserService:
def __init__(self, user_repository: 'UserRepository', email_service: 'EmailService'):
self.user_repository = user_repository
self.email_service = email_service
def create_user(self, request: CreateUserRequest) -> UserDto:
# Бизнес-логика валидации
self.validate_user_request(request)
# Создание пользователя
user = User(
name=request.name,
email=request.email,
created_date=datetime.utcnow()
)
# Сохранение через Data Access Layer
saved_user = self.user_repository.save(user)
# Бизнес-логика уведомления
self.email_service.send_welcome_email(saved_user.email)
return UserDto(
id=saved_user.id,
name=saved_user.name,
email=saved_user.email,
created_date=saved_user.created_date
)
def validate_user_request(self, request: CreateUserRequest) -> None:
if not request.name or not request.name.strip():
raise ValueError("Name is required")
if not self.is_valid_email(request.email):
raise ValueError("Valid email is required")
def is_valid_email(self, email: str) -> bool:
return '@' in email and '.' in email
# Data Access Layer
from abc import ABC, abstractmethod
from sqlalchemy import create_engine, Column, Integer, String, DateTime
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session
Base = declarative_base()
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, nullable=False)
email = Column(String, unique=True, nullable=False, index=True)
created_date = Column(DateTime, default=datetime.utcnow)
class UserRepository(ABC):
@abstractmethod
def save(self, user: User) -> User:
pass
@abstractmethod
def find_by_email(self, email: str) -> Optional[User]:
pass
@abstractmethod
def find_by_created_date_between(self, start: datetime, end: datetime) -> list[User]:
pass
class SQLAlchemyUserRepository(UserRepository):
def __init__(self, db: Session):
self.db = db
def save(self, user: User) -> User:
self.db.add(user)
self.db.commit()
self.db.refresh(user)
return user
def find_by_email(self, email: str) -> Optional[User]:
return self.db.query(User).filter(User.email == email).first()
def find_by_created_date_between(self, start: datetime, end: datetime) -> list[User]:
return self.db.query(User).filter(
User.created_date.between(start, end)
).all()
# Email Service
class EmailService(ABC):
@abstractmethod
def send_welcome_email(self, email: str) -> None:
pass
class SMTPEmailService(EmailService):
def __init__(self, smtp_server: str, smtp_port: int, username: str, password: str):
self.smtp_server = smtp_server
self.smtp_port = smtp_port
self.username = username
self.password = password
def send_welcome_email(self, email: str) -> None:
# Implementation for sending email
print(f"Sending welcome email to {email}")
# Dependency injection
def get_user_service(db: Session = Depends(get_db)) -> UserService:
user_repository = SQLAlchemyUserRepository(db)
email_service = SMTPEmailService("smtp.gmail.com", 587, "user", "password")
return UserService(user_repository, email_service)
# API endpoints
@app.post("/api/users", response_model=UserDto)
def create_user(request: CreateUserRequest, user_service: UserService = Depends(get_user_service)):
try:
return user_service.create_user(request)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail="Internal server error")
@app.get("/api/users/{user_id}", response_model=UserDto)
def get_user(user_id: int, user_service: UserService = Depends(get_user_service)):
user = user_service.get_user(user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
Node.js (Express + TypeScript + Prisma):
import express, { Request, Response, NextFunction } from 'express';
import { PrismaClient } from '@prisma/client';
import { z } from 'zod';
import nodemailer from 'nodemailer';
const app = express();
const prisma = new PrismaClient();
// Presentation Layer
interface CreateUserRequest {
name: string;
email: string;
}
interface UserDto {
id: number;
name: string;
email: string;
createdDate: Date;
}
// Business Layer
class UserService {
constructor(
private userRepository: UserRepository,
private emailService: EmailService
) {}
async createUser(request: CreateUserRequest): Promise<UserDto> {
// Бизнес-логика валидации
this.validateUserRequest(request);
// Создание пользователя
const user = {
name: request.name,
email: request.email,
createdDate: new Date()
};
// Сохранение через Data Access Layer
const savedUser = await this.userRepository.save(user);
// Бизнес-логика уведомления
await this.emailService.sendWelcomeEmail(savedUser.email);
return {
id: savedUser.id,
name: savedUser.name,
email: savedUser.email,
createdDate: savedUser.createdDate
};
}
private validateUserRequest(request: CreateUserRequest): void {
if (!request.name || !request.name.trim()) {
throw new Error('Name is required');
}
if (!this.isValidEmail(request.email)) {
throw new Error('Valid email is required');
}
}
private isValidEmail(email: string): boolean {
return email.includes('@') && email.includes('.');
}
}
// Data Access Layer
interface UserRepository {
save(user: Omit<UserDto, 'id'>): Promise<UserDto>;
findByEmail(email: string): Promise<UserDto | null>;
findByCreatedDateBetween(start: Date, end: Date): Promise<UserDto[]>;
}
class PrismaUserRepository implements UserRepository {
async save(user: Omit<UserDto, 'id'>): Promise<UserDto> {
const savedUser = await prisma.user.create({
data: {
name: user.name,
email: user.email,
createdDate: user.createdDate
}
});
return {
id: savedUser.id,
name: savedUser.name,
email: savedUser.email,
createdDate: savedUser.createdDate
};
}
async findByEmail(email: string): Promise<UserDto | null> {
const user = await prisma.user.findUnique({
where: { email }
});
if (!user) return null;
return {
id: user.id,
name: user.name,
email: user.email,
createdDate: user.createdDate
};
}
async findByCreatedDateBetween(start: Date, end: Date): Promise<UserDto[]> {
const users = await prisma.user.findMany({
where: {
createdDate: {
gte: start,
lte: end
}
}
});
return users.map(user => ({
id: user.id,
name: user.name,
email: user.email,
createdDate: user.createdDate
}));
}
}
// Email Service
interface EmailService {
sendWelcomeEmail(email: string): Promise<void>;
}
class SMTPEmailService implements EmailService {
private transporter = nodemailer.createTransporter({
host: 'smtp.gmail.com',
port: 587,
secure: false,
auth: {
user: 'user@gmail.com',
pass: 'password'
}
});
async sendWelcomeEmail(email: string): Promise<void> {
const mailOptions = {
from: 'user@gmail.com',
to: email,
subject: 'Welcome!',
text: 'Welcome to our service!'
};
await this.transporter.sendMail(mailOptions);
}
}
// Dependency injection
function getUserService(): UserService {
const userRepository = new PrismaUserRepository();
const emailService = new SMTPEmailService();
return new UserService(userRepository, emailService);
}
// Validation schemas
const createUserSchema = z.object({
name: z.string().min(1, 'Name is required'),
email: z.string().email('Valid email is required')
});
// API endpoints
app.post('/api/users', async (req: Request, res: Response, next: NextFunction) => {
try {
const userService = getUserService();
const validatedData = createUserSchema.parse(req.body);
const user = await userService.createUser(validatedData);
res.json(user);
} catch (error) {
if (error instanceof z.ZodError) {
res.status(400).json({ error: error.errors });
} else {
next(error);
}
}
});
app.get('/api/users/:userId', async (req: Request, res: Response, next: NextFunction) => {
try {
const userService = getUserService();
const userId = parseInt(req.params.userId);
const user = await userService.getUser(userId);
if (!user) {
res.status(404).json({ error: 'User not found' });
return;
}
res.json(user);
} catch (error) {
next(error);
}
});
// Error handling middleware
app.use((error: Error, req: Request, res: Response, next: NextFunction) => {
console.error(error);
res.status(500).json({ error: 'Internal server error' });
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
4. Архитектура на основе событий (Event-Driven Architecture)
Определение: Компоненты системы взаимодействуют через события, что обеспечивает слабую связанность.
Компоненты:
- Event Producers — генерируют события
- Event Consumers — обрабатывают события
- Event Bus/Message Broker — доставляет события
- Event Store — хранит события
Преимущества:
- Слабая связанность
- Масштабируемость
- Отказоустойчивость
- Асинхронная обработка
Недостатки:
- Сложность отладки
- Проблемы с порядком событий
- Сложность тестирования
- Потенциальная потеря событий
Технические примеры:
Java (Spring Boot):
// Event Producer
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private ApplicationEventPublisher eventPublisher;
public Order createOrder(CreateOrderRequest request) {
Order order = new Order();
order.setUserId(request.getUserId());
order.setAmount(request.getAmount());
order.setStatus(OrderStatus.CREATED);
Order savedOrder = orderRepository.save(order);
// Публикация события
OrderCreatedEvent event = new OrderCreatedEvent(savedOrder.getId(),
savedOrder.getUserId(), savedOrder.getAmount());
eventPublisher.publishEvent(event);
return savedOrder;
}
}
// Event Consumer
@Component
public class OrderEventHandler {
@Autowired
private InventoryService inventoryService;
@Autowired
private NotificationService notificationService;
@EventListener
public void handleOrderCreated(OrderCreatedEvent event) {
// Резервирование товара
inventoryService.reserveItems(event.getOrderId());
// Уведомление пользователя
notificationService.sendOrderConfirmation(event.getUserId(), event.getOrderId());
}
}
// Event Class
public class OrderCreatedEvent {
private final Long orderId;
private final Long userId;
private final BigDecimal amount;
private final LocalDateTime timestamp;
public OrderCreatedEvent(Long orderId, Long userId, BigDecimal amount) {
this.orderId = orderId;
this.userId = userId;
this.amount = amount;
this.timestamp = LocalDateTime.now();
}
// геттеры
}
Rust (Actix Web + Tokio):
use serde::{Deserialize, Serialize};
use tokio::sync::mpsc;
use std::sync::Arc;
use tokio::sync::Mutex;
// Event types
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrderCreatedEvent {
pub order_id: i32,
pub user_id: i32,
pub amount: f64,
pub timestamp: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum DomainEvent {
OrderCreated(OrderCreatedEvent),
OrderPaid(OrderPaidEvent),
OrderCancelled(OrderCancelledEvent),
}
// Event Bus
pub struct EventBus {
subscribers: Arc<Mutex<Vec<mpsc::UnboundedSender<DomainEvent>>>>,
}
impl EventBus {
pub fn new() -> Self {
Self {
subscribers: Arc::new(Mutex::new(Vec::new())),
}
}
pub async fn subscribe(&self) -> mpsc::UnboundedReceiver<DomainEvent> {
let (tx, rx) = mpsc::unbounded_channel();
let mut subscribers = self.subscribers.lock().await;
subscribers.push(tx);
rx
}
pub async fn publish(&self, event: DomainEvent) {
let subscribers = self.subscribers.lock().await;
for subscriber in subscribers.iter() {
let _ = subscriber.send(event.clone());
}
}
}
// Event Producer
pub struct OrderService {
order_repository: Box<dyn OrderRepository>,
event_bus: Arc<EventBus>,
}
impl OrderService {
pub fn new(order_repository: Box<dyn OrderRepository>, event_bus: Arc<EventBus>) -> Self {
Self {
order_repository,
event_bus,
}
}
pub async fn create_order(&self, request: CreateOrderRequest) -> Result<Order, OrderError> {
let order = Order {
id: 0, // Will be set by database
user_id: request.user_id,
amount: request.amount,
status: OrderStatus::Created,
created_date: chrono::Utc::now(),
};
let saved_order = self.order_repository.save(order).await?;
// Публикация события
let event = DomainEvent::OrderCreated(OrderCreatedEvent {
order_id: saved_order.id,
user_id: saved_order.user_id,
amount: saved_order.amount,
timestamp: saved_order.created_date,
});
self.event_bus.publish(event).await;
Ok(saved_order)
}
}
// Event Consumer
pub struct OrderEventHandler {
inventory_service: Arc<InventoryService>,
notification_service: Arc<NotificationService>,
}
impl OrderEventHandler {
pub fn new(
inventory_service: Arc<InventoryService>,
notification_service: Arc<NotificationService>,
) -> Self {
Self {
inventory_service,
notification_service,
}
}
pub async fn start_listening(&self, mut event_receiver: mpsc::UnboundedReceiver<DomainEvent>) {
while let Some(event) = event_receiver.recv().await {
match event {
DomainEvent::OrderCreated(order_event) => {
self.handle_order_created(order_event).await;
}
DomainEvent::OrderPaid(payment_event) => {
self.handle_order_paid(payment_event).await;
}
DomainEvent::OrderCancelled(cancel_event) => {
self.handle_order_cancelled(cancel_event).await;
}
}
}
}
async fn handle_order_created(&self, event: OrderCreatedEvent) {
// Резервирование товара
if let Err(e) = self.inventory_service.reserve_items(event.order_id).await {
eprintln!("Failed to reserve items for order {}: {}", event.order_id, e);
}
// Уведомление пользователя
if let Err(e) = self.notification_service.send_order_confirmation(event.user_id, event.order_id).await {
eprintln!("Failed to send notification for order {}: {}", event.order_id, e);
}
}
async fn handle_order_paid(&self, event: OrderPaidEvent) {
// Логика обработки оплаты
println!("Order {} paid: {}", event.order_id, event.amount);
}
async fn handle_order_cancelled(&self, event: OrderCancelledEvent) {
// Логика обработки отмены
println!("Order {} cancelled: {}", event.order_id, event.reason);
}
}
// Event Store
pub struct EventStore {
events: Arc<Mutex<Vec<StoredEvent>>>,
}
#[derive(Debug, Clone)]
pub struct StoredEvent {
pub id: i32,
pub event_type: String,
pub event_data: String,
pub timestamp: chrono::DateTime<chrono::Utc>,
}
impl EventStore {
pub fn new() -> Self {
Self {
events: Arc::new(Mutex::new(Vec::new())),
}
}
pub async fn store_event(&self, event: DomainEvent) -> Result<(), EventStoreError> {
let stored_event = StoredEvent {
id: self.get_next_id().await,
event_type: self.get_event_type(&event),
event_data: serde_json::to_string(&event)
.map_err(|e| EventStoreError::SerializationError(e.to_string()))?,
timestamp: chrono::Utc::now(),
};
let mut events = self.events.lock().await;
events.push(stored_event);
Ok(())
}
async fn get_next_id(&self) -> i32 {
let events = self.events.lock().await;
events.len() as i32 + 1
}
fn get_event_type(&self, event: &DomainEvent) -> String {
match event {
DomainEvent::OrderCreated(_) => "OrderCreated".to_string(),
DomainEvent::OrderPaid(_) => "OrderPaid".to_string(),
DomainEvent::OrderCancelled(_) => "OrderCancelled".to_string(),
}
}
}
Python (FastAPI + Celery):
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from datetime import datetime
from typing import Optional
import asyncio
from celery import Celery
from dataclasses import dataclass
import json
app = FastAPI()
# Celery configuration
celery_app = Celery('order_service', broker='redis://localhost:6379/0')
# Event types
@dataclass
class OrderCreatedEvent:
order_id: int
user_id: int
amount: float
timestamp: datetime
@dataclass
class OrderPaidEvent:
order_id: int
user_id: int
amount: float
payment_id: str
timestamp: datetime
@dataclass
class OrderCancelledEvent:
order_id: int
user_id: int
reason: str
timestamp: datetime
# Event Bus
class EventBus:
def __init__(self):
self.subscribers = {}
def subscribe(self, event_type: str, handler):
if event_type not in self.subscribers:
self.subscribers[event_type] = []
self.subscribers[event_type].append(handler)
async def publish(self, event):
event_type = type(event).__name__
if event_type in self.subscribers:
for handler in self.subscribers[event_type]:
try:
await handler(event)
except Exception as e:
print(f"Error handling event {event_type}: {e}")
# Global event bus
event_bus = EventBus()
# Event Producer
class OrderService:
def __init__(self, order_repository, event_bus):
self.order_repository = order_repository
self.event_bus = event_bus
async def create_order(self, request: CreateOrderRequest) -> Order:
order = Order(
user_id=request.user_id,
amount=request.amount,
status=OrderStatus.CREATED,
created_date=datetime.utcnow()
)
saved_order = await self.order_repository.save(order)
# Публикация события
event = OrderCreatedEvent(
order_id=saved_order.id,
user_id=saved_order.user_id,
amount=saved_order.amount,
timestamp=saved_order.created_date
)
await self.event_bus.publish(event)
return saved_order
# Event Consumers
class OrderEventHandler:
def __init__(self, inventory_service, notification_service):
self.inventory_service = inventory_service
self.notification_service = notification_service
async def handle_order_created(self, event: OrderCreatedEvent):
# Резервирование товара
try:
await self.inventory_service.reserve_items(event.order_id)
except Exception as e:
print(f"Failed to reserve items for order {event.order_id}: {e}")
# Уведомление пользователя
try:
await self.notification_service.send_order_confirmation(event.user_id, event.order_id)
except Exception as e:
print(f"Failed to send notification for order {event.order_id}: {e}")
async def handle_order_paid(self, event: OrderPaidEvent):
print(f"Order {event.order_id} paid: {event.amount}")
async def handle_order_cancelled(self, event: OrderCancelledEvent):
print(f"Order {event.order_id} cancelled: {event.reason}")
# Celery tasks for async processing
@celery_app.task
def process_order_created_async(event_data: dict):
"""Process order created event asynchronously"""
event = OrderCreatedEvent(**event_data)
# Heavy processing that doesn't need to be synchronous
print(f"Processing order {event.order_id} asynchronously")
# Send to external systems
# Update analytics
# Generate reports
pass
@celery_app.task
def send_email_notification_async(user_id: int, order_id: int):
"""Send email notification asynchronously"""
print(f"Sending email notification to user {user_id} for order {order_id}")
# Actual email sending logic here
pass
# Event Store
class EventStore:
def __init__(self):
self.events = []
async def store_event(self, event):
stored_event = {
'id': len(self.events) + 1,
'event_type': type(event).__name__,
'event_data': event.__dict__,
'timestamp': datetime.utcnow()
}
self.events.append(stored_event)
return stored_event
async def get_events_by_type(self, event_type: str):
return [event for event in self.events if event['event_type'] == event_type]
async def get_events_by_order_id(self, order_id: int):
return [event for event in self.events
if event['event_data'].get('order_id') == order_id]
# Event Replay functionality
class EventReplayer:
def __init__(self, event_store, event_bus):
self.event_store = event_store
self.event_bus = event_bus
async def replay_events_for_order(self, order_id: int):
"""Replay all events for a specific order"""
events = await self.event_store.get_events_by_order_id(order_id)
for event_data in events:
event_type = event_data['event_type']
event_class = globals()[event_type]
event = event_class(**event_data['event_data'])
await self.event_bus.publish(event)
async def replay_events_by_type(self, event_type: str):
"""Replay all events of a specific type"""
events = await self.event_store.get_events_by_type(event_type)
for event_data in events:
event_class = globals()[event_type]
event = event_class(**event_data['event_data'])
await self.event_bus.publish(event)
# Setup event handlers
async def setup_event_handlers():
inventory_service = InventoryService()
notification_service = NotificationService()
event_handler = OrderEventHandler(inventory_service, notification_service)
# Subscribe to events
event_bus.subscribe('OrderCreatedEvent', event_handler.handle_order_created)
event_bus.subscribe('OrderPaidEvent', event_handler.handle_order_paid)
event_bus.subscribe('OrderCancelledEvent', event_handler.handle_order_cancelled)
# API endpoints
@app.post("/api/orders")
async def create_order(request: CreateOrderRequest):
order_repository = OrderRepository()
order_service = OrderService(order_repository, event_bus)
try:
order = await order_service.create_order(request)
return order
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/events/replay/{order_id}")
async def replay_order_events(order_id: int):
event_store = EventStore()
replayer = EventReplayer(event_store, event_bus)
try:
await replayer.replay_events_for_order(order_id)
return {"message": f"Events replayed for order {order_id}"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# Startup event
@app.on_event("startup")
async def startup_event():
await setup_event_handlers()
Node.js (Express + TypeScript + EventEmitter):
import express, { Request, Response, NextFunction } from 'express';
import { EventEmitter } from 'events';
import { PrismaClient } from '@prisma/client';
import { z } from 'zod';
import Redis from 'ioredis';
const app = express();
const prisma = new PrismaClient();
const redis = new Redis();
// Event types
interface OrderCreatedEvent {
orderId: number;
userId: number;
amount: number;
timestamp: Date;
}
interface OrderPaidEvent {
orderId: number;
userId: number;
amount: number;
paymentId: string;
timestamp: Date;
}
interface OrderCancelledEvent {
orderId: number;
userId: number;
reason: string;
timestamp: Date;
}
// Event Bus
class EventBus extends EventEmitter {
private static instance: EventBus;
private constructor() {
super();
this.setMaxListeners(100);
}
static getInstance(): EventBus {
if (!EventBus.instance) {
EventBus.instance = new EventBus();
}
return EventBus.instance;
}
async publish(event: string, data: any): Promise<void> {
// Store event in Redis for persistence
await redis.lpush('events', JSON.stringify({
type: event,
data,
timestamp: new Date()
}));
// Emit event to local listeners
this.emit(event, data);
}
subscribe(event: string, handler: (data: any) => Promise<void>): void {
this.on(event, async (data) => {
try {
await handler(data);
} catch (error) {
console.error(`Error handling event ${event}:`, error);
}
});
}
}
// Event Producer
class OrderService {
constructor(
private orderRepository: OrderRepository,
private eventBus: EventBus
) {}
async createOrder(request: CreateOrderRequest): Promise<Order> {
const order = {
userId: request.userId,
amount: request.amount,
status: 'CREATED',
createdDate: new Date()
};
const savedOrder = await this.orderRepository.save(order);
// Публикация события
const event: OrderCreatedEvent = {
orderId: savedOrder.id,
userId: savedOrder.userId,
amount: savedOrder.amount,
timestamp: savedOrder.createdDate
};
await this.eventBus.publish('order.created', event);
return savedOrder;
}
async payOrder(orderId: number, paymentId: string): Promise<void> {
const order = await this.orderRepository.findById(orderId);
if (!order) {
throw new Error('Order not found');
}
await this.orderRepository.updateStatus(orderId, 'PAID');
const event: OrderPaidEvent = {
orderId: order.id,
userId: order.userId,
amount: order.amount,
paymentId,
timestamp: new Date()
};
await this.eventBus.publish('order.paid', event);
}
async cancelOrder(orderId: number, reason: string): Promise<void> {
const order = await this.orderRepository.findById(orderId);
if (!order) {
throw new Error('Order not found');
}
await this.orderRepository.updateStatus(orderId, 'CANCELLED');
const event: OrderCancelledEvent = {
orderId: order.id,
userId: order.userId,
reason,
timestamp: new Date()
};
await this.eventBus.publish('order.cancelled', event);
}
}
// Event Consumers
class OrderEventHandler {
constructor(
private inventoryService: InventoryService,
private notificationService: NotificationService,
private analyticsService: AnalyticsService
) {}
async handleOrderCreated(event: OrderCreatedEvent): Promise<void> {
console.log(`Processing order created: ${event.orderId}`);
// Резервирование товара
try {
await this.inventoryService.reserveItems(event.orderId);
} catch (error) {
console.error(`Failed to reserve items for order ${event.orderId}:`, error);
}
// Уведомление пользователя
try {
await this.notificationService.sendOrderConfirmation(event.userId, event.orderId);
} catch (error) {
console.error(`Failed to send notification for order ${event.orderId}:`, error);
}
// Аналитика
try {
await this.analyticsService.trackOrderCreated(event);
} catch (error) {
console.error(`Failed to track order ${event.orderId}:`, error);
}
}
async handleOrderPaid(event: OrderPaidEvent): Promise<void> {
console.log(`Order ${event.orderId} paid: ${event.amount}`);
// Обновление инвентаря
await this.inventoryService.confirmReservation(event.orderId);
// Отправка чека
await this.notificationService.sendReceipt(event.userId, event.orderId);
// Аналитика
await this.analyticsService.trackOrderPaid(event);
}
async handleOrderCancelled(event: OrderCancelledEvent): Promise<void> {
console.log(`Order ${event.orderId} cancelled: ${event.reason}`);
// Освобождение резерва
await this.inventoryService.releaseReservation(event.orderId);
// Уведомление об отмене
await this.notificationService.sendCancellationNotice(event.userId, event.orderId);
// Аналитика
await this.analyticsService.trackOrderCancelled(event);
}
}
// Event Store
class EventStore {
async storeEvent(event: any): Promise<void> {
await redis.lpush('event_store', JSON.stringify({
...event,
storedAt: new Date()
}));
}
async getEventsByOrderId(orderId: number): Promise<any[]> {
const events = await redis.lrange('event_store', 0, -1);
return events
.map(event => JSON.parse(event))
.filter(event => event.data.orderId === orderId);
}
async getEventsByType(eventType: string): Promise<any[]> {
const events = await redis.lrange('event_store', 0, -1);
return events
.map(event => JSON.parse(event))
.filter(event => event.type === eventType);
}
}
// Event Replay functionality
class EventReplayer {
constructor(
private eventStore: EventStore,
private eventBus: EventBus
) {}
async replayEventsForOrder(orderId: number): Promise<void> {
const events = await this.eventStore.getEventsByOrderId(orderId);
for (const event of events) {
await this.eventBus.publish(event.type, event.data);
}
}
async replayEventsByType(eventType: string): Promise<void> {
const events = await this.eventStore.getEventsByType(eventType);
for (const event of events) {
await this.eventBus.publish(event.type, event.data);
}
}
}
// Services implementations
class InventoryService {
async reserveItems(orderId: number): Promise<void> {
console.log(`Reserving items for order ${orderId}`);
// Implementation for inventory reservation
}
async confirmReservation(orderId: number): Promise<void> {
console.log(`Confirming reservation for order ${orderId}`);
// Implementation for reservation confirmation
}
async releaseReservation(orderId: number): Promise<void> {
console.log(`Releasing reservation for order ${orderId}`);
// Implementation for reservation release
}
}
class NotificationService {
async sendOrderConfirmation(userId: number, orderId: number): Promise<void> {
console.log(`Sending order confirmation to user ${userId} for order ${orderId}`);
// Implementation for sending confirmation email/SMS
}
async sendReceipt(userId: number, orderId: number): Promise<void> {
console.log(`Sending receipt to user ${userId} for order ${orderId}`);
// Implementation for sending receipt
}
async sendCancellationNotice(userId: number, orderId: number): Promise<void> {
console.log(`Sending cancellation notice to user ${userId} for order ${orderId}`);
// Implementation for sending cancellation notice
}
}
class AnalyticsService {
async trackOrderCreated(event: OrderCreatedEvent): Promise<void> {
console.log(`Tracking order created: ${event.orderId}`);
// Implementation for analytics tracking
}
async trackOrderPaid(event: OrderPaidEvent): Promise<void> {
console.log(`Tracking order paid: ${event.orderId}`);
// Implementation for analytics tracking
}
async trackOrderCancelled(event: OrderCancelledEvent): Promise<void> {
console.log(`Tracking order cancelled: ${event.orderId}`);
// Implementation for analytics tracking
}
}
// Repository
interface OrderRepository {
save(order: Omit<Order, 'id'>): Promise<Order>;
findById(id: number): Promise<Order | null>;
updateStatus(id: number, status: string): Promise<void>;
}
class PrismaOrderRepository implements OrderRepository {
async save(order: Omit<Order, 'id'>): Promise<Order> {
return await prisma.order.create({ data: order });
}
async findById(id: number): Promise<Order | null> {
return await prisma.order.findUnique({ where: { id } });
}
async updateStatus(id: number, status: string): Promise<void> {
await prisma.order.update({
where: { id },
data: { status }
});
}
}
// Setup event handlers
function setupEventHandlers(): void {
const eventBus = EventBus.getInstance();
const inventoryService = new InventoryService();
const notificationService = new NotificationService();
const analyticsService = new AnalyticsService();
const eventHandler = new OrderEventHandler(
inventoryService,
notificationService,
analyticsService
);
// Subscribe to events
eventBus.subscribe('order.created', eventHandler.handleOrderCreated.bind(eventHandler));
eventBus.subscribe('order.paid', eventHandler.handleOrderPaid.bind(eventHandler));
eventBus.subscribe('order.cancelled', eventHandler.handleOrderCancelled.bind(eventHandler));
}
// Validation schemas
const createOrderSchema = z.object({
userId: z.number().positive(),
amount: z.number().positive()
});
// API endpoints
app.post('/api/orders', async (req: Request, res: Response, next: NextFunction) => {
try {
const eventBus = EventBus.getInstance();
const orderRepository = new PrismaOrderRepository();
const orderService = new OrderService(orderRepository, eventBus);
const validatedData = createOrderSchema.parse(req.body);
const order = await orderService.createOrder(validatedData);
res.json(order);
} catch (error) {
if (error instanceof z.ZodError) {
res.status(400).json({ error: error.errors });
} else {
next(error);
}
}
});
app.post('/api/orders/:orderId/pay', async (req: Request, res: Response, next: NextFunction) => {
try {
const eventBus = EventBus.getInstance();
const orderRepository = new PrismaOrderRepository();
const orderService = new OrderService(orderRepository, eventBus);
const orderId = parseInt(req.params.orderId);
const paymentId = req.body.paymentId;
await orderService.payOrder(orderId, paymentId);
res.json({ message: 'Order paid successfully' });
} catch (error) {
next(error);
}
});
app.post('/api/orders/:orderId/cancel', async (req: Request, res: Response, next: NextFunction) => {
try {
const eventBus = EventBus.getInstance();
const orderRepository = new PrismaOrderRepository();
const orderService = new OrderService(orderRepository, eventBus);
const orderId = parseInt(req.params.orderId);
const reason = req.body.reason;
await orderService.cancelOrder(orderId, reason);
res.json({ message: 'Order cancelled successfully' });
} catch (error) {
next(error);
}
});
app.post('/api/events/replay/:orderId', async (req: Request, res: Response, next: NextFunction) => {
try {
const eventStore = new EventStore();
const eventBus = EventBus.getInstance();
const replayer = new EventReplayer(eventStore, eventBus);
const orderId = parseInt(req.params.orderId);
await replayer.replayEventsForOrder(orderId);
res.json({ message: `Events replayed for order ${orderId}` });
} catch (error) {
next(error);
}
});
// Error handling middleware
app.use((error: Error, req: Request, res: Response, next: NextFunction) => {
console.error(error);
res.status(500).json({ error: 'Internal server error' });
});
// Initialize event handlers
setupEventHandlers();
app.listen(3000, () => {
console.log('Server running on port 3000');
});
5. Архитектура на основе микросервисов с CQRS (Command Query Responsibility Segregation)
Определение: Разделение операций чтения и записи на отдельные модели.
Компоненты:
- Command Side — обработка команд (запись)
- Query Side — обработка запросов (чтение)
- Event Store — хранилище событий
- Projection — представления для чтения
Преимущества:
- Оптимизация для чтения и записи
- Масштабируемость
- Независимость моделей
- Аудит изменений
Недостатки:
- Сложность реализации
- Потенциальная несогласованность
- Overhead синхронизации
- Сложность отладки
Технические примеры:
Java (Spring Boot):
// Command Side
@Service
public class OrderCommandService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private EventStore eventStore;
public void createOrder(CreateOrderCommand command) {
// Валидация команды
validateCommand(command);
// Создание агрегата
OrderAggregate order = new OrderAggregate();
order.createOrder(command.getUserId(), command.getItems());
// Сохранение событий
eventStore.saveEvents(order.getUncommittedEvents());
// Публикация событий
publishEvents(order.getUncommittedEvents());
}
}
// Query Side
@Service
public class OrderQueryService {
@Autowired
private OrderReadRepository orderReadRepository;
public OrderView getOrder(Long orderId) {
return orderReadRepository.findById(orderId);
}
public List<OrderView> getUserOrders(Long userId) {
return orderReadRepository.findByUserId(userId);
}
}
// Read Model
@Entity
@Table(name = "order_views")
public class OrderView {
@Id
private Long orderId;
private Long userId;
private String status;
private BigDecimal totalAmount;
private LocalDateTime createdDate;
// геттеры и сеттеры
}
// Event Handler для синхронизации
@Component
public class OrderProjectionHandler {
@Autowired
private OrderReadRepository orderReadRepository;
@EventHandler
public void handle(OrderCreatedEvent event) {
OrderView view = new OrderView();
view.setOrderId(event.getOrderId());
view.setUserId(event.getUserId());
view.setStatus("CREATED");
view.setTotalAmount(event.getAmount());
view.setCreatedDate(event.getTimestamp());
orderReadRepository.save(view);
}
}
Rust (Actix Web + Diesel):
use serde::{Deserialize, Serialize};
use diesel::prelude::*;
use std::sync::Arc;
use tokio::sync::Mutex;
// Command Side
#[derive(Debug, Deserialize)]
pub struct CreateOrderCommand {
pub user_id: i32,
pub items: Vec<OrderItemCommand>,
}
#[derive(Debug, Deserialize)]
pub struct OrderItemCommand {
pub product_id: i32,
pub quantity: i32,
pub price: f64,
}
pub struct OrderCommandService {
event_store: Arc<EventStore>,
command_repository: Arc<dyn CommandRepository>,
}
impl OrderCommandService {
pub fn new(event_store: Arc<EventStore>, command_repository: Arc<dyn CommandRepository>) -> Self {
Self {
event_store,
command_repository,
}
}
pub async fn create_order(&self, command: CreateOrderCommand) -> Result<(), OrderError> {
// Валидация команды
self.validate_command(&command)?;
// Создание агрегата
let mut order_aggregate = OrderAggregate::new();
order_aggregate.create_order(command.user_id, command.items);
// Сохранение событий
let events = order_aggregate.get_uncommitted_events();
self.event_store.save_events(&events).await?;
// Публикация событий
self.publish_events(&events).await?;
Ok(())
}
fn validate_command(&self, command: &CreateOrderCommand) -> Result<(), OrderError> {
if command.user_id <= 0 {
return Err(OrderError::ValidationError("Invalid user ID".to_string()));
}
if command.items.is_empty() {
return Err(OrderError::ValidationError("Order must have items".to_string()));
}
Ok(())
}
async fn publish_events(&self, events: &[DomainEvent]) -> Result<(), OrderError> {
for event in events {
self.event_store.publish_event(event).await?;
}
Ok(())
}
}
// Query Side
pub struct OrderQueryService {
read_repository: Arc<dyn ReadRepository>,
}
impl OrderQueryService {
pub fn new(read_repository: Arc<dyn ReadRepository>) -> Self {
Self { read_repository }
}
pub async fn get_order(&self, order_id: i32) -> Result<Option<OrderView>, OrderError> {
self.read_repository.find_by_id(order_id).await
}
pub async fn get_user_orders(&self, user_id: i32) -> Result<Vec<OrderView>, OrderError> {
self.read_repository.find_by_user_id(user_id).await
}
pub async fn get_orders_by_status(&self, status: OrderStatus) -> Result<Vec<OrderView>, OrderError> {
self.read_repository.find_by_status(status).await
}
}
// Read Model
#[derive(Debug, Clone, Serialize, Deserialize, Queryable)]
pub struct OrderView {
pub order_id: i32,
pub user_id: i32,
pub status: String,
pub total_amount: f64,
pub created_date: chrono::DateTime<chrono::Utc>,
pub updated_date: chrono::DateTime<chrono::Utc>,
}
// Read Repository
#[async_trait::async_trait]
pub trait ReadRepository: Send + Sync {
async fn find_by_id(&self, order_id: i32) -> Result<Option<OrderView>, OrderError>;
async fn find_by_user_id(&self, user_id: i32) -> Result<Vec<OrderView>, OrderError>;
async fn find_by_status(&self, status: OrderStatus) -> Result<Vec<OrderView>, OrderError>;
async fn save(&self, view: OrderView) -> Result<OrderView, OrderError>;
async fn update(&self, view: OrderView) -> Result<OrderView, OrderError>;
}
pub struct DieselReadRepository {
connection: Arc<Mutex<PgConnection>>,
}
#[async_trait::async_trait]
impl ReadRepository for DieselReadRepository {
async fn find_by_id(&self, order_id: i32) -> Result<Option<OrderView>, OrderError> {
use crate::schema::order_views::dsl::*;
let connection = self.connection.lock().await;
order_views
.filter(order_id.eq(order_id))
.first(&*connection)
.optional()
.map_err(|e| OrderError::DatabaseError(e.to_string()))
}
async fn find_by_user_id(&self, user_id: i32) -> Result<Vec<OrderView>, OrderError> {
use crate::schema::order_views::dsl::*;
let connection = self.connection.lock().await;
order_views
.filter(user_id.eq(user_id))
.load(&*connection)
.map_err(|e| OrderError::DatabaseError(e.to_string()))
}
async fn find_by_status(&self, status: OrderStatus) -> Result<Vec<OrderView>, OrderError> {
use crate::schema::order_views::dsl::*;
let connection = self.connection.lock().await;
order_views
.filter(status.eq(status.to_string()))
.load(&*connection)
.map_err(|e| OrderError::DatabaseError(e.to_string()))
}
async fn save(&self, view: OrderView) -> Result<OrderView, OrderError> {
use crate::schema::order_views::dsl::*;
let connection = self.connection.lock().await;
diesel::insert_into(order_views)
.values(&view)
.get_result(&*connection)
.map_err(|e| OrderError::DatabaseError(e.to_string()))
}
async fn update(&self, view: OrderView) -> Result<OrderView, OrderError> {
use crate::schema::order_views::dsl::*;
let connection = self.connection.lock().await;
diesel::update(order_views.filter(order_id.eq(view.order_id)))
.set(&view)
.get_result(&*connection)
.map_err(|e| OrderError::DatabaseError(e.to_string()))
}
}
// Event Handler для синхронизации
pub struct OrderProjectionHandler {
read_repository: Arc<dyn ReadRepository>,
}
impl OrderProjectionHandler {
pub fn new(read_repository: Arc<dyn ReadRepository>) -> Self {
Self { read_repository }
}
pub async fn handle_order_created(&self, event: OrderCreatedEvent) -> Result<(), OrderError> {
let view = OrderView {
order_id: event.order_id,
user_id: event.user_id,
status: "CREATED".to_string(),
total_amount: event.amount,
created_date: event.timestamp,
updated_date: chrono::Utc::now(),
};
self.read_repository.save(view).await?;
Ok(())
}
pub async fn handle_order_paid(&self, event: OrderPaidEvent) -> Result<(), OrderError> {
if let Some(mut view) = self.read_repository.find_by_id(event.order_id).await? {
view.status = "PAID".to_string();
view.updated_date = chrono::Utc::now();
self.read_repository.update(view).await?;
}
Ok(())
}
pub async fn handle_order_cancelled(&self, event: OrderCancelledEvent) -> Result<(), OrderError> {
if let Some(mut view) = self.read_repository.find_by_id(event.order_id).await? {
view.status = "CANCELLED".to_string();
view.updated_date = chrono::Utc::now();
self.read_repository.update(view).await?;
}
Ok(())
}
}
// Event Store
pub struct EventStore {
events: Arc<Mutex<Vec<StoredEvent>>>,
}
impl EventStore {
pub fn new() -> Self {
Self {
events: Arc::new(Mutex::new(Vec::new())),
}
}
pub async fn save_events(&self, events: &[DomainEvent]) -> Result<(), OrderError> {
let mut stored_events = self.events.lock().await;
for event in events {
let stored_event = StoredEvent {
id: stored_events.len() as i32 + 1,
event_type: self.get_event_type(event),
event_data: serde_json::to_string(event)
.map_err(|e| OrderError::SerializationError(e.to_string()))?,
timestamp: chrono::Utc::now(),
};
stored_events.push(stored_event);
}
Ok(())
}
pub async fn publish_event(&self, event: &DomainEvent) -> Result<(), OrderError> {
// Здесь можно добавить логику публикации в message broker
println!("Publishing event: {:?}", event);
Ok(())
}
fn get_event_type(&self, event: &DomainEvent) -> String {
match event {
DomainEvent::OrderCreated(_) => "OrderCreated".to_string(),
DomainEvent::OrderPaid(_) => "OrderPaid".to_string(),
DomainEvent::OrderCancelled(_) => "OrderCancelled".to_string(),
}
}
}
Python (FastAPI + SQLAlchemy):
from fastapi import FastAPI, HTTPException, Depends
from pydantic import BaseModel
from datetime import datetime
from typing import List, Optional
from abc import ABC, abstractmethod
from sqlalchemy import create_engine, Column, Integer, String, Float, DateTime
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session
import asyncio
app = FastAPI()
Base = declarative_base()
# Command Side
class CreateOrderCommand(BaseModel):
user_id: int
items: List['OrderItemCommand']
class OrderItemCommand(BaseModel):
product_id: int
quantity: int
price: float
class OrderCommandService:
def __init__(self, event_store: 'EventStore', command_repository: 'CommandRepository'):
self.event_store = event_store
self.command_repository = command_repository
async def create_order(self, command: CreateOrderCommand) -> None:
# Валидация команды
self.validate_command(command)
# Создание агрегата
order_aggregate = OrderAggregate()
order_aggregate.create_order(command.user_id, command.items)
# Сохранение событий
events = order_aggregate.get_uncommitted_events()
await self.event_store.save_events(events)
# Публикация событий
await self.publish_events(events)
def validate_command(self, command: CreateOrderCommand) -> None:
if command.user_id <= 0:
raise ValueError("Invalid user ID")
if not command.items:
raise ValueError("Order must have items")
for item in command.items:
if item.quantity <= 0:
raise ValueError("Item quantity must be positive")
if item.price <= 0:
raise ValueError("Item price must be positive")
async def publish_events(self, events: List['DomainEvent']) -> None:
for event in events:
await self.event_store.publish_event(event)
# Query Side
class OrderQueryService:
def __init__(self, read_repository: 'ReadRepository'):
self.read_repository = read_repository
async def get_order(self, order_id: int) -> Optional['OrderView']:
return await self.read_repository.find_by_id(order_id)
async def get_user_orders(self, user_id: int) -> List['OrderView']:
return await self.read_repository.find_by_user_id(user_id)
async def get_orders_by_status(self, status: str) -> List['OrderView']:
return await self.read_repository.find_by_status(status)
# Read Model
class OrderView(Base):
__tablename__ = "order_views"
order_id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, nullable=False, index=True)
status = Column(String, nullable=False, index=True)
total_amount = Column(Float, nullable=False)
created_date = Column(DateTime, nullable=False)
updated_date = Column(DateTime, nullable=False, default=datetime.utcnow)
# Read Repository
class ReadRepository(ABC):
@abstractmethod
async def find_by_id(self, order_id: int) -> Optional[OrderView]:
pass
@abstractmethod
async def find_by_user_id(self, user_id: int) -> List[OrderView]:
pass
@abstractmethod
async def find_by_status(self, status: str) -> List[OrderView]:
pass
@abstractmethod
async def save(self, view: OrderView) -> OrderView:
pass
@abstractmethod
async def update(self, view: OrderView) -> OrderView:
pass
class SQLAlchemyReadRepository(ReadRepository):
def __init__(self, db: Session):
self.db = db
async def find_by_id(self, order_id: int) -> Optional[OrderView]:
return self.db.query(OrderView).filter(OrderView.order_id == order_id).first()
async def find_by_user_id(self, user_id: int) -> List[OrderView]:
return self.db.query(OrderView).filter(OrderView.user_id == user_id).all()
async def find_by_status(self, status: str) -> List[OrderView]:
return self.db.query(OrderView).filter(OrderView.status == status).all()
async def save(self, view: OrderView) -> OrderView:
self.db.add(view)
self.db.commit()
self.db.refresh(view)
return view
async def update(self, view: OrderView) -> OrderView:
self.db.commit()
self.db.refresh(view)
return view
# Event Handler для синхронизации
class OrderProjectionHandler:
def __init__(self, read_repository: ReadRepository):
self.read_repository = read_repository
async def handle_order_created(self, event: 'OrderCreatedEvent') -> None:
view = OrderView(
order_id=event.order_id,
user_id=event.user_id,
status="CREATED",
total_amount=event.amount,
created_date=event.timestamp,
updated_date=datetime.utcnow()
)
await self.read_repository.save(view)
async def handle_order_paid(self, event: 'OrderPaidEvent') -> None:
view = await self.read_repository.find_by_id(event.order_id)
if view:
view.status = "PAID"
view.updated_date = datetime.utcnow()
await self.read_repository.update(view)
async def handle_order_cancelled(self, event: 'OrderCancelledEvent') -> None:
view = await self.read_repository.find_by_id(event.order_id)
if view:
view.status = "CANCELLED"
view.updated_date = datetime.utcnow()
await self.read_repository.update(view)
# Event Store
class EventStore:
def __init__(self):
self.events = []
async def save_events(self, events: List['DomainEvent']) -> None:
for event in events:
stored_event = {
'id': len(self.events) + 1,
'event_type': type(event).__name__,
'event_data': event.__dict__,
'timestamp': datetime.utcnow()
}
self.events.append(stored_event)
async def publish_event(self, event: 'DomainEvent') -> None:
# Здесь можно добавить логику публикации в message broker
print(f"Publishing event: {event}")
async def get_events_by_order_id(self, order_id: int) -> List[dict]:
return [event for event in self.events
if event['event_data'].get('order_id') == order_id]
# Domain Events
@dataclass
class OrderCreatedEvent:
order_id: int
user_id: int
amount: float
timestamp: datetime
@dataclass
class OrderPaidEvent:
order_id: int
user_id: int
amount: float
payment_id: str
timestamp: datetime
@dataclass
class OrderCancelledEvent:
order_id: int
user_id: int
reason: str
timestamp: datetime
# Order Aggregate
class OrderAggregate:
def __init__(self):
self.uncommitted_events = []
def create_order(self, user_id: int, items: List[OrderItemCommand]) -> None:
total_amount = sum(item.price * item.quantity for item in items)
event = OrderCreatedEvent(
order_id=0, # Will be set by database
user_id=user_id,
amount=total_amount,
timestamp=datetime.utcnow()
)
self.uncommitted_events.append(event)
def get_uncommitted_events(self) -> List['DomainEvent']:
return self.uncommitted_events.copy()
def mark_events_as_committed(self) -> None:
self.uncommitted_events.clear()
# API endpoints
@app.post("/api/orders")
async def create_order(command: CreateOrderCommand):
event_store = EventStore()
command_repository = CommandRepository() # Implementation needed
command_service = OrderCommandService(event_store, command_repository)
try:
await command_service.create_order(command)
return {"message": "Order created successfully"}
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail="Internal server error")
@app.get("/api/orders/{order_id}")
async def get_order(order_id: int):
read_repository = SQLAlchemyReadRepository(get_db())
query_service = OrderQueryService(read_repository)
order = await query_service.get_order(order_id)
if not order:
raise HTTPException(status_code=404, detail="Order not found")
return order
@app.get("/api/users/{user_id}/orders")
async def get_user_orders(user_id: int):
read_repository = SQLAlchemyReadRepository(get_db())
query_service = OrderQueryService(read_repository)
orders = await query_service.get_user_orders(user_id)
return orders
@app.get("/api/orders/status/{status}")
async def get_orders_by_status(status: str):
read_repository = SQLAlchemyReadRepository(get_db())
query_service = OrderQueryService(read_repository)
orders = await query_service.get_orders_by_status(status)
return orders
Node.js (Express + TypeScript + Prisma):
import express, { Request, Response, NextFunction } from 'express';
import { PrismaClient } from '@prisma/client';
import { z } from 'zod';
import { EventEmitter } from 'events';
const app = express();
const prisma = new PrismaClient();
// Command Side
interface CreateOrderCommand {
userId: number;
items: OrderItemCommand[];
}
interface OrderItemCommand {
productId: number;
quantity: number;
price: number;
}
class OrderCommandService {
constructor(
private eventStore: EventStore,
private commandRepository: CommandRepository
) {}
async createOrder(command: CreateOrderCommand): Promise<void> {
// Валидация команды
this.validateCommand(command);
// Создание агрегата
const orderAggregate = new OrderAggregate();
orderAggregate.createOrder(command.userId, command.items);
// Сохранение событий
const events = orderAggregate.getUncommittedEvents();
await this.eventStore.saveEvents(events);
// Публикация событий
await this.publishEvents(events);
}
private validateCommand(command: CreateOrderCommand): void {
if (command.userId <= 0) {
throw new Error('Invalid user ID');
}
if (!command.items || command.items.length === 0) {
throw new Error('Order must have items');
}
for (const item of command.items) {
if (item.quantity <= 0) {
throw new Error('Item quantity must be positive');
}
if (item.price <= 0) {
throw new Error('Item price must be positive');
}
}
}
private async publishEvents(events: DomainEvent[]): Promise<void> {
for (const event of events) {
await this.eventStore.publishEvent(event);
}
}
}
// Query Side
class OrderQueryService {
constructor(private readRepository: ReadRepository) {}
async getOrder(orderId: number): Promise<OrderView | null> {
return await this.readRepository.findById(orderId);
}
async getUserOrders(userId: number): Promise<OrderView[]> {
return await this.readRepository.findByUserId(userId);
}
async getOrdersByStatus(status: string): Promise<OrderView[]> {
return await this.readRepository.findByStatus(status);
}
}
// Read Model
interface OrderView {
orderId: number;
userId: number;
status: string;
totalAmount: number;
createdDate: Date;
updatedDate: Date;
}
// Read Repository
interface ReadRepository {
findById(orderId: number): Promise<OrderView | null>;
findByUserId(userId: number): Promise<OrderView[]>;
findByStatus(status: string): Promise<OrderView[]>;
save(view: OrderView): Promise<OrderView>;
update(view: OrderView): Promise<OrderView>;
}
class PrismaReadRepository implements ReadRepository {
async findById(orderId: number): Promise<OrderView | null> {
const order = await prisma.orderView.findUnique({
where: { orderId }
});
if (!order) return null;
return {
orderId: order.orderId,
userId: order.userId,
status: order.status,
totalAmount: order.totalAmount,
createdDate: order.createdDate,
updatedDate: order.updatedDate
};
}
async findByUserId(userId: number): Promise<OrderView[]> {
const orders = await prisma.orderView.findMany({
where: { userId }
});
return orders.map(order => ({
orderId: order.orderId,
userId: order.userId,
status: order.status,
totalAmount: order.totalAmount,
createdDate: order.createdDate,
updatedDate: order.updatedDate
}));
}
async findByStatus(status: string): Promise<OrderView[]> {
const orders = await prisma.orderView.findMany({
where: { status }
});
return orders.map(order => ({
orderId: order.orderId,
userId: order.userId,
status: order.status,
totalAmount: order.totalAmount,
createdDate: order.createdDate,
updatedDate: order.updatedDate
}));
}
async save(view: OrderView): Promise<OrderView> {
const savedOrder = await prisma.orderView.create({
data: {
orderId: view.orderId,
userId: view.userId,
status: view.status,
totalAmount: view.totalAmount,
createdDate: view.createdDate,
updatedDate: view.updatedDate
}
});
return {
orderId: savedOrder.orderId,
userId: savedOrder.userId,
status: savedOrder.status,
totalAmount: savedOrder.totalAmount,
createdDate: savedOrder.createdDate,
updatedDate: savedOrder.updatedDate
};
}
async update(view: OrderView): Promise<OrderView> {
const updatedOrder = await prisma.orderView.update({
where: { orderId: view.orderId },
data: {
status: view.status,
totalAmount: view.totalAmount,
updatedDate: view.updatedDate
}
});
return {
orderId: updatedOrder.orderId,
userId: updatedOrder.userId,
status: updatedOrder.status,
totalAmount: updatedOrder.totalAmount,
createdDate: updatedOrder.createdDate,
updatedDate: updatedOrder.updatedDate
};
}
}
// Event Handler для синхронизации
class OrderProjectionHandler {
constructor(private readRepository: ReadRepository) {}
async handleOrderCreated(event: OrderCreatedEvent): Promise<void> {
const view: OrderView = {
orderId: event.orderId,
userId: event.userId,
status: 'CREATED',
totalAmount: event.amount,
createdDate: event.timestamp,
updatedDate: new Date()
};
await this.readRepository.save(view);
}
async handleOrderPaid(event: OrderPaidEvent): Promise<void> {
const existingView = await this.readRepository.findById(event.orderId);
if (existingView) {
existingView.status = 'PAID';
existingView.updatedDate = new Date();
await this.readRepository.update(existingView);
}
}
async handleOrderCancelled(event: OrderCancelledEvent): Promise<void> {
const existingView = await this.readRepository.findById(event.orderId);
if (existingView) {
existingView.status = 'CANCELLED';
existingView.updatedDate = new Date();
await this.readRepository.update(existingView);
}
}
}
// Event Store
class EventStore {
private events: any[] = [];
async saveEvents(events: DomainEvent[]): Promise<void> {
for (const event of events) {
const storedEvent = {
id: this.events.length + 1,
eventType: this.getEventType(event),
eventData: event,
timestamp: new Date()
};
this.events.push(storedEvent);
}
}
async publishEvent(event: DomainEvent): Promise<void> {
console.log(`Publishing event: ${JSON.stringify(event)}`);
// Здесь можно добавить логику публикации в message broker
}
async getEventsByOrderId(orderId: number): Promise<any[]> {
return this.events.filter(event =>
event.eventData.orderId === orderId
);
}
private getEventType(event: DomainEvent): string {
if ('orderId' in event && 'userId' in event && 'amount' in event) {
return 'OrderCreated';
}
if ('orderId' in event && 'paymentId' in event) {
return 'OrderPaid';
}
if ('orderId' in event && 'reason' in event) {
return 'OrderCancelled';
}
return 'Unknown';
}
}
// Domain Events
interface OrderCreatedEvent {
orderId: number;
userId: number;
amount: number;
timestamp: Date;
}
interface OrderPaidEvent {
orderId: number;
userId: number;
amount: number;
paymentId: string;
timestamp: Date;
}
interface OrderCancelledEvent {
orderId: number;
userId: number;
reason: string;
timestamp: Date;
}
type DomainEvent = OrderCreatedEvent | OrderPaidEvent | OrderCancelledEvent;
// Order Aggregate
class OrderAggregate {
private uncommittedEvents: DomainEvent[] = [];
createOrder(userId: number, items: OrderItemCommand[]): void {
const totalAmount = items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
const event: OrderCreatedEvent = {
orderId: 0, // Will be set by database
userId,
amount: totalAmount,
timestamp: new Date()
};
this.uncommittedEvents.push(event);
}
getUncommittedEvents(): DomainEvent[] {
return [...this.uncommittedEvents];
}
markEventsAsCommitted(): void {
this.uncommittedEvents = [];
}
}
// Command Repository
interface CommandRepository {
save(command: any): Promise<any>;
}
class PrismaCommandRepository implements CommandRepository {
async save(command: any): Promise<any> {
// Implementation for saving commands
return command;
}
}
// Validation schemas
const createOrderSchema = z.object({
userId: z.number().positive(),
items: z.array(z.object({
productId: z.number().positive(),
quantity: z.number().positive(),
price: z.number().positive()
}))
});
// API endpoints
app.post('/api/orders', async (req: Request, res: Response, next: NextFunction) => {
try {
const eventStore = new EventStore();
const commandRepository = new PrismaCommandRepository();
const commandService = new OrderCommandService(eventStore, commandRepository);
const validatedData = createOrderSchema.parse(req.body);
await commandService.createOrder(validatedData);
res.json({ message: 'Order created successfully' });
} catch (error) {
if (error instanceof z.ZodError) {
res.status(400).json({ error: error.errors });
} else {
next(error);
}
}
});
app.get('/api/orders/:orderId', async (req: Request, res: Response, next: NextFunction) => {
try {
const readRepository = new PrismaReadRepository();
const queryService = new OrderQueryService(readRepository);
const orderId = parseInt(req.params.orderId);
const order = await queryService.getOrder(orderId);
if (!order) {
res.status(404).json({ error: 'Order not found' });
return;
}
res.json(order);
} catch (error) {
next(error);
}
});
app.get('/api/users/:userId/orders', async (req: Request, res: Response, next: NextFunction) => {
try {
const readRepository = new PrismaReadRepository();
const queryService = new OrderQueryService(readRepository);
const userId = parseInt(req.params.userId);
const orders = await queryService.getUserOrders(userId);
res.json(orders);
} catch (error) {
next(error);
}
});
app.get('/api/orders/status/:status', async (req: Request, res: Response, next: NextFunction) => {
try {
const readRepository = new PrismaReadRepository();
const queryService = new OrderQueryService(readRepository);
const status = req.params.status;
const orders = await queryService.getOrdersByStatus(status);
res.json(orders);
} catch (error) {
next(error);
}
});
// Error handling middleware
app.use((error: Error, req: Request, res: Response, next: NextFunction) => {
console.error(error);
res.status(500).json({ error: 'Internal server error' });
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
6. Гексагональная архитектура (Hexagonal Architecture)
Определение: Архитектура, где бизнес-логика изолирована от внешних зависимостей через порты и адаптеры.
Компоненты:
- Core (Domain) — бизнес-логика
- Ports — интерфейсы для взаимодействия
- Adapters — реализации портов
- Infrastructure — внешние системы
Преимущества:
- Независимость от фреймворков
- Легкость тестирования
- Гибкость в выборе технологий
- Четкое разделение ответственности
Недостатки:
- Дополнительная сложность
- Потенциальный over-engineering
- Кривая обучения
- Больше кода
Технический пример:
// Domain Core
public class Order {
private Long id;
private Long userId;
private List<OrderItem> items;
private OrderStatus status;
public void addItem(Product product, int quantity) {
if (status != OrderStatus.DRAFT) {
throw new IllegalStateException("Cannot add items to non-draft order");
}
OrderItem item = new OrderItem(product, quantity);
items.add(item);
}
public void confirm() {
if (items.isEmpty()) {
throw new IllegalStateException("Cannot confirm empty order");
}
this.status = OrderStatus.CONFIRMED;
}
}
// Ports (Interfaces)
public interface OrderRepository {
Order save(Order order);
Optional<Order> findById(Long id);
List<Order> findByUserId(Long userId);
}
public interface PaymentService {
PaymentResult processPayment(PaymentRequest request);
}
public interface NotificationService {
void sendOrderConfirmation(Long userId, Long orderId);
}
// Application Service (Use Cases)
@Service
public class OrderApplicationService {
private final OrderRepository orderRepository;
private final PaymentService paymentService;
private final NotificationService notificationService;
public OrderApplicationService(OrderRepository orderRepository,
PaymentService paymentService,
NotificationService notificationService) {
this.orderRepository = orderRepository;
this.paymentService = paymentService;
this.notificationService = notificationService;
}
public Order createOrder(CreateOrderRequest request) {
Order order = new Order();
order.setUserId(request.getUserId());
for (OrderItemRequest itemRequest : request.getItems()) {
order.addItem(itemRequest.getProduct(), itemRequest.getQuantity());
}
order.confirm();
return orderRepository.save(order);
}
public void processPayment(Long orderId, PaymentRequest paymentRequest) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException("Order not found"));
PaymentResult result = paymentService.processPayment(paymentRequest);
if (result.isSuccess()) {
order.markAsPaid();
orderRepository.save(order);
notificationService.sendOrderConfirmation(order.getUserId(), orderId);
}
}
}
// Adapters (Infrastructure)
@Repository
public class JpaOrderRepository implements OrderRepository {
@Autowired
private OrderJpaRepository jpaRepository;
@Override
public Order save(Order order) {
OrderEntity entity = mapToEntity(order);
OrderEntity saved = jpaRepository.save(entity);
return mapToDomain(saved);
}
@Override
public Optional<Order> findById(Long id) {
return jpaRepository.findById(id)
.map(this::mapToDomain);
}
}
@Component
public class StripePaymentService implements PaymentService {
@Autowired
private StripeClient stripeClient;
@Override
public PaymentResult processPayment(PaymentRequest request) {
try {
Charge charge = stripeClient.charges().create(
ChargeCreateParams.builder()
.setAmount(request.getAmount().multiply(BigDecimal.valueOf(100)).longValue())
.setCurrency("usd")
.setSource(request.getToken())
.build()
);
return PaymentResult.success(charge.getId());
} catch (StripeException e) {
return PaymentResult.failure(e.getMessage());
}
}
}
@Component
public class EmailNotificationService implements NotificationService {
@Autowired
private EmailService emailService;
@Override
public void sendOrderConfirmation(Long userId, Long orderId) {
String email = getUserEmail(userId);
String subject = "Order Confirmation";
String body = "Your order #" + orderId + " has been confirmed.";
emailService.sendEmail(email, subject, body);
}
}
7. Архитектура на основе доменов (Domain-Driven Design)
Определение: Архитектура, ориентированная на бизнес-домен и его модели.
Компоненты:
- Domain Model — бизнес-модели
- Aggregates — консистентные группы объектов
- Value Objects — неизменяемые объекты
- Domain Services — бизнес-логика
- Repositories — доступ к данным
- Bounded Contexts — границы доменов
Преимущества:
- Фокус на бизнес-логике
- Четкие границы доменов
- Переиспользование моделей
- Легкость тестирования
Недостатки:
- Сложность для простых систем
- Кривая обучения
- Потенциальный over-engineering
- Сложность интеграции
Технический пример:
// Domain Model
public class Order {
private OrderId id;
private CustomerId customerId;
private List<OrderItem> items;
private OrderStatus status;
private Money totalAmount;
public static Order create(CustomerId customerId) {
Order order = new Order();
order.id = OrderId.generate();
order.customerId = customerId;
order.status = OrderStatus.DRAFT;
order.items = new ArrayList<>();
order.totalAmount = Money.zero();
return order;
}
public void addItem(Product product, Quantity quantity) {
if (status != OrderStatus.DRAFT) {
throw new OrderDomainException("Cannot add items to non-draft order");
}
OrderItem item = OrderItem.create(product, quantity);
items.add(item);
recalculateTotal();
}
public void confirm() {
if (items.isEmpty()) {
throw new OrderDomainException("Cannot confirm empty order");
}
if (totalAmount.isZero()) {
throw new OrderDomainException("Cannot confirm order with zero amount");
}
this.status = OrderStatus.CONFIRMED;
// Domain Event
DomainEvents.raise(new OrderConfirmedEvent(this.id, this.customerId));
}
private void recalculateTotal() {
this.totalAmount = items.stream()
.map(OrderItem::getSubtotal)
.reduce(Money.zero(), Money::add);
}
}
// Value Objects
public class Money {
private final BigDecimal amount;
private final Currency currency;
public Money(BigDecimal amount, Currency currency) {
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("Amount cannot be negative");
}
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);
}
public boolean isZero() {
return amount.compareTo(BigDecimal.ZERO) == 0;
}
}
public class OrderId {
private final String value;
private OrderId(String value) {
this.value = value;
}
public static OrderId generate() {
return new OrderId(UUID.randomUUID().toString());
}
public static OrderId of(String value) {
return new OrderId(value);
}
}
// Aggregate Root
@Entity
@Table(name = "orders")
public class OrderAggregate {
@Id
private String id;
@Embedded
private CustomerId customerId;
@Enumerated(EnumType.STRING)
private OrderStatus status;
@Embedded
private Money totalAmount;
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<OrderItemEntity> items;
@Transient
private List<DomainEvent> uncommittedEvents = new ArrayList<>();
// Domain methods
public void addItem(Product product, Quantity quantity) {
OrderItem item = OrderItem.create(product, quantity);
this.items.add(new OrderItemEntity(item));
recalculateTotal();
}
public void confirm() {
if (items.isEmpty()) {
throw new OrderDomainException("Cannot confirm empty order");
}
this.status = OrderStatus.CONFIRMED;
// Raise domain event
uncommittedEvents.add(new OrderConfirmedEvent(
OrderId.of(this.id),
this.customerId
));
}
public List<DomainEvent> getUncommittedEvents() {
return uncommittedEvents;
}
public void markEventsAsCommitted() {
uncommittedEvents.clear();
}
}
// Domain Service
@Service
public class OrderDomainService {
public boolean canAddItem(Order order, Product product) {
// Бизнес-правило: нельзя добавлять товары из разных категорий
if (order.hasItems()) {
String existingCategory = order.getFirstItemCategory();
return product.getCategory().equals(existingCategory);
}
return true;
}
public Money calculateDiscount(Order order, Customer customer) {
// Бизнес-правило: скидка для VIP клиентов
if (customer.isVip()) {
return order.getTotalAmount().multiply(0.1); // 10% скидка
}
return Money.zero();
}
}
// Repository Interface
public interface OrderRepository {
void save(OrderAggregate order);
Optional<OrderAggregate> findById(OrderId orderId);
List<OrderAggregate> findByCustomerId(CustomerId customerId);
}
// Application Service
@Service
@Transactional
public class OrderApplicationService {
private final OrderRepository orderRepository;
private final OrderDomainService orderDomainService;
private final EventPublisher eventPublisher;
public void addItemToOrder(OrderId orderId, ProductId productId, Quantity quantity) {
OrderAggregate order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException("Order not found"));
Product product = productService.getProduct(productId);
if (!orderDomainService.canAddItem(order, product)) {
throw new OrderDomainException("Cannot add item from different category");
}
order.addItem(product, quantity);
orderRepository.save(order);
// Publish domain events
order.getUncommittedEvents().forEach(eventPublisher::publish);
order.markEventsAsCommitted();
}
}
8. Архитектура на основе компонентов (Component-Based Architecture)
Определение: Система строится из переиспользуемых компонентов с четко определенными интерфейсами.
Компоненты:
- Components — переиспользуемые модули
- Interfaces — контракты взаимодействия
- Connectors — механизмы связи
- Configuration — настройка компонентов
Преимущества:
- Переиспользование кода
- Модульность
- Легкость тестирования
- Независимая разработка
Недостатки:
- Сложность проектирования интерфейсов
- Потенциальные проблемы с производительностью
- Сложность управления зависимостями
- Риск over-engineering
Технический пример:
// Component Interface
public interface PaymentComponent {
PaymentResult processPayment(PaymentRequest request);
RefundResult processRefund(RefundRequest request);
PaymentStatus getPaymentStatus(String paymentId);
}
// Component Implementation
@Component("stripePaymentComponent")
public class StripePaymentComponent implements PaymentComponent {
@Autowired
private StripeClient stripeClient;
@Override
public PaymentResult processPayment(PaymentRequest request) {
try {
Charge charge = stripeClient.charges().create(
ChargeCreateParams.builder()
.setAmount(request.getAmount().multiply(BigDecimal.valueOf(100)).longValue())
.setCurrency("usd")
.setSource(request.getToken())
.build()
);
return PaymentResult.success(charge.getId());
} catch (StripeException e) {
return PaymentResult.failure(e.getMessage());
}
}
@Override
public RefundResult processRefund(RefundRequest request) {
try {
Refund refund = stripeClient.refunds().create(
RefundCreateParams.builder()
.setCharge(request.getChargeId())
.setAmount(request.getAmount().multiply(BigDecimal.valueOf(100)).longValue())
.build()
);
return RefundResult.success(refund.getId());
} catch (StripeException e) {
return RefundResult.failure(e.getMessage());
}
}
@Override
public PaymentStatus getPaymentStatus(String paymentId) {
try {
Charge charge = stripeClient.charges().retrieve(paymentId);
return PaymentStatus.valueOf(charge.getStatus().toUpperCase());
} catch (StripeException e) {
return PaymentStatus.UNKNOWN;
}
}
}
// Component Registry
@Service
public class ComponentRegistry {
private final Map<String, PaymentComponent> paymentComponents = new HashMap<>();
@PostConstruct
public void initializeComponents() {
// Регистрация компонентов
paymentComponents.put("stripe", stripePaymentComponent);
paymentComponents.put("paypal", paypalPaymentComponent);
paymentComponents.put("square", squarePaymentComponent);
}
public PaymentComponent getPaymentComponent(String provider) {
return paymentComponents.get(provider);
}
}
// Component Manager
@Service
public class PaymentComponentManager {
@Autowired
private ComponentRegistry componentRegistry;
public PaymentResult processPayment(String provider, PaymentRequest request) {
PaymentComponent component = componentRegistry.getPaymentComponent(provider);
if (component == null) {
throw new UnsupportedPaymentProviderException("Provider not supported: " + provider);
}
return component.processPayment(request);
}
}
Сравнение архитектурных стилей
| Стиль | Сложность | Производительность | Масштабируемость | Тестируемость | Гибкость |
|---|---|---|---|---|---|
| Монолитная | Низкая | Высокая | Низкая | Высокая | Низкая |
| Микросервисы | Высокая | Средняя | Высокая | Средняя | Высокая |
| Слоистая | Средняя | Средняя | Средняя | Высокая | Средняя |
| Event-Driven | Высокая | Средняя | Высокая | Низкая | Высокая |
| CQRS | Очень высокая | Высокая | Высокая | Средняя | Высокая |
| Гексагональная | Высокая | Средняя | Средняя | Высокая | Высокая |
| DDD | Высокая | Средняя | Средняя | Высокая | Высокая |
| Component-Based | Средняя | Средняя | Средняя | Высокая | Высокая |
Выбор архитектурного стиля
Факторы для выбора:
- Размер команды — большие команды лучше подходят для микросервисов
- Сложность домена — сложные домены требуют DDD или гексагональной архитектуры
- Требования к производительности — критичная производительность может потребовать монолита
- Необходимость масштабирования — микросервисы лучше для горизонтального масштабирования
- Технологическое разнообразие — микросервисы позволяют использовать разные технологии
- Время разработки — монолит быстрее в разработке для простых систем
- Сложность тестирования — слоистая архитектура проще для тестирования
- Необходимость аудита — CQRS лучше для аудита изменений
- Высокие требования к масштабируемости
Технический пример:
User Service (отдельное приложение):
// User Service - отдельный микросервис
@SpringBootApplication
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;
@PostMapping
public ResponseEntity<User> createUser(@RequestBody User user) {
return ResponseEntity.ok(userService.createUser(user));
}
@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
return ResponseEntity.ok(userService.getUser(id));
}
}
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public User createUser(User user) {
return userRepository.save(user);
}
public User getUser(Long id) {
return userRepository.findById(id).orElse(null);
}
}
// Собственная база данных для User Service
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
// геттеры и сеттеры
}
Order Service (отдельное приложение):
// Order Service - отдельный микросервис
@SpringBootApplication
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@Autowired
private OrderService orderService;
@PostMapping
public ResponseEntity<Order> createOrder(@RequestBody Order order) {
return ResponseEntity.ok(orderService.createOrder(order));
}
@GetMapping("/{id}")
public ResponseEntity<Order> getOrder(@PathVariable Long id) {
return ResponseEntity.ok(orderService.getOrder(id));
}
}
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private UserServiceClient userServiceClient; // HTTP клиент для User Service
public Order createOrder(Order order) {
// Проверяем пользователя через HTTP вызов
User user = userServiceClient.getUser(order.getUserId());
if (user == null) {
throw new IllegalArgumentException("User not found");
}
return orderRepository.save(order);
}
public Order getOrder(Long id) {
return orderRepository.findById(id).orElse(null);
}
}
// HTTP клиент для взаимодействия с User Service
@Component
public class UserServiceClient {
@Autowired
private RestTemplate restTemplate;
private static final String USER_SERVICE_URL = "http://user-service:8080";
public User getUser(Long userId) {
try {
ResponseEntity<User> response = restTemplate.getForEntity(
USER_SERVICE_URL + "/api/users/" + userId,
User.class
);
return response.getBody();
} catch (Exception e) {
// Обработка ошибок сети
return null;
}
}
}
// Собственная база данных для Order Service
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long userId; // Ссылка на пользователя в другом сервисе
private BigDecimal amount;
// геттеры и сеттеры
}
API Gateway (для маршрутизации):
// API Gateway - точка входа для всех микросервисов
@SpringBootApplication
@EnableZuulProxy
public class ApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
}
// Конфигурация маршрутов
@Configuration
public class GatewayConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("user-service", r -> r.path("/api/users/**")
.uri("http://user-service:8080"))
.route("order-service", r -> r.path("/api/orders/**")
.uri("http://order-service:8080"))
.route("product-service", r -> r.path("/api/products/**")
.uri("http://product-service:8080"))
.build();
}
}
Docker Compose для развертывания:
version: '3.8'
services:
user-service:
build: ./user-service
ports:
- "8081:8080"
environment:
- SPRING_DATASOURCE_URL=jdbc:postgresql://user-db:5432/userdb
depends_on:
- user-db
order-service:
build: ./order-service
ports:
- "8082:8080"
environment:
- SPRING_DATASOURCE_URL=jdbc:postgresql://order-db:5432/orderdb
- USER_SERVICE_URL=http://user-service:8080
depends_on:
- order-db
product-service:
build: ./product-service
ports:
- "8083:8080"
environment:
- SPRING_DATASOURCE_URL=jdbc:postgresql://product-db:5432/productdb
depends_on:
- product-db
api-gateway:
build: ./api-gateway
ports:
- "8080:8080"
depends_on:
- user-service
- order-service
- product-service
user-db:
image: postgres:13
environment:
- POSTGRES_DB=userdb
- POSTGRES_USER=user
- POSTGRES_PASSWORD=password
order-db:
image: postgres:13
environment:
- POSTGRES_DB=orderdb
- POSTGRES_USER=order
- POSTGRES_PASSWORD=password
product-db:
image: postgres:13
environment:
- POSTGRES_DB=productdb
- POSTGRES_USER=product
- POSTGRES_PASSWORD=password
3. Service-Oriented Architecture (SOA)
Определение: Архитектура, основанная на сервисах
Характеристики:
- Сервисы как основные компоненты
- Стандартизированные интерфейсы
- Переиспользование сервисов
- Слабая связанность
4. Event-Driven Architecture
Определение: Архитектура, основанная на событиях
Компоненты:
- Event Producers (производители событий)
- Event Consumers (потребители событий)
- Event Bus (шина событий)
Преимущества:
- Асинхронная обработка
- Слабая связанность
- Масштабируемость
- Отказоустойчивость
Архитектурные паттерны
1. Layered Architecture (Слоистая архитектура)
┌─────────────────┐
│ Presentation │ ← UI Layer
├─────────────────┤
│ Business │ ← Business Logic
├─────────────────┤
│ Persistence │ ← Data Access
└─────────────────┘
Принципы:
- Каждый слой зависит только от нижележащего
- Запрет на обход слоев
- Четкое разделение ответственности
2. Hexagonal Architecture (Порты и адаптеры)
┌─────────────┐
│ Adapters │ ← Внешние интерфейсы
└─────────────┘
│
┌─────────────┐
│ Ports │ ← Интерфейсы
└─────────────┘
│
┌─────────────┐
│ Core │ ← Бизнес-логика
└─────────────┘
Преимущества:
- Независимость от внешних систем
- Легкое тестирование
- Гибкость в выборе технологий
3. CQRS (Command Query Responsibility Segregation)
Разделение команд и запросов
Команды → Модель записи → Хранилище событий
Запросы → Модель чтения → База данных чтения
Когда использовать:
- Разные требования к чтению и записи
- Сложные запросы
- Высокая нагрузка на чтение
4. Event Sourcing
Хранение событий вместо состояния
События → Хранилище событий → Проекции → Модели чтения
Преимущества:
- Полная история изменений
- Возможность воспроизведения состояния
- Аудит и отладка
Качественные атрибуты
Определение
Качественные атрибуты — это свойства системы, которые определяют её качество с точки зрения пользователей и стейкхолдеров.
Классификация качественных атрибутов
1. Функциональные требования
- Что система должна делать
- Описывают функциональность
- Примеры: аутентификация, обработка платежей
2. Нефункциональные требования (NFR)
- Как система должна работать
- Описывают качество системы
- Влияют на архитектурные решения
Основные качественные атрибуты
Производительность (Performance)
Определение: Скорость и эффективность выполнения операций
Метрики:
- Время отклика (Response Time)
- Пропускная способность (Throughput)
- Использование ресурсов (Resource Utilization)
Архитектурные тактики:
- Кэширование
- Пулы соединений
- Асинхронная обработка
- Оптимизация запросов
Масштабируемость (Scalability)
Определение: Способность системы обрабатывать увеличение нагрузки
Типы масштабирования:
- Горизонтальное (Scale Out) — добавление серверов
- Вертикальное (Scale Up) — увеличение мощности сервера
Паттерны:
- Load Balancing
- Database Sharding
- Microservices
- CDN
Надежность (Reliability)
Определение: Способность системы работать без сбоев
Метрики:
- MTBF (Mean Time Between Failures)
- MTTR (Mean Time To Repair)
- Availability (Доступность)
Тактики:
- Redundancy (Избыточность)
- Fault Tolerance (Отказоустойчивость)
- Health Checks
- Circuit Breaker
Безопасность (Security)
Определение: Защита системы от угроз
Аспекты:
- Аутентификация
- Авторизация
- Шифрование
- Аудит
Принципы:
- Defense in Depth
- Least Privilege
- Security by Design
Удобство использования (Usability)
Определение: Простота использования системы
Факторы:
- Интуитивность интерфейса
- Производительность пользователя
- Обучаемость
- Доступность
Совместимость (Compatibility)
Определение: Способность работать с другими системами
Типы:
- Обратная совместимость
- Форвардная совместимость
- Межплатформенная совместимость
Переносимость (Portability)
Определение: Легкость переноса системы на другие платформы
Факторы:
- Независимость от ОС
- Стандартизированные интерфейсы
- Абстракция от аппаратуры
Тестируемость (Testability)
Определение: Легкость тестирования системы
Принципы:
- Dependency Injection
- Mocking
- Unit Testing
- Integration Testing
Поддерживаемость (Maintainability)
Определение: Легкость внесения изменений
Аспекты:
- Читаемость кода
- Документирование
- Модульность
- Рефакторинг
Trade-offs между атрибутами
Часто улучшение одного атрибута ухудшает другой:
- Производительность vs Безопасность — шифрование замедляет работу
- Масштабируемость vs Простота — распределенные системы сложнее
- Надежность vs Производительность — репликация добавляет overhead
- Безопасность vs Удобство — сложные пароли неудобны
Архитектурные тактики
Тактика — это архитектурное решение для достижения качественного атрибута.
Тактики для производительности:
-
Управление ресурсами
- Увеличение ресурсов
- Уменьшение накладных расходов
- Управление ресурсами
-
Управление запросами
- Приоритизация запросов
- Уменьшение времени обработки
- Управление событиями
Тактики для надежности:
-
Предотвращение ошибок
- Удаление точек отказа
- Транзакции
- Валидация
-
Обнаружение ошибок
- Ping/Echo
- Heartbeat
- Exception handling
-
Восстановление после ошибок
- Voting
- Active redundancy
- Passive redundancy
Роль архитектора в команде
Определение роли архитектора
Архитектор ПО — это специалист, который:
- Проектирует структуру системы
- Принимает ключевые технические решения
- Обеспечивает соответствие архитектуры требованиям
- Руководит техническим развитием проекта
Обязанности архитектора
1. Проектирование архитектуры
- Анализ требований
- Выбор архитектурных решений
- Проектирование компонентов
- Определение интерфейсов
2. Техническое лидерство
- Наставничество разработчиков
- Code review
- Техническое планирование
- Управление техническим долгом
3. Коммуникация
- Взаимодействие с бизнесом
- Объяснение технических решений
- Презентация архитектуры
- Документирование решений
4. Контроль качества
- Архитектурные ревью
- Соответствие стандартам
- Мониторинг метрик
- Управление рисками
Навыки архитектора
Технические навыки
- Глубокое понимание технологий
- Знание архитектурных паттернов
- Опыт работы с различными платформами
- Понимание качественных атрибутов
Мягкие навыки
- Лидерские качества
- Коммуникативные навыки
- Аналитическое мышление
- Принятие решений
Бизнес-навыки
- Понимание бизнес-процессов
- Анализ требований
- Управление проектами
- Работа со стейкхолдерами
Типы архитекторов
1. Solution Architect
- Проектирует решения для конкретных бизнес-задач
- Работает с бизнес-требованиями
- Фокусируется на интеграции систем
2. Enterprise Architect
- Проектирует архитектуру на уровне предприятия
- Стратегическое планирование
- Управление технологическим портфелем
3. Technical Architect
- Фокусируется на технических аспектах
- Выбор технологий
- Проектирование компонентов
4. Cloud Architect
- Специализируется на облачных решениях
- Проектирование cloud-native приложений
- Оптимизация облачных ресурсов
Закон Конвея
"Организации проектируют системы, которые копируют структуру коммуникации в организации"
Следствия:
- Структура команды влияет на архитектуру
- Необходимо учитывать организационную структуру
- Команды должны быть организованы по доменам
Примеры:
- Команда по фронтенду → отдельный фронтенд-сервис
- Команда по платежам → отдельный платежный сервис
- Команда по аналитике → отдельный аналитический сервис
Архитектурные ревью
Цель: Оценка соответствия архитектуры требованиям
Типы ревью:
- Архитектурный анализ — оценка текущей архитектуры
- Архитектурная оценка — оценка предлагаемых изменений
- Архитектурный аудит — проверка соответствия стандартам
Процесс ревью:
- Подготовка материалов
- Презентация архитектуры
- Обсуждение и вопросы
- Выработка рекомендаций
- Документирование результатов
Управление техническим долгом
Технический долг — это компромиссы в качестве кода, которые делаются для ускорения разработки.
Типы технического долга:
- Устаревшие технологии
- Неоптимальная архитектура
- Отсутствие тестов
- Плохая документация
Стратегии управления:
- Регулярный рефакторинг
- Планирование технических спринтов
- Мониторинг метрик качества
- Обучение команды
Практические задания
Задание 1: Анализ архитектуры
Проанализируйте архитектуру известной системы (например, Twitter, Netflix, Uber) и определите:
- Архитектурный стиль
- Основные компоненты
- Качественные атрибуты
- Архитектурные решения
Задание 2: Применение SOLID принципов
Возьмите пример кода с нарушениями SOLID принципов и рефакторите его согласно принципам.
Задание 3: Trade-off анализ
Рассмотрите конкретный случай, где нужно выбрать между двумя качественными атрибутами, и обоснуйте выбор.
Дополнительные ресурсы
Книги
- "Software Architecture in Practice" by Len Bass, Paul Clements, Rick Kazman
- "Clean Architecture" by Robert C. Martin
- "Building Microservices" by Sam Newman
- "Domain-Driven Design" by Eric Evans
Стандарты
- IEEE 1471-2000: Recommended Practice for Architectural Description
- ISO/IEC 42010: Systems and software engineering — Architecture description
Онлайн ресурсы
Контрольные вопросы
- Что такое архитектура ПО и зачем она нужна?
- Объясните принципы SOLID с примерами.
- Какие архитектурные стили вы знаете? В чем их различия?
- Что такое качественные атрибуты? Приведите примеры.
- Какова роль архитектора в команде?
- Что такое Закон Конвея и как он влияет на архитектуру?
- Как управлять техническим долгом?
- Какие trade-offs существуют между качественными атрибутами?