Стратегии декомпозиции
Проблема декомпозиции
Неправильная декомпозиция -- главная причина провала микросервисных проектов. Если границы сервисов проведены неправильно, каждое изменение затрагивает несколько сервисов, а коммуникация между ними становится узким местом.
Domain-Driven Design (DDD)
DDD предоставляет методологию определения границ сервисов через анализ бизнес-домена.
Стратегические паттерны
| Концепция | Описание |
|---|---|
| Domain | Предметная область бизнеса |
| Subdomain | Часть домена (core, supporting, generic) |
| Bounded Context | Граница, внутри которой модель единообразна |
| Ubiquitous Language | Единый язык между разработчиками и бизнесом |
| Context Map | Карта связей между bounded contexts |
Типы поддоменов
| Тип | Описание | Пример (E-commerce) | Стратегия |
|---|---|---|---|
| Core | Конкурентное преимущество | Рекомендации, ценообразование | Делать самим, лучшая команда |
| Supporting | Поддерживает core, но не уникально | Каталог, отзывы | Делать самим или аутсорс |
| Generic | Стандартные решения | Авторизация, рассылки | Купить готовое решение |
Bounded Context = Граница сервиса
<?php
declare(strict_types=1);
/**
* Different Bounded Contexts have different models
* for the same real-world entity
*/
// Bounded Context: Catalog
namespace App\Catalog;
final readonly class Product
{
public function __construct(
public string $id,
public string $name,
public string $description,
public string $category,
public array $images,
public array $specifications,
) {}
}
// Bounded Context: Orders
namespace App\Orders;
// Same "Product" but completely different model!
final readonly class OrderItem
{
public function __construct(
public string $productId, // Reference only
public string $productName, // Snapshot at order time
public float $price, // Price at order time (may differ from catalog)
public int $quantity,
) {}
}
// Bounded Context: Inventory
namespace App\Inventory;
// Again "Product" but from inventory perspective
final readonly class StockItem
{
public function __construct(
public string $sku, // SKU, not product ID
public int $quantity,
public string $warehouse,
public int $reservedCount,
) {}
}
Ключевой инсайт: один и тот же объект реального мира (Product) имеет разные модели в разных контекстах. Не пытайтесь создать единую модель.
Context Map
┌────────────┐ ┌────────────┐
│ Catalog │ ──OHS──>│ Orders │
│ Context │ │ Context │
└────────────┘ └─────┬──────┘
│
ACL │
│
┌────────────┐ ┌─────┴──────┐
│ Payments │ <──CF── │ Inventory │
│ Context │ │ Context │
└────────────┘ └────────────┘
OHS = Open Host Service (публичный API)
ACL = Anti-Corruption Layer (адаптер)
CF = Conformist (принимает модель другого контекста)
<?php
declare(strict_types=1);
/**
* Anti-Corruption Layer: translate between bounded contexts
*/
namespace App\Orders\Infrastructure;
use App\Orders\Domain\ProductSnapshot;
final class CatalogAntiCorruptionLayer
{
public function __construct(
private readonly CatalogApiClient $catalogClient,
) {}
/**
* Translate Catalog's model to Orders' model
* This prevents Catalog's changes from breaking Orders
*/
public function getProductSnapshot(string $productId): ProductSnapshot
{
// External model (Catalog context)
$catalogProduct = $this->catalogClient->getProduct($productId);
// Translate to internal model (Orders context)
return new ProductSnapshot(
productId: $catalogProduct['id'],
name: $catalogProduct['title'], // Different field name!
price: (float) $catalogProduct['prices']['retail'], // Nested structure
currency: $catalogProduct['prices']['currency'],
imageUrl: $catalogProduct['media'][0]['url'] ?? null,
);
}
}
Стратегии декомпозиции
1. По бизнес-домену (рекомендуемый)
Каждый сервис соответствует бизнес-возможности (business capability):
E-commerce platform:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Catalog │ │ Orders │ │ Payments │
│ Service │ │ Service │ │ Service │
│ │ │ │ │ │
│ - Products │ │ - Cart │ │ - Charges │
│ - Search │ │ - Checkout │ │ - Refunds │
│ - Reviews │ │ - History │ │ - Billing │
└─────────────┘ └─────────────┘ └─────────────┘
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Shipping │ │ Users │ │Notifications│
│ Service │ │ Service │ │ Service │
│ │ │ │ │ │
│ - Tracking │ │ - Auth │ │ - Email │
│ - Rates │ │ - Profiles │ │ - Push │
│ - Labels │ │ - Prefs │ │ - SMS │
└─────────────┘ └─────────────┘ └─────────────┘
2. По данным (database per service)
Сервис владеет своими данными. Другие сервисы обращаются только через API.
<?php
declare(strict_types=1);
/**
* Each service owns its data exclusively
*/
// Order Service: owns orders table
namespace App\OrderService;
final class OrderRepository
{
public function __construct(
private readonly \PDO $orderDb, // Separate database
) {}
public function findById(string $id): ?array
{
$stmt = $this->orderDb->prepare('SELECT * FROM orders WHERE id = :id');
$stmt->execute(['id' => $id]);
return $stmt->fetch(\PDO::FETCH_ASSOC) ?: null;
}
// This service NEVER reads from users or products tables directly
// It calls other services' APIs to get that data
}
// User Service: owns users table
namespace App\UserService;
final class UserRepository
{
public function __construct(
private readonly \PDO $userDb, // Different database
) {}
public function findById(string $id): ?array
{
$stmt = $this->userDb->prepare('SELECT * FROM users WHERE id = :id');
$stmt->execute(['id' => $id]);
return $stmt->fetch(\PDO::FETCH_ASSOC) ?: null;
}
}
3. Пример модульной структуры PHP-проекта
<?php
declare(strict_types=1);
/**
* Modular monolith structure (step before microservices)
*
* src/
* ├── Module/
* │ ├── Catalog/
* │ │ ├── Api/ # Public API (interface)
* │ │ │ ├── CatalogModuleApi.php
* │ │ │ └── Dto/
* │ │ │ └── ProductDto.php
* │ │ ├── Domain/ # Business logic
* │ │ │ ├── Product.php
* │ │ │ └── ProductRepository.php
* │ │ ├── Infrastructure/ # DB, external services
* │ │ │ ├── PostgresProductRepository.php
* │ │ │ └── ElasticsearchProductSearch.php
* │ │ └── Application/ # Use cases
* │ │ ├── CreateProduct.php
* │ │ └── SearchProducts.php
* │ │
* │ ├── Orders/
* │ │ ├── Api/
* │ │ │ └── OrderModuleApi.php
* │ │ ├── Domain/
* │ │ ├── Infrastructure/
* │ │ │ └── CatalogModuleAdapter.php # Uses CatalogModuleApi
* │ │ └── Application/
* │ │
* │ └── Payments/
* │ ├── Api/
* │ ├── Domain/
* │ ├── Infrastructure/
* │ └── Application/
*/
// Module API: the ONLY way to communicate between modules
namespace App\Module\Catalog\Api;
interface CatalogModuleApi
{
public function getProduct(string $productId): ProductDto;
public function searchProducts(string $query, int $limit = 20): array;
public function checkAvailability(array $productIds): array;
}
// Orders module uses Catalog through its API, never directly
namespace App\Module\Orders\Infrastructure;
use App\Module\Catalog\Api\CatalogModuleApi;
final class CatalogModuleAdapter
{
public function __construct(
private readonly CatalogModuleApi $catalog,
) {}
public function getProductForOrder(string $productId): OrderProductSnapshot
{
$product = $this->catalog->getProduct($productId);
return new OrderProductSnapshot(
productId: $product->id,
name: $product->name,
price: $product->price,
);
}
}
Правила хорошей декомпозиции
Высокая связность (High Cohesion)
Связанные функции -- в одном сервисе. Если изменение одной функции требует изменения другой, они должны быть в одном сервисе.
Слабая связанность (Loose Coupling)
Минимум зависимостей между сервисами. Изменение внутренней реализации сервиса не должно затрагивать другие сервисы.
Тест: правильно ли проведены границы?
| Вопрос | Плохо если | Хорошо если |
|---|---|---|
| Сколько сервисов затрагивает фича? | 3+ сервиса | 1 сервис |
| Нужна ли синхронная связь? | Цепочка вызовов | Асинхронные события |
| Нужна ли распределённая транзакция? | Часто | Редко |
| Можно ли деплоить независимо? | Нужен координированный деплой | Да |
| Владеет ли сервис своими данными? | Shared database | Database per service |
Итоги
- DDD и bounded contexts -- лучший инструмент для определения границ
- Один реальный объект может иметь разные модели в разных контекстах
- Anti-Corruption Layer защищает контекст от изменений в другом контексте
- Начинайте с модульного монолита, извлекайте сервисы по необходимости
- Хорошая декомпозиция: высокая связность внутри, слабая связанность между