Anti-Corruption Layer: Защита от устаревших систем
Anti-Corruption Layer (ACL) — это архитектурный паттерн из Domain-Driven Design (DDD), который создает изолирующий слой между вашей чистой доменной моделью и внешними системами с несовместимыми моделями данных. Этот паттерн особенно важен при интеграции с устаревшими (legacy) системами или внешними API.
Что такое Anti-Corruption Layer?
Anti-Corruption Layer — это адаптер между двумя подсистемами, который не позволяет концепциям одной системы “загрязнить” модель другой системы. ACL переводит запросы и ответы между различными моделями данных, обеспечивая изоляцию доменной логики.
Термин “corruption” здесь означает не повреждение данных, а концептуальное загрязнение одной модели понятиями из другой модели.
Основные функции ACL
- Трансляция моделей — преобразование данных между различными представлениями
- Изоляция интерфейсов — скрытие сложности внешних систем
- Адаптация протоколов — работа с различными способами коммуникации
- Обработка ошибок — унификация обработки исключительных ситуаций
Когда использовать ACL?
Сценарии применения
- Интеграция с legacy-системами с устаревшими моделями данных
- Подключение внешних API с неидеальным дизайном
- Миграция между системами с сохранением обратной совместимости
- Интеграция между bounded contexts в DDD
Признаки необходимости ACL
// ❌ Плохо - legacy модель загрязняет домен
type User struct {
UserID string // Наша модель
Name string // Наша модель
LegacyUserCode string // Загрязнение от legacy системы
LegacyStatus int // Загрязнение от legacy системы
LegacyCreatedAt string // Загрязнение от legacy системы (строка вместо time.Time)
}
// ✅ Хорошо - чистая доменная модель
type User struct {
ID UserID
Name string
Status UserStatus
CreatedAt time.Time
}
Структура Anti-Corruption Layer
Компоненты ACL
- Domain Model — чистая модель вашего домена
- External Model — модель внешней системы
- Translator/Adapter — преобразователь между моделями
- ACL Interface — интерфейс для работы с внешней системой
// Архитектура ACL в Go
package acl
// Доменная модель
type Order struct {
ID OrderID
CustomerID CustomerID
Items []OrderItem
Status OrderStatus
CreatedAt time.Time
TotalAmount Money
}
// Внешняя модель (legacy система)
type LegacyOrder struct {
OrderNum string `json:"order_num"`
CustCode string `json:"cust_code"`
OrderItems string `json:"order_items"` // JSON строка
OrderStat int `json:"order_stat"`
CreateDate string `json:"create_date"` // "YYYY-MM-DD"
TotalCents int `json:"total_cents"`
}
Реализация ACL на Go
Базовая структура
package orderacl
import (
"encoding/json"
"fmt"
"strconv"
"time"
"github.com/myapp/domain/order"
)
// ACL интерфейс
type OrderACL interface {
GetOrder(orderID string) (*order.Order, error)
CreateOrder(order *order.Order) error
UpdateOrderStatus(orderID string, status order.Status) error
}
// Реализация ACL
type LegacyOrderACL struct {
legacyClient LegacyOrderClient
translator *OrderTranslator
}
func NewLegacyOrderACL(client LegacyOrderClient) OrderACL {
return &LegacyOrderACL{
legacyClient: client,
translator: NewOrderTranslator(),
}
}
Translator (Переводчик)
// Translator для преобразования между моделями
type OrderTranslator struct{}
func NewOrderTranslator() *OrderTranslator {
return &OrderTranslator{}
}
// Из legacy модели в доменную модель
func (t *OrderTranslator) ToDomain(legacyOrder *LegacyOrder) (*order.Order, error) {
// Преобразование ID
orderID, err := order.NewOrderID(legacyOrder.OrderNum)
if err != nil {
return nil, fmt.Errorf("invalid order ID: %w", err)
}
// Преобразование статуса
status, err := t.convertStatus(legacyOrder.OrderStat)
if err != nil {
return nil, fmt.Errorf("invalid status: %w", err)
}
// Преобразование даты
createdAt, err := time.Parse("2006-01-02", legacyOrder.CreateDate)
if err != nil {
return nil, fmt.Errorf("invalid date format: %w", err)
}
// Преобразование товаров
items, err := t.convertItems(legacyOrder.OrderItems)
if err != nil {
return nil, fmt.Errorf("failed to convert items: %w", err)
}
// Преобразование суммы
totalAmount := order.NewMoney(legacyOrder.TotalCents, "RUB")
return &order.Order{
ID: orderID,
CustomerID: order.NewCustomerID(legacyOrder.CustCode),
Items: items,
Status: status,
CreatedAt: createdAt,
TotalAmount: totalAmount,
}, nil
}
// Из доменной модели в legacy модель
func (t *OrderTranslator) ToLegacy(domainOrder *order.Order) (*LegacyOrder, error) {
// Сериализация товаров в JSON
itemsJSON, err := json.Marshal(domainOrder.Items)
if err != nil {
return nil, fmt.Errorf("failed to serialize items: %w", err)
}
return &LegacyOrder{
OrderNum: domainOrder.ID.String(),
CustCode: domainOrder.CustomerID.String(),
OrderItems: string(itemsJSON),
OrderStat: t.convertStatusToLegacy(domainOrder.Status),
CreateDate: domainOrder.CreatedAt.Format("2006-01-02"),
TotalCents: domainOrder.TotalAmount.Cents(),
}, nil
}
// Вспомогательные методы преобразования
func (t *OrderTranslator) convertStatus(legacyStatus int) (order.Status, error) {
switch legacyStatus {
case 0:
return order.StatusPending, nil
case 1:
return order.StatusConfirmed, nil
case 2:
return order.StatusShipped, nil
case 3:
return order.StatusDelivered, nil
case 9:
return order.StatusCancelled, nil
default:
return "", fmt.Errorf("unknown legacy status: %d", legacyStatus)
}
}
func (t *OrderTranslator) convertStatusToLegacy(status order.Status) int {
switch status {
case order.StatusPending:
return 0
case order.StatusConfirmed:
return 1
case order.StatusShipped:
return 2
case order.StatusDelivered:
return 3
case order.StatusCancelled:
return 9
default:
return 0
}
}
func (t *OrderTranslator) convertItems(itemsJSON string) ([]order.Item, error) {
var legacyItems []struct {
ProductCode string `json:"product_code"`
Qty int `json:"qty"`
PriceCents int `json:"price_cents"`
}
if err := json.Unmarshal([]byte(itemsJSON), &legacyItems); err != nil {
return nil, err
}
items := make([]order.Item, len(legacyItems))
for i, legacyItem := range legacyItems {
items[i] = order.Item{
ProductID: order.NewProductID(legacyItem.ProductCode),
Quantity: legacyItem.Qty,
Price: order.NewMoney(legacyItem.PriceCents, "RUB"),
}
}
return items, nil
}
Реализация ACL методов
// Получение заказа
func (acl *LegacyOrderACL) GetOrder(orderID string) (*order.Order, error) {
// Вызов legacy API
legacyOrder, err := acl.legacyClient.GetOrder(orderID)
if err != nil {
return nil, fmt.Errorf("failed to get legacy order: %w", err)
}
// Преобразование в доменную модель
domainOrder, err := acl.translator.ToDomain(legacyOrder)
if err != nil {
return nil, fmt.Errorf("failed to translate order: %w", err)
}
return domainOrder, nil
}
// Создание заказа
func (acl *LegacyOrderACL) CreateOrder(domainOrder *order.Order) error {
// Преобразование в legacy модель
legacyOrder, err := acl.translator.ToLegacy(domainOrder)
if err != nil {
return fmt.Errorf("failed to translate order: %w", err)
}
// Вызов legacy API
if err := acl.legacyClient.CreateOrder(legacyOrder); err != nil {
return fmt.Errorf("failed to create legacy order: %w", err)
}
return nil
}
// Обновление статуса
func (acl *LegacyOrderACL) UpdateOrderStatus(orderID string, status order.Status) error {
// Преобразование статуса
legacyStatus := acl.translator.convertStatusToLegacy(status)
// Вызов legacy API
if err := acl.legacyClient.UpdateOrderStatus(orderID, legacyStatus); err != nil {
return fmt.Errorf("failed to update legacy order status: %w", err)
}
return nil
}
Legacy Client
// Клиент для работы с legacy системой
type LegacyOrderClient interface {
GetOrder(orderNum string) (*LegacyOrder, error)
CreateOrder(order *LegacyOrder) error
UpdateOrderStatus(orderNum string, status int) error
}
type HTTPLegacyClient struct {
baseURL string
client *http.Client
}
func NewHTTPLegacyClient(baseURL string) LegacyOrderClient {
return &HTTPLegacyClient{
baseURL: baseURL,
client: &http.Client{Timeout: 30 * time.Second},
}
}
func (c *HTTPLegacyClient) GetOrder(orderNum string) (*LegacyOrder, error) {
url := fmt.Sprintf("%s/api/orders/%s", c.baseURL, orderNum)
resp, err := c.client.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("legacy API returned status: %d", resp.StatusCode)
}
var legacyOrder LegacyOrder
if err := json.NewDecoder(resp.Body).Decode(&legacyOrder); err != nil {
return nil, err
}
return &legacyOrder, nil
}
Реализация ACL на PHP
Структура классов
<?php
namespace App\ACL\Order;
use App\Domain\Order\Order;
use App\Domain\Order\OrderId;
use App\Domain\Order\Status;
use DateTimeImmutable;
// ACL интерфейс
interface OrderACLInterface
{
public function getOrder(string $orderId): ?Order;
public function createOrder(Order $order): void;
public function updateOrderStatus(string $orderId, Status $status): void;
}
// Реализация ACL
class LegacyOrderACL implements OrderACLInterface
{
private LegacyOrderClient $legacyClient;
private OrderTranslator $translator;
public function __construct(
LegacyOrderClient $legacyClient,
OrderTranslator $translator
) {
$this->legacyClient = $legacyClient;
$this->translator = $translator;
}
public function getOrder(string $orderId): ?Order
{
try {
// Получаем данные из legacy системы
$legacyOrder = $this->legacyClient->getOrder($orderId);
if ($legacyOrder === null) {
return null;
}
// Преобразуем в доменную модель
return $this->translator->toDomain($legacyOrder);
} catch (Exception $e) {
throw new ACLException("Failed to get order: " . $e->getMessage(), 0, $e);
}
}
public function createOrder(Order $order): void
{
try {
// Преобразуем в legacy модель
$legacyOrder = $this->translator->toLegacy($order);
// Создаем в legacy системе
$this->legacyClient->createOrder($legacyOrder);
} catch (Exception $e) {
throw new ACLException("Failed to create order: " . $e->getMessage(), 0, $e);
}
}
public function updateOrderStatus(string $orderId, Status $status): void
{
try {
// Преобразуем статус
$legacyStatus = $this->translator->statusToLegacy($status);
// Обновляем в legacy системе
$this->legacyClient->updateOrderStatus($orderId, $legacyStatus);
} catch (Exception $e) {
throw new ACLException("Failed to update order status: " . $e->getMessage(), 0, $e);
}
}
}
Legacy модель
<?php
namespace App\ACL\Order;
// Legacy модель данных
class LegacyOrder
{
public string $orderNum;
public string $custCode;
public string $orderItems; // JSON строка
public int $orderStat;
public string $createDate; // YYYY-MM-DD
public int $totalCents;
public function __construct(
string $orderNum,
string $custCode,
string $orderItems,
int $orderStat,
string $createDate,
int $totalCents
) {
$this->orderNum = $orderNum;
$this->custCode = $custCode;
$this->orderItems = $orderItems;
$this->orderStat = $orderStat;
$this->createDate = $createDate;
$this->totalCents = $totalCents;
}
public function toArray(): array
{
return [
'order_num' => $this->orderNum,
'cust_code' => $this->custCode,
'order_items' => $this->orderItems,
'order_stat' => $this->orderStat,
'create_date' => $this->createDate,
'total_cents' => $this->totalCents,
];
}
public static function fromArray(array $data): self
{
return new self(
$data['order_num'],
$data['cust_code'],
$data['order_items'],
$data['order_stat'],
$data['create_date'],
$data['total_cents']
);
}
}
Translator на PHP
<?php
namespace App\ACL\Order;
use App\Domain\Order\Order;
use App\Domain\Order\OrderId;
use App\Domain\Order\CustomerId;
use App\Domain\Order\Status;
use App\Domain\Order\OrderItem;
use App\Domain\Order\ProductId;
use App\Domain\Order\Money;
use DateTimeImmutable;
use InvalidArgumentException;
class OrderTranslator
{
// Из legacy в доменную модель
public function toDomain(LegacyOrder $legacyOrder): Order
{
return new Order(
new OrderId($legacyOrder->orderNum),
new CustomerId($legacyOrder->custCode),
$this->convertItems($legacyOrder->orderItems),
$this->convertStatus($legacyOrder->orderStat),
DateTimeImmutable::createFromFormat('Y-m-d', $legacyOrder->createDate),
new Money($legacyOrder->totalCents, 'RUB')
);
}
// Из доменной модели в legacy
public function toLegacy(Order $order): LegacyOrder
{
return new LegacyOrder(
$order->getId()->value(),
$order->getCustomerId()->value(),
$this->serializeItems($order->getItems()),
$this->statusToLegacy($order->getStatus()),
$order->getCreatedAt()->format('Y-m-d'),
$order->getTotalAmount()->getCents()
);
}
// Преобразование статуса из legacy
private function convertStatus(int $legacyStatus): Status
{
return match($legacyStatus) {
0 => Status::PENDING,
1 => Status::CONFIRMED,
2 => Status::SHIPPED,
3 => Status::DELIVERED,
9 => Status::CANCELLED,
default => throw new InvalidArgumentException("Unknown legacy status: $legacyStatus")
};
}
// Преобразование статуса в legacy
public function statusToLegacy(Status $status): int
{
return match($status) {
Status::PENDING => 0,
Status::CONFIRMED => 1,
Status::SHIPPED => 2,
Status::DELIVERED => 3,
Status::CANCELLED => 9,
};
}
// Преобразование товаров из JSON
private function convertItems(string $itemsJson): array
{
$legacyItems = json_decode($itemsJson, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new InvalidArgumentException('Invalid items JSON: ' . json_last_error_msg());
}
return array_map(function($item) {
return new OrderItem(
new ProductId($item['product_code']),
$item['qty'],
new Money($item['price_cents'], 'RUB')
);
}, $legacyItems);
}
// Сериализация товаров в JSON
private function serializeItems(array $items): string
{
$legacyItems = array_map(function(OrderItem $item) {
return [
'product_code' => $item->getProductId()->value(),
'qty' => $item->getQuantity(),
'price_cents' => $item->getPrice()->getCents()
];
}, $items);
return json_encode($legacyItems);
}
}
Legacy Client на PHP
<?php
namespace App\ACL\Order;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
interface LegacyOrderClient
{
public function getOrder(string $orderNum): ?LegacyOrder;
public function createOrder(LegacyOrder $order): void;
public function updateOrderStatus(string $orderNum, int $status): void;
}
class HttpLegacyOrderClient implements LegacyOrderClient
{
private Client $httpClient;
private string $baseUrl;
public function __construct(string $baseUrl)
{
$this->baseUrl = rtrim($baseUrl, '/');
$this->httpClient = new Client([
'timeout' => 30,
'headers' => [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
]
]);
}
public function getOrder(string $orderNum): ?LegacyOrder
{
try {
$response = $this->httpClient->get(
"{$this->baseUrl}/api/orders/{$orderNum}"
);
if ($response->getStatusCode() === 404) {
return null;
}
$data = json_decode($response->getBody()->getContents(), true);
return LegacyOrder::fromArray($data);
} catch (GuzzleException $e) {
throw new LegacyClientException("Failed to get order: " . $e->getMessage(), 0, $e);
}
}
public function createOrder(LegacyOrder $order): void
{
try {
$response = $this->httpClient->post(
"{$this->baseUrl}/api/orders",
['json' => $order->toArray()]
);
if ($response->getStatusCode() >= 400) {
throw new LegacyClientException("Legacy API returned status: " . $response->getStatusCode());
}
} catch (GuzzleException $e) {
throw new LegacyClientException("Failed to create order: " . $e->getMessage(), 0, $e);
}
}
public function updateOrderStatus(string $orderNum, int $status): void
{
try {
$response = $this->httpClient->patch(
"{$this->baseUrl}/api/orders/{$orderNum}/status",
['json' => ['status' => $status]]
);
if ($response->getStatusCode() >= 400) {
throw new LegacyClientException("Legacy API returned status: " . $response->getStatusCode());
}
} catch (GuzzleException $e) {
throw new LegacyClientException("Failed to update order status: " . $e->getMessage(), 0, $e);
}
}
}
Тестирование ACL
Unit тесты для Go
package orderacl_test
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/myapp/acl/orderacl"
"github.com/myapp/domain/order"
)
// Mock для legacy client
type MockLegacyClient struct {
mock.Mock
}
func (m *MockLegacyClient) GetOrder(orderNum string) (*orderacl.LegacyOrder, error) {
args := m.Called(orderNum)
return args.Get(0).(*orderacl.LegacyOrder), args.Error(1)
}
func (m *MockLegacyClient) CreateOrder(order *orderacl.LegacyOrder) error {
args := m.Called(order)
return args.Error(0)
}
func (m *MockLegacyClient) UpdateOrderStatus(orderNum string, status int) error {
args := m.Called(orderNum, status)
return args.Error(0)
}
func TestOrderACL_GetOrder(t *testing.T) {
// Arrange
mockClient := new(MockLegacyClient)
acl := orderacl.NewLegacyOrderACL(mockClient)
legacyOrder := &orderacl.LegacyOrder{
OrderNum: "ORDER-123",
CustCode: "CUST-456",
OrderItems: `[{"product_code":"PROD-001","qty":2,"price_cents":1500}]`,
OrderStat: 1,
CreateDate: "2025-01-13",
TotalCents: 3000,
}
mockClient.On("GetOrder", "ORDER-123").Return(legacyOrder, nil)
// Act
result, err := acl.GetOrder("ORDER-123")
// Assert
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, "ORDER-123", result.ID.String())
assert.Equal(t, "CUST-456", result.CustomerID.String())
assert.Equal(t, order.StatusConfirmed, result.Status)
assert.Equal(t, 3000, result.TotalAmount.Cents())
mockClient.AssertExpectations(t)
}
Unit тесты для PHP
<?php
namespace Tests\Unit\ACL\Order;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\MockObject\MockObject;
use App\ACL\Order\LegacyOrderACL;
use App\ACL\Order\LegacyOrderClient;
use App\ACL\Order\OrderTranslator;
use App\ACL\Order\LegacyOrder;
use App\Domain\Order\Order;
use App\Domain\Order\Status;
class LegacyOrderACLTest extends TestCase
{
private MockObject $mockClient;
private OrderTranslator $translator;
private LegacyOrderACL $acl;
protected function setUp(): void
{
$this->mockClient = $this->createMock(LegacyOrderClient::class);
$this->translator = new OrderTranslator();
$this->acl = new LegacyOrderACL($this->mockClient, $this->translator);
}
public function testGetOrder(): void
{
// Arrange
$legacyOrder = new LegacyOrder(
'ORDER-123',
'CUST-456',
'[{"product_code":"PROD-001","qty":2,"price_cents":1500}]',
1,
'2025-01-13',
3000
);
$this->mockClient
->expects($this->once())
->method('getOrder')
->with('ORDER-123')
->willReturn($legacyOrder);
// Act
$result = $this->acl->getOrder('ORDER-123');
// Assert
$this->assertInstanceOf(Order::class, $result);
$this->assertEquals('ORDER-123', $result->getId()->value());
$this->assertEquals('CUST-456', $result->getCustomerId()->value());
$this->assertEquals(Status::CONFIRMED, $result->getStatus());
$this->assertEquals(3000, $result->getTotalAmount()->getCents());
}
public function testCreateOrder(): void
{
// Arrange
$order = $this->createTestOrder();
$this->mockClient
->expects($this->once())
->method('createOrder')
->with($this->callback(function(LegacyOrder $legacyOrder) {
return $legacyOrder->orderNum === 'ORDER-123' &&
$legacyOrder->orderStat === 1;
}));
// Act & Assert
$this->acl->createOrder($order);
}
}
Лучшие практики
1. Четкое разделение моделей
// ❌ Плохо - смешивание моделей
type User struct {
// Доменные поля
ID UserID
Name string
// Legacy поля - НЕ ДЕЛАЙТЕ ТАК!
LegacyUserCode string `json:"legacy_user_code"`
LegacyStatus int `json:"legacy_status"`
}
// ✅ Хорошо - четкое разделение
// Доменная модель
type User struct {
ID UserID
Name string
Status UserStatus
}
// Legacy модель (только в ACL)
type LegacyUser struct {
UserCode string `json:"user_code"`
UserName string `json:"user_name"`
Status int `json:"status"`
}
2. Обработка ошибок трансляции
<?php
class OrderTranslator
{
public function toDomain(LegacyOrder $legacyOrder): Order
{
try {
return new Order(
new OrderId($legacyOrder->orderNum),
new CustomerId($legacyOrder->custCode),
$this->convertItems($legacyOrder->orderItems),
$this->convertStatus($legacyOrder->orderStat),
DateTimeImmutable::createFromFormat('Y-m-d', $legacyOrder->createDate),
new Money($legacyOrder->totalCents, 'RUB')
);
} catch (Exception $e) {
throw new TranslationException("Failed to translate legacy order: " . $e->getMessage(), 0, $e);
}
}
}
3. Кэширование и производительность
// Кэширование переводов для часто используемых данных
type CachedOrderTranslator struct {
translator *OrderTranslator
cache map[string]*order.Order
mutex sync.RWMutex
ttl time.Duration
}
func (t *CachedOrderTranslator) ToDomain(legacyOrder *LegacyOrder) (*order.Order, error) {
cacheKey := legacyOrder.OrderNum + "_" + fmt.Sprint(legacyOrder.OrderStat)
t.mutex.RLock()
if cached, exists := t.cache[cacheKey]; exists {
t.mutex.RUnlock()
return cached, nil
}
t.mutex.RUnlock()
// Переводим и кэшируем
domainOrder, err := t.translator.ToDomain(legacyOrder)
if err != nil {
return nil, err
}
t.mutex.Lock()
t.cache[cacheKey] = domainOrder
t.mutex.Unlock()
return domainOrder, nil
}
4. Версионирование ACL
<?php
interface VersionedACL
{
public function getVersion(): string;
public function supportsLegacyVersion(string $version): bool;
}
class OrderACLV1 implements OrderACLInterface, VersionedACL
{
public function getVersion(): string
{
return '1.0';
}
public function supportsLegacyVersion(string $version): bool
{
return in_array($version, ['1.0', '1.1']);
}
}
class OrderACLV2 implements OrderACLInterface, VersionedACL
{
public function getVersion(): string
{
return '2.0';
}
public function supportsLegacyVersion(string $version): bool
{
return version_compare($version, '2.0', '>=');
}
}
5. Мониторинг ACL
// Метрики для ACL
type ACLMetrics struct {
translationErrors prometheus.Counter
translationDuration prometheus.Histogram
legacyAPIErrors prometheus.Counter
cacheHits prometheus.Counter
cacheMisses prometheus.Counter
}
func (acl *LegacyOrderACL) GetOrderWithMetrics(orderID string) (*order.Order, error) {
start := time.Now()
defer func() {
acl.metrics.translationDuration.Observe(time.Since(start).Seconds())
}()
// Проверяем кэш
if cached := acl.getFromCache(orderID); cached != nil {
acl.metrics.cacheHits.Inc()
return cached, nil
}
acl.metrics.cacheMisses.Inc()
// Получаем из legacy системы
legacyOrder, err := acl.legacyClient.GetOrder(orderID)
if err != nil {
acl.metrics.legacyAPIErrors.Inc()
return nil, err
}
// Переводим
domainOrder, err := acl.translator.ToDomain(legacyOrder)
if err != nil {
acl.metrics.translationErrors.Inc()
return nil, err
}
// Кэшируем и возвращаем
acl.putToCache(orderID, domainOrder)
return domainOrder, nil
}
Паттерны интеграции с ACL
1. Facade Pattern + ACL
// Фасад, скрывающий сложность ACL
type OrderFacade struct {
orderACL OrderACL
paymentACL PaymentACL
inventoryACL InventoryACL
}
func (f *OrderFacade) ProcessOrder(orderRequest *CreateOrderRequest) (*order.Order, error) {
// Создаем заказ через ACL
newOrder, err := f.orderACL.CreateOrder(orderRequest.ToOrder())
if err != nil {
return nil, err
}
// Обрабатываем платеж через отдельный ACL
payment, err := f.paymentACL.ProcessPayment(&PaymentRequest{
OrderID: newOrder.ID,
Amount: newOrder.TotalAmount,
})
if err != nil {
// Откатываем заказ
f.orderACL.CancelOrder(newOrder.ID)
return nil, err
}
// Резервируем товары через третий ACL
if err := f.inventoryACL.ReserveItems(newOrder.Items); err != nil {
f.paymentACL.RefundPayment(payment.ID)
f.orderACL.CancelOrder(newOrder.ID)
return nil, err
}
return newOrder, nil
}
2. Repository Pattern с ACL
<?php
class OrderRepository
{
private OrderACLInterface $orderACL;
private CacheInterface $cache;
public function __construct(OrderACLInterface $orderACL, CacheInterface $cache)
{
$this->orderACL = $orderACL;
$this->cache = $cache;
}
public function findById(string $orderId): ?Order
{
// Проверяем локальный кэш
$cacheKey = "order:$orderId";
if ($cached = $this->cache->get($cacheKey)) {
return $cached;
}
// Получаем через ACL
$order = $this->orderACL->getOrder($orderId);
if ($order !== null) {
$this->cache->set($cacheKey, $order, 300); // 5 минут
}
return $order;
}
public function save(Order $order): void
{
// Сохраняем через ACL
if ($order->getId()->value() === null) {
$this->orderACL->createOrder($order);
} else {
$this->orderACL->updateOrder($order);
}
// Инвалидируем кэш
$this->cache->delete("order:" . $order->getId()->value());
}
}
Сложные сценарии
1. Множественные legacy системы
// ACL для работы с несколькими legacy системами
type CompositeOrderACL struct {
primaryACL OrderACL // Основная система
secondaryACL OrderACL // Резервная система
router *ACLRouter // Роутер запросов
}
func (c *CompositeOrderACL) GetOrder(orderID string) (*order.Order, error) {
// Определяем, какую систему использовать
aclToUse := c.router.RouteGetOrder(orderID)
switch aclToUse {
case "primary":
order, err := c.primaryACL.GetOrder(orderID)
if err != nil {
// Fallback на вторичную систему
return c.secondaryACL.GetOrder(orderID)
}
return order, nil
case "secondary":
return c.secondaryACL.GetOrder(orderID)
default:
return nil, fmt.Errorf("no suitable ACL found for order %s", orderID)
}
}
2. Async ACL с очередями
// Асинхронный ACL с очередями сообщений
type AsyncOrderACL struct {
messageQueue MessageQueue
translator *OrderTranslator
resultStore ResultStore
}
func (a *AsyncOrderACL) CreateOrderAsync(order *order.Order, correlationID string) error {
// Преобразуем в legacy модель
legacyOrder, err := a.translator.ToLegacy(order)
if err != nil {
return err
}
// Отправляем в очередь
message := &CreateOrderMessage{
LegacyOrder: legacyOrder,
CorrelationID: correlationID,
Timestamp: time.Now(),
}
return a.messageQueue.Publish("legacy.order.create", message)
}
func (a *AsyncOrderACL) GetOrderResult(correlationID string) (*order.Order, error) {
// Получаем результат из хранилища
result, err := a.resultStore.Get(correlationID)
if err != nil {
return nil, err
}
if result.Status == "pending" {
return nil, ErrOrderPending
}
if result.Status == "error" {
return nil, fmt.Errorf("order creation failed: %s", result.Error)
}
return a.translator.ToDomain(result.LegacyOrder)
}
Инструменты и библиотеки
Go библиотеки
// Использование Go библиотек для ACL
import (
"github.com/mitchellh/mapstructure" // Для маппинга структур
"github.com/go-playground/validator" // Для валидации
"github.com/patrickmn/go-cache" // Для кэширования
)
type ValidatedTranslator struct {
validator *validator.Validate
cache *cache.Cache
}
func (t *ValidatedTranslator) ToDomainWithValidation(data map[string]interface{}) (*order.Order, error) {
// Маппинг в legacy структуру
var legacyOrder LegacyOrder
if err := mapstructure.Decode(data, &legacyOrder); err != nil {
return nil, err
}
// Валидация legacy данных
if err := t.validator.Struct(&legacyOrder); err != nil {
return nil, fmt.Errorf("validation failed: %w", err)
}
// Кэшированный перевод
cacheKey := fmt.Sprintf("order_%s", legacyOrder.OrderNum)
if cached, found := t.cache.Get(cacheKey); found {
return cached.(*order.Order), nil
}
// Обычный перевод
domainOrder, err := t.ToDomain(&legacyOrder)
if err != nil {
return nil, err
}
// Кэшируем результат
t.cache.Set(cacheKey, domainOrder, cache.DefaultExpiration)
return domainOrder, nil
}
PHP библиотеки
<?php
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Validator\Validator\ValidatorInterface;
class SerializerBasedTranslator
{
private Serializer $serializer;
private ValidatorInterface $validator;
public function __construct(ValidatorInterface $validator)
{
$this->validator = $validator;
$this->serializer = new Serializer(
[new ObjectNormalizer()],
[new JsonEncoder()]
);
}
public function toDomainFromArray(array $legacyData): Order
{
// Десериализация в legacy объект
$legacyOrder = $this->serializer->denormalize(
$legacyData,
LegacyOrder::class
);
// Валидация
$violations = $this->validator->validate($legacyOrder);
if (count($violations) > 0) {
throw new ValidationException($violations);
}
// Перевод в доменную модель
return $this->toDomain($legacyOrder);
}
}
Заключение
Anti-Corruption Layer — это критически важный паттерн для поддержания чистоты архитектуры при интеграции с внешними системами. Ключевые преимущества ACL:
Преимущества
- Изоляция доменной модели от внешних зависимостей
- Гибкость в изменениях — изменения в legacy системе не влияют на домен
- Тестируемость — легко мокать ACL для тестов
- Переиспользование — один ACL может обслуживать несколько потребителей
- Эволюция системы — постепенная замена legacy компонентов
Рекомендации по применению
- Используйте ACL всегда при интеграции с внешними системами
- Инвестируйте в качественные тесты для translator’ов
- Мониторьте производительность трансляций
- Планируйте версионирование ACL заранее
- Документируйте маппинги между моделями
- Рассматривайте кэширование для часто используемых переводов
Anti-Corruption Layer помогает создавать устойчивую архитектуру, которая может развиваться независимо от ограничений устаревших систем. Инвестиции в качественную реализацию ACL окупаются снижением технического долга и повышением гибкости системы.
Интересуют другие архитектурные паттерны? Читайте наши статьи о Event-Driven Architecture и микросервисах.