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 координирует существующие |