Adapter và Facade Pattern: Tích Hợp và Đơn Giản Hóa Interface

Giới Thiệu
Bạn đã nắm vững các creational pattern — Singleton, Factory, Builder, Prototype. Giờ là lúc bước sang structural pattern: nhóm pattern tập trung vào cách ghép nối các đối tượng và class thành cấu trúc lớn hơn.
Có hai structural pattern mà bạn sẽ gặp đi gặp lại trong dự án thực tế:
| Pattern | Tóm Tắt |
|---|---|
| Adapter | Chuyển đổi một interface không tương thích thành interface mà code đang cần |
| Facade | Gom nhiều thành phần phức tạp lại sau một interface đơn giản, dễ dùng |
Cả hai đều xoay quanh interface — nhưng giải quyết hai bài toán ngược nhau. Adapter chuyển đổi interface. Facade đơn giản hóa interface.
Bạn Sẽ Học Được Gì
✅ Hiểu rõ bài toán mà từng pattern sinh ra để giải quyết
✅ Phân biệt và triển khai Object Adapter với Class Adapter
✅ Dựng Facade để giấu đi sự phức tạp bên trong hệ thống con
✅ Áp dụng cả hai pattern trong TypeScript, Python và Java
✅ Biết khi nào chọn Adapter, khi nào chọn Facade, khi nào dùng cả hai
✅ Tránh những lỗi phổ biến khi bọc interface
Điều Kiện Tiên Quyết
- Đã hoàn thành Prototype Pattern Explained
- Quen thuộc với Polymorphism và Interfaces
- Nắm được SOLID Principles, đặc biệt Interface Segregation và Dependency Inversion
Phần 1: Adapter Pattern
Vấn Đề: Interface Không Tương Thích
Bạn đang xây dựng một hệ thống thanh toán. Toàn bộ codebase đều gọi qua interface PaymentProcessor:
interface PaymentProcessor {
charge(amount: number, currency: string): PaymentResult;
}Mọi thứ chạy ngon — checkout, subscription, hoàn tiền — tất cả đều đi qua PaymentProcessor.
Rồi một ngày sếp bảo: "Thêm Stripe vào đi." Vấn đề là SDK của Stripe không implement interface PaymentProcessor của bạn. Nó có API hoàn toàn khác:
// SDK của Stripe — mình không sửa được
class StripeAPI {
createCharge(amountInCents: number, cur: string, idempotencyKey: string): StripeResponse {
// Logic riêng của Stripe
}
}Không có Adapter pattern, bạn sẽ phải rải if/else khắp codebase:
// ❌ Mỗi hàm checkout gắn chặt với từng nhà cung cấp thanh toán
function checkout(cart: Cart, provider: string) {
if (provider === "internal") {
internalProcessor.charge(cart.total, "USD");
} else if (provider === "stripe") {
stripeAPI.createCharge(cart.total * 100, "USD", generateKey());
} else if (provider === "paypal") {
paypalSDK.makePayment({ value: cart.total, currency_code: "USD" });
}
// Thêm provider mới = sửa tất cả các chỗ gọi checkout ❌
}Adapter pattern giải quyết bằng cách bọc class không tương thích, biến nó thành đúng interface mà code đang cần — không cần sửa đổi bất kỳ bên nào.
Cấu Trúc Pattern
Các thành phần chính:
- Target — interface mà code của bạn đang dùng (
PaymentProcessor) - Adaptee — class có interface khác, không tương thích (
StripeAPI) - Adapter — lớp trung gian, bọc adaptee và implement target (
StripeAdapter) - Client — chỉ làm việc với Target, không hề biết Adapter tồn tại
Object Adapter vs Class Adapter
Có hai cách triển khai adapter:
| Biến Thể | Cơ Chế | Hỗ Trợ Ngôn Ngữ |
|---|---|---|
| Object Adapter | Giữ tham chiếu đến adaptee (composition) | Tất cả ngôn ngữ |
| Class Adapter | Kế thừa cả Target và Adaptee (đa kế thừa) | C++, Python |
Nên dùng Object Adapter trong hầu hết trường hợp — linh hoạt hơn và ngôn ngữ nào cũng hỗ trợ. Class Adapter chỉ dùng được khi ngôn ngữ cho phép đa kế thừa (Python thì được, Java và TypeScript thì không).
Triển Khai TypeScript
Object Adapter (Khuyến nghị)
// Interface target — code của chúng ta mong đợi
interface PaymentProcessor {
charge(amount: number, currency: string): PaymentResult;
}
interface PaymentResult {
success: boolean;
transactionId: string;
}
// Adaptee — SDK bên ngoài, không sửa được
class StripeAPI {
createCharge(
amountInCents: number,
cur: string,
idempotencyKey: string
): { id: string; status: string } {
console.log(`Stripe: tính phí ${amountInCents} cents (${cur})`);
return { id: `stripe_${Date.now()}`, status: "succeeded" };
}
}
// Adapter — cầu nối giữa hai bên
class StripeAdapter implements PaymentProcessor {
constructor(private stripe: StripeAPI) {}
charge(amount: number, currency: string): PaymentResult {
const amountInCents = Math.round(amount * 100);
const key = `idempotent_${Date.now()}_${Math.random()}`;
const response = this.stripe.createCharge(amountInCents, currency, key);
return {
success: response.status === "succeeded",
transactionId: response.id,
};
}
}
// Code client — chạy được với bất kỳ PaymentProcessor nào
function processCheckout(processor: PaymentProcessor, amount: number) {
const result = processor.charge(amount, "USD");
if (result.success) {
console.log(`Thanh toán thành công: ${result.transactionId}`);
}
}
// Client không hề biết Stripe tồn tại
const stripeAdapter = new StripeAdapter(new StripeAPI());
processCheckout(stripeAdapter, 49.99);Nhiều Adapter Cho Nhiều Provider
// Adaptee khác — PayPal SDK
class PayPalSDK {
makePayment(options: {
value: number;
currency_code: string;
}): { orderId: string; outcome: string } {
console.log(`PayPal: ${options.value} ${options.currency_code}`);
return { orderId: `pp_${Date.now()}`, outcome: "COMPLETED" };
}
}
// Adapter cho PayPal
class PayPalAdapter implements PaymentProcessor {
constructor(private paypal: PayPalSDK) {}
charge(amount: number, currency: string): PaymentResult {
const response = this.paypal.makePayment({
value: amount,
currency_code: currency,
});
return {
success: response.outcome === "COMPLETED",
transactionId: response.orderId,
};
}
}
// Giờ hai provider dùng thay thế cho nhau được
const processors: Record<string, PaymentProcessor> = {
stripe: new StripeAdapter(new StripeAPI()),
paypal: new PayPalAdapter(new PayPalSDK()),
};
function checkout(cart: Cart, providerName: string) {
const processor = processors[providerName];
return processor.charge(cart.total, "USD"); // ✅ gọn, không if/else
}Triển Khai Python
Python hỗ trợ cả hai kiểu adapter nhờ có đa kế thừa:
Object Adapter
from abc import ABC, abstractmethod
from dataclasses import dataclass
import time, random
# Target
class PaymentProcessor(ABC):
@abstractmethod
def charge(self, amount: float, currency: str) -> dict:
pass
# Adaptee — SDK bên thứ ba
class StripeAPI:
def create_charge(self, amount_in_cents: int, cur: str, idempotency_key: str) -> dict:
print(f"Stripe: tính phí {amount_in_cents} cents ({cur})")
return {"id": f"stripe_{int(time.time())}", "status": "succeeded"}
# Object Adapter
class StripeAdapter(PaymentProcessor):
def __init__(self, stripe: StripeAPI):
self._stripe = stripe
def charge(self, amount: float, currency: str) -> dict:
amount_in_cents = round(amount * 100)
key = f"idempotent_{time.time()}_{random.random()}"
response = self._stripe.create_charge(amount_in_cents, currency, key)
return {
"success": response["status"] == "succeeded",
"transaction_id": response["id"],
}
# Sử dụng
adapter = StripeAdapter(StripeAPI())
result = adapter.charge(49.99, "USD")
print(result) # {'success': True, 'transaction_id': 'stripe_...'}Class Adapter (Chỉ Python — đa kế thừa)
# Class Adapter — kế thừa cả Target và Adaptee
class StripeClassAdapter(PaymentProcessor, StripeAPI):
"""
Kế thừa PaymentProcessor (Target) và StripeAPI (Adaptee).
Override charge() để ủy quyền cho create_charge() được kế thừa.
"""
def charge(self, amount: float, currency: str) -> dict:
amount_in_cents = round(amount * 100)
key = f"idempotent_{time.time()}_{random.random()}"
# Gọi phương thức kế thừa trực tiếp — không cần composition
response = self.create_charge(amount_in_cents, currency, key)
return {
"success": response["status"] == "succeeded",
"transaction_id": response["id"],
}
# Cùng interface, khác cách nối dây
adapter = StripeClassAdapter()
result = adapter.charge(29.99, "EUR")Lưu ý: Class Adapter coupling chặt hơn vì nó kế thừa toàn bộ phương thức của
StripeAPI, khiến việc test khó hơn. Hãy ưu tiên Object Adapter trừ khi bạn có lý do rõ ràng để kế thừa.
Triển Khai Java
// Interface target
public interface PaymentProcessor {
PaymentResult charge(double amount, String currency);
}
public record PaymentResult(boolean success, String transactionId) {}
// Adaptee — SDK bên thứ ba
public class StripeAPI {
public StripeResponse createCharge(int amountInCents, String cur, String idempotencyKey) {
System.out.printf("Stripe: tính phí %d cents (%s)%n", amountInCents, cur);
return new StripeResponse("stripe_" + System.currentTimeMillis(), "succeeded");
}
}
public record StripeResponse(String id, String status) {}
// Adapter
public class StripeAdapter implements PaymentProcessor {
private final StripeAPI stripe;
public StripeAdapter(StripeAPI stripe) {
this.stripe = stripe;
}
@Override
public PaymentResult charge(double amount, String currency) {
int amountInCents = (int) Math.round(amount * 100);
String key = "idempotent_" + System.currentTimeMillis();
StripeResponse response = stripe.createCharge(amountInCents, currency, key);
return new PaymentResult(
"succeeded".equals(response.status()),
response.id()
);
}
}
// Code client
public class CheckoutService {
private final PaymentProcessor processor;
public CheckoutService(PaymentProcessor processor) {
this.processor = processor;
}
public void checkout(double amount) {
PaymentResult result = processor.charge(amount, "USD");
if (result.success()) {
System.out.println("Thanh toán thành công: " + result.transactionId());
}
}
}
// Sử dụng
CheckoutService service = new CheckoutService(new StripeAdapter(new StripeAPI()));
service.checkout(49.99);Ví Dụ Adapter Thực Tế
1. Logger Adapter
App của bạn dùng interface Logger riêng, nhưng bạn muốn dễ dàng đổi qua Winston, Pino hay bất kỳ thư viện logging nào khác:
interface Logger {
info(message: string, context?: Record<string, unknown>): void;
error(message: string, error?: Error): void;
warn(message: string): void;
}
// Adapt Winston sang interface Logger của bạn
class WinstonAdapter implements Logger {
constructor(private winston: WinstonLogger) {}
info(message: string, context?: Record<string, unknown>): void {
this.winston.log("info", message, { metadata: context });
}
error(message: string, error?: Error): void {
this.winston.log("error", message, {
stack: error?.stack,
name: error?.name,
});
}
warn(message: string): void {
this.winston.log("warn", message);
}
}
// Adapt Pino sang cùng interface
class PinoAdapter implements Logger {
constructor(private pino: PinoLogger) {}
info(message: string, context?: Record<string, unknown>): void {
this.pino.info(context ?? {}, message);
}
error(message: string, error?: Error): void {
this.pino.error({ err: error }, message);
}
warn(message: string): void {
this.pino.warn(message);
}
}2. Data Format Adapter
Kết nối hệ thống cũ trả XML với code mới dùng JSON:
interface DataProvider {
getData(): Record<string, unknown>;
}
// Hệ thống cũ trả về XML
class LegacyXMLService {
fetchData(): string {
return "<user><name>Alice</name><age>30</age></user>";
}
}
class XMLToJSONAdapter implements DataProvider {
constructor(private xmlService: LegacyXMLService) {}
getData(): Record<string, unknown> {
const xml = this.xmlService.fetchData();
return this.parseXML(xml);
}
private parseXML(xml: string): Record<string, unknown> {
// đơn giản hóa — trong production dùng thư viện như fast-xml-parser
const nameMatch = xml.match(/<name>(.*?)<\/name>/);
const ageMatch = xml.match(/<age>(.*?)<\/age>/);
return {
name: nameMatch?.[1] ?? "",
age: Number(ageMatch?.[1] ?? 0),
};
}
}Phần 2: Facade Pattern
Vấn Đề: Quá Nhiều Thứ Phải Phối Hợp
Bạn đang xây dựng hệ thống thương mại điện tử. Mỗi lần đặt hàng phải gọi qua hàng loạt hệ thống con:
// ❌ Client phải tự tay gọi 6 hệ thống con theo đúng thứ tự
function placeOrder(userId: string, items: CartItem[]) {
// 1. Kiểm tra tồn kho
const inventory = new InventoryService();
for (const item of items) {
if (!inventory.checkStock(item.productId, item.quantity)) {
throw new Error(`Hết hàng: ${item.productId}`);
}
}
// 2. Tính giá
const pricing = new PricingEngine();
const subtotal = pricing.calculateSubtotal(items);
const tax = pricing.calculateTax(subtotal, "US-CA");
const discount = pricing.applyPromoCode("SAVE10", subtotal);
const total = subtotal + tax - discount;
// 3. Xử lý thanh toán
const payment = new PaymentGateway();
const paymentResult = payment.authorize(userId, total);
if (!paymentResult.success) throw new Error("Thanh toán thất bại");
payment.capture(paymentResult.authorizationId);
// 4. Đặt trước tồn kho
for (const item of items) {
inventory.reserve(item.productId, item.quantity);
}
// 5. Tạo vận chuyển
const shipping = new ShippingService();
const address = new AddressBook().getDefault(userId);
const shipment = shipping.createShipment(items, address);
shipping.schedulePickup(shipment.id);
// 6. Gửi thông báo
const notifier = new NotificationService();
notifier.sendOrderConfirmation(userId, paymentResult.orderId);
notifier.sendShippingNotification(userId, shipment.trackingNumber);
}Đây là mớ bòng bong:
- Mọi client muốn đặt hàng đều phải biết đúng thứ tự gọi 6 hệ thống con
- Coupling chặt — client phụ thuộc trực tiếp vào API của từng hệ thống con
- Dễ vỡ — sửa API một chỗ là hỏng khắp nơi
- Lặp code — web checkout, mobile app, API endpoint đều viết lại cùng một logic
Cấu Trúc Pattern
Các thành phần chính:
- Facade — cổng vào duy nhất, gom mọi thứ phức tạp sau một interface gọn gàng
- Subsystems — các class làm việc thật sự (giữ nguyên, không cần sửa)
- Client — chỉ gọi Facade, không cần biết bên trong có gì
Khác Adapter ở chỗ nào? Facade không chuyển đổi interface — nó đơn giản hóa việc dùng nhiều interface cùng lúc. Các hệ thống con không hề "không tương thích" — chúng chỉ phức tạp khi phải phối hợp với nhau.
Triển Khai TypeScript
// Các class hệ thống con (giữ nguyên — bản thân chúng đã đúng)
class InventoryService {
checkStock(productId: string, quantity: number): boolean {
console.log(`Kiểm tra tồn kho cho ${productId}`);
return true;
}
reserve(productId: string, quantity: number): void {
console.log(`Đã đặt trước ${quantity}x ${productId}`);
}
release(productId: string, quantity: number): void {
console.log(`Đã giải phóng ${quantity}x ${productId}`);
}
}
class PricingEngine {
calculateSubtotal(items: CartItem[]): number {
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
calculateTax(subtotal: number, region: string): number {
const rates: Record<string, number> = { "US-CA": 0.0725, "US-TX": 0.0625 };
return subtotal * (rates[region] ?? 0);
}
applyPromoCode(code: string, subtotal: number): number {
const discounts: Record<string, number> = { SAVE10: 0.1, SAVE20: 0.2 };
return subtotal * (discounts[code] ?? 0);
}
}
class PaymentGateway {
authorize(userId: string, amount: number): { success: boolean; authorizationId: string } {
console.log(`Xác thực thanh toán $${amount} cho user ${userId}`);
return { success: true, authorizationId: `auth_${Date.now()}` };
}
capture(authorizationId: string): void {
console.log(`Đã capture thanh toán: ${authorizationId}`);
}
refund(authorizationId: string): void {
console.log(`Đã hoàn tiền: ${authorizationId}`);
}
}
class ShippingService {
createShipment(items: CartItem[], address: Address): { id: string; trackingNumber: string } {
console.log(`Vận chuyển ${items.length} mặt hàng đến ${address.city}`);
return { id: `ship_${Date.now()}`, trackingNumber: `TRK${Date.now()}` };
}
schedulePickup(shipmentId: string): void {
console.log(`Đã lên lịch lấy hàng cho ${shipmentId}`);
}
}
class NotificationService {
sendOrderConfirmation(userId: string, orderId: string): void {
console.log(`Xác nhận đơn hàng đã gửi cho ${userId}`);
}
sendShippingNotification(userId: string, trackingNumber: string): void {
console.log(`Thông báo vận chuyển đã gửi cho ${userId}`);
}
}
// ✅ Facade — gọi một phương thức, mọi thứ phức tạp được xử lý bên trong
class OrderFacade {
private inventory = new InventoryService();
private pricing = new PricingEngine();
private payment = new PaymentGateway();
private shipping = new ShippingService();
private notifications = new NotificationService();
placeOrder(userId: string, items: CartItem[], promoCode?: string): OrderResult {
// 1. Kiểm tra tồn kho
for (const item of items) {
if (!this.inventory.checkStock(item.productId, item.quantity)) {
return { success: false, error: `Hết hàng: ${item.productId}` };
}
}
// 2. Tính tổng
const subtotal = this.pricing.calculateSubtotal(items);
const tax = this.pricing.calculateTax(subtotal, "US-CA");
const discount = promoCode
? this.pricing.applyPromoCode(promoCode, subtotal)
: 0;
const total = subtotal + tax - discount;
// 3. Xử lý thanh toán
const paymentResult = this.payment.authorize(userId, total);
if (!paymentResult.success) {
return { success: false, error: "Thanh toán thất bại" };
}
this.payment.capture(paymentResult.authorizationId);
// 4. Đặt trước tồn kho
for (const item of items) {
this.inventory.reserve(item.productId, item.quantity);
}
// 5. Vận chuyển
const address = { city: "San Francisco", zip: "94102" } as Address;
const shipment = this.shipping.createShipment(items, address);
this.shipping.schedulePickup(shipment.id);
// 6. Thông báo
this.notifications.sendOrderConfirmation(userId, paymentResult.authorizationId);
this.notifications.sendShippingNotification(userId, shipment.trackingNumber);
return {
success: true,
orderId: paymentResult.authorizationId,
trackingNumber: shipment.trackingNumber,
total,
};
}
}
// Client chỉ cần gọi đúng một dòng
const orderService = new OrderFacade();
const result = orderService.placeOrder("user_123", cartItems, "SAVE10");Triển Khai Python
from dataclasses import dataclass
import time
# Các class hệ thống con
class InventoryService:
def check_stock(self, product_id: str, quantity: int) -> bool:
print(f"Kiểm tra tồn kho cho {product_id}")
return True
def reserve(self, product_id: str, quantity: int) -> None:
print(f"Đã đặt trước {quantity}x {product_id}")
class PricingEngine:
def calculate_subtotal(self, items: list[dict]) -> float:
return sum(item["price"] * item["quantity"] for item in items)
def calculate_tax(self, subtotal: float, region: str) -> float:
rates = {"US-CA": 0.0725, "US-TX": 0.0625}
return subtotal * rates.get(region, 0)
def apply_promo_code(self, code: str, subtotal: float) -> float:
discounts = {"SAVE10": 0.1, "SAVE20": 0.2}
return subtotal * discounts.get(code, 0)
class PaymentGateway:
def authorize(self, user_id: str, amount: float) -> dict:
print(f"Xác thực thanh toán ${amount} cho {user_id}")
return {"success": True, "authorization_id": f"auth_{int(time.time())}"}
def capture(self, authorization_id: str) -> None:
print(f"Đã capture thanh toán: {authorization_id}")
class ShippingService:
def create_shipment(self, items: list[dict], address: dict) -> dict:
print(f"Vận chuyển {len(items)} mặt hàng đến {address['city']}")
return {"id": f"ship_{int(time.time())}", "tracking": f"TRK{int(time.time())}"}
def schedule_pickup(self, shipment_id: str) -> None:
print(f"Đã lên lịch lấy hàng cho {shipment_id}")
class NotificationService:
def send_order_confirmation(self, user_id: str, order_id: str) -> None:
print(f"Xác nhận đơn hàng đã gửi cho {user_id}")
def send_shipping_notification(self, user_id: str, tracking: str) -> None:
print(f"Thông báo vận chuyển đã gửi cho {user_id}")
# ✅ Facade
@dataclass
class OrderResult:
success: bool
order_id: str = ""
tracking_number: str = ""
total: float = 0.0
error: str = ""
class OrderFacade:
def __init__(self):
self._inventory = InventoryService()
self._pricing = PricingEngine()
self._payment = PaymentGateway()
self._shipping = ShippingService()
self._notifications = NotificationService()
def place_order(
self, user_id: str, items: list[dict], promo_code: str | None = None
) -> OrderResult:
# 1. Kiểm tra tồn kho
for item in items:
if not self._inventory.check_stock(item["product_id"], item["quantity"]):
return OrderResult(success=False, error=f"Hết hàng: {item['product_id']}")
# 2. Tính tổng
subtotal = self._pricing.calculate_subtotal(items)
tax = self._pricing.calculate_tax(subtotal, "US-CA")
discount = self._pricing.apply_promo_code(promo_code, subtotal) if promo_code else 0
total = subtotal + tax - discount
# 3. Xử lý thanh toán
payment_result = self._payment.authorize(user_id, total)
if not payment_result["success"]:
return OrderResult(success=False, error="Thanh toán thất bại")
self._payment.capture(payment_result["authorization_id"])
# 4. Đặt trước tồn kho
for item in items:
self._inventory.reserve(item["product_id"], item["quantity"])
# 5. Vận chuyển
shipment = self._shipping.create_shipment(items, {"city": "San Francisco"})
self._shipping.schedule_pickup(shipment["id"])
# 6. Thông báo
self._notifications.send_order_confirmation(user_id, payment_result["authorization_id"])
self._notifications.send_shipping_notification(user_id, shipment["tracking"])
return OrderResult(
success=True,
order_id=payment_result["authorization_id"],
tracking_number=shipment["tracking"],
total=total,
)
# Sử dụng
facade = OrderFacade()
result = facade.place_order("user_123", [
{"product_id": "SKU001", "quantity": 2, "price": 29.99},
{"product_id": "SKU002", "quantity": 1, "price": 49.99},
], promo_code="SAVE10")
print(f"Đơn hàng {'đã đặt' if result.success else 'thất bại'}: {result}")Triển Khai Java
// Các class hệ thống con (đơn giản hóa)
public class InventoryService {
public boolean checkStock(String productId, int quantity) {
System.out.println("Kiểm tra tồn kho cho " + productId);
return true;
}
public void reserve(String productId, int quantity) {
System.out.printf("Đã đặt trước %dx %s%n", quantity, productId);
}
}
public class PricingEngine {
public double calculateSubtotal(List<CartItem> items) {
return items.stream()
.mapToDouble(item -> item.price() * item.quantity())
.sum();
}
public double calculateTax(double subtotal, String region) {
return switch (region) {
case "US-CA" -> subtotal * 0.0725;
case "US-TX" -> subtotal * 0.0625;
default -> 0;
};
}
}
public class PaymentGateway {
public PaymentAuth authorize(String userId, double amount) {
System.out.printf("Xác thực thanh toán $%.2f cho %s%n", amount, userId);
return new PaymentAuth(true, "auth_" + System.currentTimeMillis());
}
public void capture(String authorizationId) {
System.out.println("Đã capture: " + authorizationId);
}
}
public record PaymentAuth(boolean success, String authorizationId) {}
public record CartItem(String productId, int quantity, double price) {}
// ✅ Facade
public class OrderFacade {
private final InventoryService inventory = new InventoryService();
private final PricingEngine pricing = new PricingEngine();
private final PaymentGateway payment = new PaymentGateway();
public OrderResult placeOrder(String userId, List<CartItem> items) {
// 1. Kiểm tra tồn kho
for (CartItem item : items) {
if (!inventory.checkStock(item.productId(), item.quantity())) {
return OrderResult.failure("Hết hàng: " + item.productId());
}
}
// 2. Tính tổng
double subtotal = pricing.calculateSubtotal(items);
double tax = pricing.calculateTax(subtotal, "US-CA");
double total = subtotal + tax;
// 3. Xử lý thanh toán
PaymentAuth auth = payment.authorize(userId, total);
if (!auth.success()) {
return OrderResult.failure("Thanh toán thất bại");
}
payment.capture(auth.authorizationId());
// 4. Đặt trước tồn kho
for (CartItem item : items) {
inventory.reserve(item.productId(), item.quantity());
}
return OrderResult.success(auth.authorizationId(), total);
}
}
public record OrderResult(boolean success, String orderId, double total, String error) {
public static OrderResult success(String orderId, double total) {
return new OrderResult(true, orderId, total, "");
}
public static OrderResult failure(String error) {
return new OrderResult(false, "", 0, error);
}
}
// Sử dụng
OrderFacade facade = new OrderFacade();
OrderResult result = facade.placeOrder("user_123", List.of(
new CartItem("SKU001", 2, 29.99),
new CartItem("SKU002", 1, 49.99)
));
System.out.println(result.success() ? "Đã đặt hàng!" : "Thất bại: " + result.error());Ví Dụ Facade Thực Tế
1. Video Conversion Facade
// Hệ thống con phức tạp
class VideoFile { constructor(public path: string) {} }
class CodecFactory {
detect(file: VideoFile): string { return "h264"; }
}
class MPEG4Codec { encode(data: Buffer): Buffer { return data; } }
class OGGCodec { encode(data: Buffer): Buffer { return data; } }
class AudioExtractor { extract(file: VideoFile): Buffer { return Buffer.from([]); } }
class AudioMixer { mix(video: Buffer, audio: Buffer): Buffer { return video; } }
class BitrateReader {
static read(file: VideoFile): Buffer { return Buffer.from([]); }
static convert(buffer: Buffer, codec: MPEG4Codec | OGGCodec): Buffer {
return codec.encode(buffer);
}
}
// ✅ Facade — giấu hết codec, audio, mixing, encoding phía sau
class VideoConverterFacade {
convert(inputPath: string, outputFormat: string): string {
const file = new VideoFile(inputPath);
const sourceCodec = new CodecFactory().detect(file);
const codec = outputFormat === "mp4" ? new MPEG4Codec() : new OGGCodec();
const videoData = BitrateReader.read(file);
const audioData = new AudioExtractor().extract(file);
const convertedVideo = BitrateReader.convert(videoData, codec);
const finalFile = new AudioMixer().mix(convertedVideo, audioData);
const outputPath = inputPath.replace(/\.\w+$/, `.${outputFormat}`);
console.log(`Đã chuyển đổi ${inputPath} → ${outputPath}`);
return outputPath;
}
}
// Client chỉ cần biết một phương thức
const converter = new VideoConverterFacade();
converter.convert("video.avi", "mp4");2. Database Facade
// Hệ thống con phức tạp: connection pooling, query building, transaction
class ConnectionPool {
acquire(): Connection { /* ... */ return {} as Connection; }
release(conn: Connection): void { /* ... */ }
}
class QueryBuilder {
select(table: string, where?: Record<string, unknown>): string {
const conditions = where
? " WHERE " + Object.entries(where).map(([k, v]) => `${k} = '${v}'`).join(" AND ")
: "";
return `SELECT * FROM ${table}${conditions}`;
}
insert(table: string, data: Record<string, unknown>): string {
const cols = Object.keys(data).join(", ");
const vals = Object.values(data).map(v => `'${v}'`).join(", ");
return `INSERT INTO ${table} (${cols}) VALUES (${vals})`;
}
}
class TransactionManager {
begin(conn: Connection): void { /* ... */ }
commit(conn: Connection): void { /* ... */ }
rollback(conn: Connection): void { /* ... */ }
}
// ✅ Facade
class DatabaseFacade {
private pool = new ConnectionPool();
private queryBuilder = new QueryBuilder();
private txManager = new TransactionManager();
async findAll(table: string, where?: Record<string, unknown>): Promise<Row[]> {
const conn = this.pool.acquire();
try {
const sql = this.queryBuilder.select(table, where);
return await conn.execute(sql);
} finally {
this.pool.release(conn);
}
}
async insert(table: string, data: Record<string, unknown>): Promise<void> {
const conn = this.pool.acquire();
this.txManager.begin(conn);
try {
const sql = this.queryBuilder.insert(table, data);
await conn.execute(sql);
this.txManager.commit(conn);
} catch (error) {
this.txManager.rollback(conn);
throw error;
} finally {
this.pool.release(conn);
}
}
}
// Client — không cần quan tâm pool, builder hay transaction
const db = new DatabaseFacade();
const users = await db.findAll("users", { role: "admin" });
await db.insert("users", { name: "Alice", role: "user" });Adapter vs Facade: Khi Nào Dùng Cái Nào
| Khía Cạnh | Adapter | Facade |
|---|---|---|
| Mục đích | Chuyển interface A thành interface B | Gom nhiều thứ phức tạp sau một interface dễ dùng |
| Bọc bao nhiêu class | Thường là một | Nhiều |
| Làm gì với interface | Chuyển đổi cho khớp | Tạo mới, đơn giản hơn |
| Client biết gì | Không biết adaptee tồn tại | Không biết bên trong có gì |
| Dùng khi | Tích hợp thư viện bên thứ ba hoặc hệ thống cũ | Giảm coupling với hệ thống phức tạp bên trong |
| Nguyên tắc SOLID | Dependency Inversion | Interface Segregation |
Kết Hợp Cả Hai Pattern
Thực tế bạn hay dùng cả hai cùng lúc. Facade lo điều phối tổng thể, còn Adapter lo chuyển đổi interface của từng thư viện bên thứ ba:
// Adapter cho các dịch vụ bên thứ ba không tương thích
const stripeAdapter = new StripeAdapter(new StripeAPI());
const sendgridAdapter = new SendGridAdapter(new SendGridAPI());
// Facade sử dụng adapter làm hệ thống con
class OrderFacade {
constructor(
private payment: PaymentProcessor, // adapter
private emailer: EmailService, // adapter
private inventory: InventoryService // code của chúng ta
) {}
placeOrder(userId: string, items: CartItem[]): OrderResult {
// Facade điều phối; adapter xử lý chuyển đổi interface
this.inventory.reserve(items);
const payment = this.payment.charge(calculateTotal(items), "USD");
this.emailer.sendConfirmation(userId, payment.transactionId);
return { success: true };
}
}Những Lỗi Hay Mắc
1. Biến Adapter Thành "God Adapter"
// ❌ Adapter nhét cả business logic vào — sai vai trò
class StripeAdapter implements PaymentProcessor {
charge(amount: number, currency: string): PaymentResult {
if (amount > 10000) throw new Error("Số tiền quá cao"); // ❌ đây là business rule
if (currency !== "USD") amount *= getExchangeRate(currency); // ❌ logic business
const response = this.stripe.createCharge(/* ... */);
return { success: response.status === "succeeded", /* ... */ };
}
}
// ✅ Adapter chỉ lo chuyển đổi — business logic để chỗ khác
class StripeAdapter implements PaymentProcessor {
charge(amount: number, currency: string): PaymentResult {
const amountInCents = Math.round(amount * 100);
const response = this.stripe.createCharge(amountInCents, currency, generateKey());
return {
success: response.status === "succeeded",
transactionId: response.id,
};
}
}2. Facade Để Lộ "Ruột" Bên Trong
// ❌ Để lộ kiểu nội bộ ra ngoài facade
class OrderFacade {
placeOrder(userId: string, items: CartItem[]): {
payment: StripeResponse; // ❌ lộ kiểu Stripe
shipment: FedExShipment; // ❌ lộ kiểu FedEx
} {
// ...
}
}
// ✅ Facade dùng kiểu trả về của riêng mình
class OrderFacade {
placeOrder(userId: string, items: CartItem[]): OrderResult {
// kiểu riêng của facade — không lộ gì bên trong
}
}3. Lạm Dụng Facade Cho Mọi Thứ
// ❌ Thừa — bọc một lớp mà chẳng đơn giản hóa gì
class UserFacade {
getUser(id: string) {
return this.userRepository.findById(id); // gọi thẳng qua — vô nghĩa
}
}
// ✅ Chỉ dùng facade khi thật sự có sự phức tạp cần giấu đi
// Một lời gọi repository đơn lẻ thì gọi thẳng cho rồi4. Adapt Ngược Hướng
// ❌ Bẻ code MÌNH cho khớp interface bên thứ ba — sai hướng!
class OurPaymentAdapter implements StripeInterface {
// Đừng bao giờ uốn code của mình theo API bên ngoài
}
// ✅ Bẻ code BÊN THỨ BA cho khớp interface CỦA MÌNH
class StripeAdapter implements OurPaymentProcessor {
// Bên ngoài phải theo mình — mình sở hữu interface
}Tổng Kết
Adapter và Facade là hai structural pattern bạn sẽ dùng nhiều nhất. Cả hai đều bọc code có sẵn, nhưng vì mục đích khác nhau:
- Adapter là người phiên dịch — chuyển interface không tương thích thành interface code đang cần
- Facade là người gác cổng — giấu hết sự phức tạp bên trong, chỉ để lộ một interface gọn gàng
Ghi nhớ:
✅ Dùng Adapter khi cần tích hợp thư viện bên thứ ba, hệ thống cũ, hoặc bất kỳ code nào có interface không khớp
✅ Dùng Facade khi hệ thống con quá nhiều class và client cần một API đơn giản hơn
✅ Ưu tiên Object Adapter (composition) — linh hoạt hơn Class Adapter và ngôn ngữ nào cũng dùng được
✅ Adapter chỉ lo chuyển đổi — business logic để chỗ khác
✅ Facade phải giấu kín nội bộ — đừng để lộ kiểu dữ liệu của hệ thống con qua API
✅ Có thể kết hợp cả hai: Facade lo điều phối, Adapter lo chuyển đổi bên trong
Tiếp Theo
Các bài tiếp theo trong series OOP & Design Patterns:
- OOP-13: Decorator và Proxy Patterns — bọc đối tượng để mở rộng hoặc kiểm soát hành vi
- OOP-14: Strategy và Template Method Patterns — behavioral pattern cho các thuật toán có thể hoán đổi
- OOP-15: Observer, Command và State Patterns — behavioral pattern cho xử lý sự kiện và máy trạng thái
Bài Tập Thực Hành
-
Multi-Cloud Storage Adapter: Tạo interface
StorageServicevớiupload(key, data),download(key)vàdelete(key). Viết adapter cho AWS S3 (dùngputObject/getObject/deleteObject) và Google Cloud Storage (dùngbucket.file.save()/bucket.file.download()/bucket.file.delete()). Viết test chứng minh có thể đổi provider mà không sửa code client. -
Home Automation Facade: Dựng
SmartHomeFacadevớigoodMorning(),leaveHome(),movieMode()vàgoodNight(). Mỗi phương thức phối hợp nhiều hệ thống con: đèn (độ sáng, nhiệt độ màu), điều hòa (nhiệt độ, chế độ), an ninh (bật/tắt, camera), giải trí (TV, loa). Viết bằng TypeScript với ít nhất 4 class hệ thống con. -
Legacy API Migration: Giả sử bạn có REST client cũ trả về chuỗi XML. Viết adapter để nó tuân theo interface
ApiClienthiện đại (trả typed object). Sau đó dựng facade gộp nhiều lời gọi API đã adapt thành một phương thứcgetDashboardData()duy nhất. -
Adapter + Facade Kết Hợp: Xây dựng hệ thống gửi email thông báo.
NotificationFacade.notify(userId, event)sẽ tra cứu preference của user, render template, rồi gửi email. Phần gửi email dùng adapter pattern để dễ dàng đổi giữa SendGrid, Mailgun và console logger khi test.
Phần 12 trong series OOP & Design Patterns. Bài structural pattern đầu tiên, tiếp nối Prototype Pattern.
📬 Đăng ký nhận bản tin
Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.
Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.
💬 Comments
Đăng nhập để bình luận
Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.