EasyТеория4 min

Facade

Паттерн Facade в Go: упрощённый интерфейс к сложной подсистеме

Facade (Фасад)

Проблема

У вас есть сложная подсистема из нескольких компонентов (инвентарь, платежи, доставка, уведомления). Клиент не должен знать о деталях взаимодействия между ними. Facade предоставляет простой интерфейс к сложной системе.

Диаграмма

    Client --> OrderFacade.PlaceOrder()
                    |
                    +---> Inventory.Reserve()
                    +---> Payment.Charge()
                    +---> Shipping.CreateLabel()
                    +---> Notification.Send()
                    +---> Analytics.Track()

    Без Facade клиент должен знать о всех 5 подсистемах
    и правильном порядке вызовов.

Реализация

Подсистемы

package order

import (
    "context"
    "fmt"
)

// --- Inventory subsystem ---

type InventoryService struct{}

func (s *InventoryService) CheckStock(ctx context.Context, productID string, qty int) (bool, error) {
    // Check warehouse stock levels
    return true, nil
}

func (s *InventoryService) Reserve(ctx context.Context, productID string, qty int) (string, error) {
    // Reserve items, return reservation ID
    return fmt.Sprintf("RSV-%s-%d", productID, qty), nil
}

func (s *InventoryService) Release(ctx context.Context, reservationID string) error {
    // Release reserved items on failure
    return nil
}

// --- Payment subsystem ---

type PaymentService struct{}

type PaymentResult struct {
    TransactionID string
    Amount        int64 // cents
    Status        string
}

func (s *PaymentService) Charge(ctx context.Context, customerID string, amount int64) (*PaymentResult, error) {
    // Process payment through gateway
    return &PaymentResult{
        TransactionID: fmt.Sprintf("TXN-%s", customerID),
        Amount:        amount,
        Status:        "completed",
    }, nil
}

func (s *PaymentService) Refund(ctx context.Context, transactionID string) error {
    // Refund payment on failure
    return nil
}

// --- Shipping subsystem ---

type ShippingService struct{}

type ShipmentInfo struct {
    TrackingNumber string
    Carrier        string
    EstimatedDays  int
}

func (s *ShippingService) CalculateRate(ctx context.Context, address string, weight float64) (int64, error) {
    // Calculate shipping cost
    return 599, nil // $5.99
}

func (s *ShippingService) CreateShipment(ctx context.Context, address string, items []string) (*ShipmentInfo, error) {
    return &ShipmentInfo{
        TrackingNumber: "1Z999AA10123456784",
        Carrier:        "UPS",
        EstimatedDays:  3,
    }, nil
}

// --- Notification subsystem ---

type NotificationService struct{}

func (s *NotificationService) SendOrderConfirmation(ctx context.Context, email string, orderID string) error {
    // Send confirmation email
    return nil
}

func (s *NotificationService) SendShippingUpdate(ctx context.Context, email string, tracking string) error {
    // Send shipping notification
    return nil
}

Фасад

package order

import (
    "context"
    "fmt"
    "time"
)

// OrderRequest contains everything needed to place an order.
type OrderRequest struct {
    CustomerID string
    Email      string
    ProductID  string
    Quantity   int
    Address    string
    Amount     int64
}

// OrderResult contains the outcome of a placed order.
type OrderResult struct {
    OrderID        string
    TransactionID  string
    TrackingNumber string
    EstimatedDays  int
}

// Facade provides a simple interface to the order processing subsystem.
type Facade struct {
    inventory    *InventoryService
    payment      *PaymentService
    shipping     *ShippingService
    notification *NotificationService
}

// NewFacade creates an order processing facade.
func NewFacade(
    inv *InventoryService,
    pay *PaymentService,
    ship *ShippingService,
    notif *NotificationService,
) *Facade {
    return &Facade{
        inventory:    inv,
        payment:      pay,
        shipping:     ship,
        notification: notif,
    }
}

