- 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
6366 lines
198 KiB
Markdown
6366 lines
198 KiB
Markdown
# Модуль 1: Основы архитектуры программного обеспечения
|
|
|
|
## Содержание модуля
|
|
1. [Роль и обязанности Solution Architect](#роль-и-обязанности-solution-architect)
|
|
2. [Введение в архитектуру ПО](#введение-в-архитектуру-по)
|
|
3. [Доменная модель и Domain-Driven Design](#доменная-модель-и-domain-driven-design)
|
|
4. [Принципы проектирования](#принципы-проектирования)
|
|
5. [Архитектурные стили и паттерны](#архитектурные-стили-и-паттерны)
|
|
6. [Качественные атрибуты](#качественные-атрибуты)
|
|
7. [Роль архитектора в команде](#роль-архитектора-в-команде)
|
|
|
|
---
|
|
|
|
## Роль и обязанности 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-директора |
|
|
|
|
**Пример различий:**
|
|
|
|
```java
|
|
// 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)
|
|
|
|
**Процесс анализа:**
|
|
|
|
```java
|
|
// Пример анализа требований
|
|
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)
|
|
|
|
**Этапы проектирования:**
|
|
|
|
1. **Высокоуровневое проектирование**
|
|
- Определение основных компонентов
|
|
- Выбор архитектурного стиля
|
|
- Определение интерфейсов
|
|
|
|
2. **Детальное проектирование**
|
|
- Проектирование компонентов
|
|
- Выбор технологий
|
|
- Определение паттернов взаимодействия
|
|
|
|
3. **Валидация архитектуры**
|
|
- Проверка соответствия требованиям
|
|
- Анализ рисков
|
|
- Прототипирование критических компонентов
|
|
|
|
#### 3. Документирование (Documentation)
|
|
|
|
**Типы архитектурной документации:**
|
|
|
|
```markdown
|
|
# 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)
|
|
|
|
**Определение:** Способность системы обрабатывать увеличение нагрузки.
|
|
|
|
**Метрики:**
|
|
- Количество одновременных пользователей
|
|
- Объем обрабатываемых данных
|
|
- Количество транзакций в секунду
|
|
|
|
**Примеры требований:**
|
|
```yaml
|
|
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
|
|
|
|
#### Технические навыки
|
|
|
|
1. **Архитектурные паттерны и стили**
|
|
- Микросервисы, SOA, Event-driven
|
|
- CQRS, Event Sourcing, Saga
|
|
- API Gateway, Service Mesh
|
|
|
|
2. **Облачные технологии**
|
|
- AWS, Azure, GCP
|
|
- Containerization (Docker, Kubernetes)
|
|
- Serverless архитектура
|
|
|
|
3. **Интеграционные технологии**
|
|
- REST, GraphQL, gRPC
|
|
- Message Queues (RabbitMQ, Kafka)
|
|
- API Management
|
|
|
|
#### Мягкие навыки
|
|
|
|
1. **Коммуникация**
|
|
- Презентация технических решений
|
|
- Взаимодействие с бизнесом
|
|
- Техническое письмо
|
|
|
|
2. **Лидерство**
|
|
- Техническое лидерство
|
|
- Менторство
|
|
- Управление конфликтами
|
|
|
|
3. **Аналитическое мышление**
|
|
- Анализ требований
|
|
- Принятие решений
|
|
- Управление рисками
|
|
|
|
### Карьерный путь Solution Architect
|
|
|
|
#### Уровни развития
|
|
|
|
1. **Junior Solution Architect** (2-3 года опыта)
|
|
- Помощь в проектировании решений
|
|
- Изучение архитектурных паттернов
|
|
- Работа под руководством Senior Architect
|
|
|
|
2. **Solution Architect** (3-7 лет опыта)
|
|
- Независимое проектирование решений
|
|
- Взаимодействие с бизнесом
|
|
- Техническое лидерство команды
|
|
|
|
3. **Senior Solution Architect** (7+ лет опыта)
|
|
- Сложные enterprise решения
|
|
- Архитектурная стратегия
|
|
- Менторство других архитекторов
|
|
|
|
4. **Principal/Lead Solution Architect** (10+ лет опыта)
|
|
- Архитектурная стратегия компании
|
|
- Управление архитектурной командой
|
|
- Влияние на технологическую стратегию
|
|
|
|
---
|
|
|
|
## Введение в архитектуру ПО
|
|
|
|
### Определение архитектуры ПО
|
|
|
|
**Архитектура программного обеспечения** — это структура системы, включающая:
|
|
- Компоненты системы и их взаимодействие
|
|
- Внешние интерфейсы
|
|
- Ограничения и требования
|
|
- Принципы проектирования
|
|
|
|
### Ключевые характеристики архитектуры
|
|
|
|
1. **Структурность** — архитектура определяет структуру системы
|
|
2. **Абстрактность** — скрывает детали реализации
|
|
3. **Системность** — рассматривает систему как целое
|
|
4. **Эволюционность** — может изменяться со временем
|
|
|
|
### Зачем нужна архитектура?
|
|
|
|
- **Управление сложностью** — разбиение сложной системы на понятные части
|
|
- **Коммуникация** — общий язык для обсуждения системы
|
|
- **Планирование** — основа для принятия технических решений
|
|
- **Переиспользование** — возможность использования компонентов в других проектах
|
|
|
|
### Архитектурные решения
|
|
|
|
**Архитектурное решение** — это выбор, который:
|
|
- Влияет на структуру системы
|
|
- Имеет долгосрочные последствия
|
|
- Трудно изменить в будущем
|
|
- Влияет на качественные атрибуты
|
|
|
|
### Примеры архитектурных решений:
|
|
|
|
#### 1. Выбор между монолитом и микросервисами
|
|
```java
|
|
// Монолитная архитектура
|
|
@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)
|
|
```java
|
|
// 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)
|
|
```java
|
|
// 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. Выбор стратегии кэширования
|
|
```java
|
|
// 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)** — это предметная область, в которой работает приложение. Это сфера знаний, правил и процессов, которые определяют, как работает бизнес.
|
|
|
|
#### Ключевые характеристики домена:
|
|
|
|
1. **Бизнес-логика** — правила и процессы, специфичные для предметной области
|
|
2. **Терминология** — язык, используемый экспертами в данной области
|
|
3. **Ограничения** — правила и ограничения, которые должны соблюдаться
|
|
4. **Процессы** — последовательности действий, выполняемых в домене
|
|
|
|
### Domain-Driven Design (DDD)
|
|
|
|
**Domain-Driven Design** — подход к разработке программного обеспечения, который фокусируется на моделировании предметной области.
|
|
|
|
#### Основные принципы DDD:
|
|
|
|
1. **Фокус на домене** — архитектура отражает структуру домена
|
|
2. **Единый язык** — общий язык между разработчиками и экспертами
|
|
3. **Модель-код соответствие** — код отражает доменную модель
|
|
4. **Итеративное уточнение** — постоянное улучшение понимания домена
|
|
|
|
### Строительные блоки DDD
|
|
|
|
#### 1. Entity (Сущность)
|
|
|
|
**Entity** — объект, который имеет уникальную идентичность и жизненный цикл.
|
|
|
|
```java
|
|
// Пример 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** — объект, который определяется только своими атрибутами, без идентичности.
|
|
|
|
```java
|
|
// Пример 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** — кластер связанных объектов, которые рассматриваются как единое целое.
|
|
|
|
```java
|
|
// Пример 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** — сервис, который содержит бизнес-логику, не принадлежащую ни одной сущности.
|
|
|
|
```java
|
|
// Пример 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** — интерфейс для доступа к агрегатам.
|
|
|
|
```java
|
|
// Интерфейс репозитория
|
|
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:
|
|
|
|
1. **Order Management** — управление заказами
|
|
2. **Customer Management** — управление клиентами
|
|
3. **Inventory Management** — управление складом
|
|
4. **Payment Processing** — обработка платежей
|
|
|
|
```java
|
|
// 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:
|
|
|
|
```java
|
|
// Термины домена
|
|
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:
|
|
|
|
1. **Доменные события** — что происходит в системе
|
|
2. **Команды** — что вызывает события
|
|
3. **Агрегаты** — что обрабатывает команды
|
|
4. **Политики** — бизнес-правила
|
|
5. **Внешние системы** — интеграции
|
|
|
|
```markdown
|
|
# Event Storming для E-commerce
|
|
|
|
## Доменные события
|
|
- OrderCreated (Заказ создан)
|
|
- OrderConfirmed (Заказ подтвержден)
|
|
- PaymentProcessed (Платеж обработан)
|
|
- OrderShipped (Заказ отправлен)
|
|
- OrderDelivered (Заказ доставлен)
|
|
|
|
## Команды
|
|
- CreateOrder (Создать заказ)
|
|
- ConfirmOrder (Подтвердить заказ)
|
|
- ProcessPayment (Обработать платеж)
|
|
- ShipOrder (Отправить заказ)
|
|
|
|
## Агрегаты
|
|
- Order (Заказ)
|
|
- Customer (Клиент)
|
|
- Product (Товар)
|
|
- Payment (Платеж)
|
|
|
|
## Политики
|
|
- Скидка за количество товаров
|
|
- VIP статус клиента
|
|
- Праздничные скидки
|
|
```
|
|
|
|
### Антипаттерны DDD
|
|
|
|
#### 1. Anemic Domain Model (Анемичная доменная модель)
|
|
|
|
**Проблема:** Доменные объекты содержат только данные без поведения.
|
|
|
|
```java
|
|
// Плохо: Анемичная модель
|
|
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 (Божественный объект)
|
|
|
|
**Проблема:** Один объект содержит слишком много ответственности.
|
|
|
|
```java
|
|
// Плохо: 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
|
|
|
|
```java
|
|
// 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
|
|
|
|
```java
|
|
// 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. Начните с доменных событий
|
|
|
|
```java
|
|
// Сначала определите события
|
|
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 для примитивов
|
|
|
|
```java
|
|
// Вместо 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. Следите за границами агрегатов
|
|
|
|
```java
|
|
// Правильно: Агрегат управляет только своими данными
|
|
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 помогает создавать архитектуру, которая:
|
|
|
|
1. **Отражает бизнес-логику** — код соответствует домену
|
|
2. **Улучшает коммуникацию** — единый язык между командами
|
|
3. **Повышает качество** — четкие границы и ответственности
|
|
4. **Упрощает тестирование** — изолированная доменная логика
|
|
5. **Обеспечивает эволюцию** — гибкая архитектура для изменений
|
|
|
|
DDD особенно эффективен для сложных бизнес-доменов, где важно точное понимание предметной области и создание архитектуры, которая может эволюционировать вместе с бизнесом.
|
|
|
|
---
|
|
|
|
## Принципы проектирования
|
|
|
|
### SOLID принципы
|
|
|
|
#### 1. Single Responsibility Principle (SRP)
|
|
**Принцип единственной ответственности**
|
|
|
|
> Класс должен иметь только одну причину для изменения
|
|
|
|
**Пример нарушения:**
|
|
```java
|
|
// Нарушение 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) {
|
|
// Аналогично много ответственностей...
|
|
}
|
|
}
|
|
```
|
|
|
|
**Правильная реализация:**
|
|
```java
|
|
// Разделение ответственностей
|
|
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)
|
|
**Принцип открытости/закрытости**
|
|
|
|
> Программные сущности должны быть открыты для расширения, но закрыты для модификации
|
|
|
|
**Пример нарушения:**
|
|
```java
|
|
// Нарушение 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) {
|
|
// логика обработки банковского перевода
|
|
}
|
|
}
|
|
```
|
|
|
|
**Правильная реализация:**
|
|
```java
|
|
// Соблюдение 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)
|
|
**Принцип подстановки Лисков**
|
|
|
|
> Объекты производного класса должны быть заменяемы объектами базового класса
|
|
|
|
**Пример нарушения:**
|
|
```java
|
|
// Нарушение 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!
|
|
}
|
|
```
|
|
|
|
**Правильная реализация:**
|
|
```java
|
|
// Соблюдение 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)
|
|
**Принцип разделения интерфейсов**
|
|
|
|
> Клиенты не должны зависеть от интерфейсов, которые они не используют
|
|
|
|
**Пример нарушения:**
|
|
```java
|
|
// Нарушение 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() { /* не используется */ }
|
|
}
|
|
```
|
|
|
|
**Правильная реализация:**
|
|
```java
|
|
// Соблюдение 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)
|
|
**Принцип инверсии зависимостей**
|
|
|
|
> Модули высокого уровня не должны зависеть от модулей низкого уровня
|
|
|
|
**Пример нарушения:**
|
|
```java
|
|
// Нарушение 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"
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
**Правильная реализация:**
|
|
```java
|
|
// Соблюдение 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 (Не повторяйся)
|
|
**Не повторяйся**
|
|
|
|
- Избегайте дублирования кода
|
|
- Выносите общую логику в отдельные функции/классы
|
|
- Используйте абстракции для переиспользования
|
|
|
|
**Пример нарушения:**
|
|
```java
|
|
// Нарушение 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");
|
|
}
|
|
// обновление пользователя
|
|
}
|
|
}
|
|
```
|
|
|
|
**Правильная реализация:**
|
|
```java
|
|
// Соблюдение 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 (Будь проще)
|
|
**Делай проще**
|
|
|
|
- Простые решения часто лучше сложных
|
|
- Избегайте преждевременной оптимизации
|
|
- Читаемость кода важнее "умности"
|
|
|
|
**Пример нарушения:**
|
|
```java
|
|
// Нарушение 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;
|
|
}
|
|
}
|
|
```
|
|
|
|
**Правильная реализация:**
|
|
```java
|
|
// Соблюдение 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
|
|
|
|
**Пример нарушения:**
|
|
```java
|
|
// Нарушение 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);
|
|
}
|
|
}
|
|
```
|
|
|
|
**Правильная реализация:**
|
|
```java
|
|
// Соблюдение 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)
|
|
- Компоненты минимально зависят друг от друга
|
|
- Изменения в одном компоненте не влияют на другие
|
|
- Легче тестировать и поддерживать
|
|
|
|
**Пример нарушения:**
|
|
```java
|
|
// Нарушение - тесная связанность
|
|
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());
|
|
}
|
|
}
|
|
```
|
|
|
|
**Правильная реализация:**
|
|
```java
|
|
// Соблюдение - слабая связанность
|
|
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)
|
|
- Элементы внутри модуля тесно связаны
|
|
- Модуль имеет четко определенную ответственность
|
|
- Все функции модуля работают для достижения одной цели
|
|
|
|
**Пример нарушения:**
|
|
```java
|
|
// Нарушение - низкая сплоченность
|
|
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) {
|
|
// Сжатие файла
|
|
}
|
|
}
|
|
```
|
|
|
|
**Правильная реализация:**
|
|
```java
|
|
// Соблюдение - высокая сплоченность
|
|
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. Монолитная архитектура
|
|
**Определение:** Вся система реализована как единое приложение
|
|
|
|
**Преимущества:**
|
|
- Простота разработки и развертывания
|
|
- Легкое тестирование
|
|
- Простая отладка
|
|
- Низкая сложность сети
|
|
|
|
**Недостатки:**
|
|
- Сложность масштабирования
|
|
- Технологическая привязка
|
|
- Сложность обновлений
|
|
- Риск каскадных отказов
|
|
|
|
**Когда использовать:**
|
|
- Небольшие команды
|
|
- Простые приложения
|
|
- Быстрое прототипирование
|
|
|
|
**Технический пример:**
|
|
```java
|
|
// Монолитное приложение - все в одном проекте
|
|
@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):**
|
|
```java
|
|
// 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):**
|
|
```rust
|
|
// 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):**
|
|
```python
|
|
# 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):**
|
|
```typescript
|
|
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):**
|
|
```java
|
|
// 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):**
|
|
```rust
|
|
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):**
|
|
```python
|
|
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):**
|
|
```typescript
|
|
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):**
|
|
```java
|
|
// 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):**
|
|
```rust
|
|
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):**
|
|
```python
|
|
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):**
|
|
```typescript
|
|
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
|
|
- Кривая обучения
|
|
- Больше кода
|
|
|
|
**Технический пример:**
|
|
```java
|
|
// 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
|
|
- Сложность интеграции
|
|
|
|
**Технический пример:**
|
|
```java
|
|
// 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
|
|
|
|
**Технический пример:**
|
|
```java
|
|
// 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 | Средняя | Средняя | Средняя | Высокая | Высокая |
|
|
|
|
### Выбор архитектурного стиля
|
|
|
|
**Факторы для выбора:**
|
|
|
|
1. **Размер команды** — большие команды лучше подходят для микросервисов
|
|
2. **Сложность домена** — сложные домены требуют DDD или гексагональной архитектуры
|
|
3. **Требования к производительности** — критичная производительность может потребовать монолита
|
|
4. **Необходимость масштабирования** — микросервисы лучше для горизонтального масштабирования
|
|
5. **Технологическое разнообразие** — микросервисы позволяют использовать разные технологии
|
|
6. **Время разработки** — монолит быстрее в разработке для простых систем
|
|
7. **Сложность тестирования** — слоистая архитектура проще для тестирования
|
|
8. **Необходимость аудита** — CQRS лучше для аудита изменений
|
|
- Высокие требования к масштабируемости
|
|
|
|
**Технический пример:**
|
|
|
|
**User Service (отдельное приложение):**
|
|
```java
|
|
// 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 (отдельное приложение):**
|
|
```java
|
|
// 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 (для маршрутизации):**
|
|
```java
|
|
// 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 для развертывания:**
|
|
```yaml
|
|
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 Удобство** — сложные пароли неудобны
|
|
|
|
### Архитектурные тактики
|
|
|
|
**Тактика** — это архитектурное решение для достижения качественного атрибута.
|
|
|
|
#### Тактики для производительности:
|
|
1. **Управление ресурсами**
|
|
- Увеличение ресурсов
|
|
- Уменьшение накладных расходов
|
|
- Управление ресурсами
|
|
|
|
2. **Управление запросами**
|
|
- Приоритизация запросов
|
|
- Уменьшение времени обработки
|
|
- Управление событиями
|
|
|
|
#### Тактики для надежности:
|
|
1. **Предотвращение ошибок**
|
|
- Удаление точек отказа
|
|
- Транзакции
|
|
- Валидация
|
|
|
|
2. **Обнаружение ошибок**
|
|
- Ping/Echo
|
|
- Heartbeat
|
|
- Exception handling
|
|
|
|
3. **Восстановление после ошибок**
|
|
- 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. **Архитектурный анализ** — оценка текущей архитектуры
|
|
2. **Архитектурная оценка** — оценка предлагаемых изменений
|
|
3. **Архитектурный аудит** — проверка соответствия стандартам
|
|
|
|
**Процесс ревью:**
|
|
1. Подготовка материалов
|
|
2. Презентация архитектуры
|
|
3. Обсуждение и вопросы
|
|
4. Выработка рекомендаций
|
|
5. Документирование результатов
|
|
|
|
### Управление техническим долгом
|
|
|
|
**Технический долг** — это компромиссы в качестве кода, которые делаются для ускорения разработки.
|
|
|
|
**Типы технического долга:**
|
|
- Устаревшие технологии
|
|
- Неоптимальная архитектура
|
|
- Отсутствие тестов
|
|
- Плохая документация
|
|
|
|
**Стратегии управления:**
|
|
- Регулярный рефакторинг
|
|
- Планирование технических спринтов
|
|
- Мониторинг метрик качества
|
|
- Обучение команды
|
|
|
|
---
|
|
|
|
## Практические задания
|
|
|
|
### Задание 1: Анализ архитектуры
|
|
Проанализируйте архитектуру известной системы (например, Twitter, Netflix, Uber) и определите:
|
|
1. Архитектурный стиль
|
|
2. Основные компоненты
|
|
3. Качественные атрибуты
|
|
4. Архитектурные решения
|
|
|
|
### Задание 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
|
|
|
|
### Онлайн ресурсы
|
|
- [Software Architecture Guide](https://martinfowler.com/architecture/)
|
|
- [Architecture Decision Records](https://adr.github.io/)
|
|
- [C4 Model](https://c4model.com/)
|
|
|
|
---
|
|
|
|
## Контрольные вопросы
|
|
|
|
1. Что такое архитектура ПО и зачем она нужна?
|
|
2. Объясните принципы SOLID с примерами.
|
|
3. Какие архитектурные стили вы знаете? В чем их различия?
|
|
4. Что такое качественные атрибуты? Приведите примеры.
|
|
5. Какова роль архитектора в команде?
|
|
6. Что такое Закон Конвея и как он влияет на архитектуру?
|
|
7. Как управлять техническим долгом?
|
|
8. Какие trade-offs существуют между качественными атрибутами?
|