// PlaceOrder orchestrates the entire order process.
// The client doesn't need to know about inventory, payment,
// shipping, or notification subsystems.
func (f *Facade) PlaceOrder(ctx context.Context, req OrderRequest) (*OrderResult, error) {
    // Step 1: Check and reserve inventory
    available, err := f.inventory.CheckStock(ctx, req.ProductID, req.Quantity)
    if err != nil {
        return nil, fmt.Errorf("check stock: %w", err)
    }
    if !available {
        return nil, fmt.Errorf("product %s is out of stock", req.ProductID)
    }

    reservationID, err := f.inventory.Reserve(ctx, req.ProductID, req.Quantity)
    if err != nil {
        return nil, fmt.Errorf("reserve inventory: %w", err)
    }

    // Step 2: Process payment
    payment, err := f.payment.Charge(ctx, req.CustomerID, req.Amount)
    if err != nil {
        // Rollback: release inventory
        _ = f.inventory.Release(ctx, reservationID)
        return nil, fmt.Errorf("charge payment: %w", err)
    }

    // Step 3: Create shipment
    shipment, err := f.shipping.CreateShipment(ctx, req.Address, []string{req.ProductID})
    if err != nil {
        // Rollback: refund payment and release inventory
        _ = f.payment.Refund(ctx, payment.TransactionID)
        _ = f.inventory.Release(ctx, reservationID)
        return nil, fmt.Errorf("create shipment: %w", err)
    }

    // Step 4: Send confirmation (best-effort, don't fail the order)
    orderID := fmt.Sprintf("ORD-%d", time.Now().UnixNano())
    _ = f.notification.SendOrderConfirmation(ctx, req.Email, orderID)

    return &OrderResult{
        OrderID:        orderID,
        TransactionID:  payment.TransactionID,
        TrackingNumber: shipment.TrackingNumber,
        EstimatedDays:  shipment.EstimatedDays,
    }, nil
}

Использование

func main() {
    // Create the facade with all subsystems
    facade := order.NewFacade(
        &order.InventoryService{},
        &order.PaymentService{},
        &order.ShippingService{},
        &order.NotificationService{},
    )

    // Client makes ONE call instead of coordinating 5 subsystems
    result, err := facade.PlaceOrder(context.Background(), order.OrderRequest{
        CustomerID: "CUST-123",
        Email:      "[email protected]",
        ProductID:  "PROD-456",
        Quantity:   2,
        Address:    "123 Main St, City",
        Amount:     4999, // $49.99
    })

    if err != nil {
        log.Fatalf("order failed: %v", err)
    }

    fmt.Printf("Order %s placed! Tracking: %s (arrives in %d days)\n",
        result.OrderID, result.TrackingNumber, result.EstimatedDays)
}

Facade с интерфейсом для тестирования

// OrderPlacer is the facade interface for testing.
type OrderPlacer interface {
    PlaceOrder(ctx context.Context, req OrderRequest) (*OrderResult, error)
}

// Facade implements OrderPlacer.
var _ OrderPlacer = (*Facade)(nil)

// In tests, you can create a mock OrderPlacer
// without needing real inventory/payment/shipping services.

Когда использовать

Используйте, когда:

  • Сложная подсистема с множеством компонентов
  • Клиенту не нужен доступ к деталям подсистемы
  • Нужна точка входа для многошаговых процессов
  • Координация между компонентами имеет определённый порядок

Не используйте, когда:

  • Подсистема простая (1-2 вызова)
  • Клиенту нужен тонкий контроль над подсистемами
  • Facade становится "божественным объектом" с сотнями методов

Примеры в экосистеме Go

// net/http -- ServeMux is a facade for routing
mux := http.NewServeMux()
mux.Handle("/api/", apiHandler)
http.ListenAndServe(":8080", mux)

// database/sql -- *DB is a facade for connection pool + driver
db, _ := sql.Open("postgres", dsn)
db.QueryRow("SELECT ...") // hides pool management, driver details

// encoding/json -- simple facade for Marshal/Unmarshal
json.Marshal(data)    // hides reflection, encoding details
json.Unmarshal(b, &v) // hides parsing, type conversion

Сравнение с похожими паттернами

Паттерн Отличие
Adapter Приводит один интерфейс к другому; Facade упрощает сложную систему
Mediator Координирует двустороннее общение; Facade -- односторонний вызов
Abstract Factory Создаёт объекты; Facade координирует существующие

Проверь себя

Какова главная цель паттерна Facade?

Как Facade обрабатывает ошибки при многошаговом процессе?