Design patterns hay hỏi, giải bằng bài toán của shop-backend

4 câu hỏi~35 phútTừ khóa: Singleton, Factory, Builder, Strategy, Observer, Decorator, Adapter, Template Method, Proxy, over-engineering

Tiếp đi: design pattern. Phỏng vấn hay hỏi những pattern nào?

Phỏng vấn ít khi bắt vẽ UML. Họ hỏi "pattern này giải quyết vấn đề gì, em đã dùng chưa". Nên mỗi pattern dưới đây đi kèm một vấn đề thật trong shop-backend.

1. Bài toán: OrderService làm mọi thứ

public PaymentResult checkout(Order order, String method) {
    if (method.equals("MOMO")) { ... } else if (method.equals("VNPAY")) { ... } else if ...
    double discount = order.getCustomer().isVip() ? 0.1 : (isBlackFriday() ? 0.2 : 0);
    emailService.send(...); smsService.send(...); loyaltyService.addPoints(...);
    // gọi SDK cũ của đơn vị vận chuyển với API rất khác
}

2. Vấn đề

3. Giải pháp: 3 nhóm pattern

Creational  (tạo object)     : Singleton, Factory, Builder
Structural  (ghép object)    : Adapter, Decorator, Proxy
Behavioral  (chia hành vi)   : Strategy, Observer, Template Method

4. Code từng bước

Bước 1: Strategy, mỗi khuyến mãi một class

public interface DiscountPolicy { BigDecimal apply(Order order); }

@Component class VipDiscount implements DiscountPolicy { ... }
@Component class BlackFridayDiscount implements DiscountPolicy { ... }

BigDecimal discount = policies.stream().map(p -> p.apply(order)).max(naturalOrder()).orElse(ZERO);

Bước 2: Factory, chọn cổng thanh toán theo tên

@Component
public class PaymentGatewayFactory {
    private final Map<String, PaymentGateway> byType;
    public PaymentGatewayFactory(List<PaymentGateway> gateways) {
        byType = gateways.stream().collect(toMap(PaymentGateway::type, g -> g));
    }
    public PaymentGateway get(String type) {
        return Optional.ofNullable(byType.get(type))
                .orElseThrow(() -> new IllegalArgumentException("Unsupported: " + type));
    }
}

Bước 3: Builder, tạo Order nhiều field tùy chọn

Order order = Order.builder()
        .customerId("C-1")
        .line("SKU-1", 2)
        .voucher("SALE10")          // tùy chọn
        .note("Giao giờ hành chính")  // tùy chọn
        .build();                   // build() validate một chỗ

Thay cho constructor 8 tham số dễ truyền nhầm thứ tự. Lombok @Builder sinh sẵn.

Bước 4: Observer, các việc sau khi đặt hàng

public record OrderPlaced(String orderId) {}
publisher.publishEvent(new OrderPlaced(order.getId()));   // OrderService không biết ai nghe
@EventListener void sendEmail(OrderPlaced e) { ... }      // thêm listener mới không sửa OrderService

Chi tiết cách Spring làm xem Events, scheduling, @Async.

Bước 5: Adapter, bọc SDK vận chuyển cũ

public interface ShippingProvider { String createShipment(Order o); }

public class LegacyGhnAdapter implements ShippingProvider {
    private final GhnClientV1 sdk;                    // API cũ: createWaybill(Map<String,String>)
    public String createShipment(Order o) {
        Map<String, String> req = Map.of("ma_don", o.getId(), "dia_chi", o.getAddress());
        return sdk.createWaybill(req).getCode();      // đổi interface này sang interface kia
    }
}

Bước 6: Decorator, thêm retry + log quanh gateway

public class RetryingGateway implements PaymentGateway {
    private final PaymentGateway inner;               // cùng interface, bọc object khác
    public PaymentResult charge(Order o) {
        for (int i = 1; ; i++) {
            try { return inner.charge(o); }
            catch (PaymentGatewayException e) { if (i == 3) throw e; }
        }
    }
}
PaymentGateway g = new LoggingGateway(new RetryingGateway(new MomoGateway()));

Bước 7: Template Method và Proxy

Bước 8: Singleton

public enum IdGenerator {                  // cách an toàn nhất: enum
    INSTANCE;
    private final AtomicLong seq = new AtomicLong();
    public long next() { return seq.incrementAndGet(); }
}

5. Cách hoạt động: Decorator vs Proxy vs Adapter

Adapter:    Client ──ShippingProvider──▶ Adapter ──API khác──▶ GhnClientV1
Decorator:  Client ──Gateway──▶ Logging ──Gateway──▶ Retry ──Gateway──▶ Momo
Proxy:      Client ──Service──▶ Proxy(begin tx) ──Service──▶ Service thật (commit/rollback)

6. So sánh

PatternVấn đề trong shop-backendÝ chính
SingletonMột IdGenerator cho cả appĐúng 1 instance
FactoryChọn gateway theo "MOMO"Giấu logic tạo/chọn object
BuilderOrder nhiều field tùy chọnTạo từng bước, validate cuối
StrategyNhiều kiểu khuyến mãiĐổi thuật toán qua interface
ObserverEmail, SMS, điểm sau đặt hàngPhát sự kiện, ai cần thì nghe
AdapterSDK vận chuyển khác interfaceĐổi interface
DecoratorRetry, log quanh gatewayThêm hành vi, giữ interface
Template MethodKhung pay() chungKhung cố định ở cha, bước ở con
ProxyTransaction quanh serviceĐứng thay, kiểm soát truy cập

7. Khi nào nên dùng / không nên dùng

Singleton tự viết với getInstance() có vấn đề gì? Mình thấy nhiều bài viết gọi nó là anti-pattern.

Đúng rồi, Singleton "cổ điển" có mấy vấn đề:

public class PaymentConfig {
    private static volatile PaymentConfig instance;
    private PaymentConfig() {}
    public static PaymentConfig getInstance() {             // double-checked locking
        if (instance == null) {
            synchronized (PaymentConfig.class) {
                if (instance == null) instance = new PaymentConfig();
            }
        }
        return instance;
    }
}

Spring giải quyết bằng singleton scope: container tạo 1 instance và inject vào nơi cần. Vẫn chỉ 1 instance, nhưng class là class thường, test thì truyền mock qua constructor. Lưu ý: singleton của Spring là "1 bean trong 1 container", không phải 1 trong cả JVM. Chi tiết scope xem Bean scope & lifecycle.

Decorator với Proxy code gần như y hệt: cùng implement interface, bọc object bên trong. Khác nhau ở đâu?

Bạn nhìn đúng: về cấu trúc thì gần như giống nhau. Khác ở mục đích và ai lắp ghép:

DecoratorProxy
Mục đíchThêm tính năng (retry, log, nén)Kiểm soát truy cập (lazy, transaction, security, remote)
Ai tạoClient tự xếp lớp, xếp nhiều lớp tùy ýFramework tạo, client thường không biết
Ví dụ JDK/SpringBufferedInputStream(new FileInputStream)@Transactional, Hibernate lazy proxy

Tương tự Strategy và State: Strategy do client chọn từ ngoài; State thì object tự đổi trạng thái bên trong (ví dụ Order chuyển NEW → PAID → SHIPPED, mỗi state cho phép hành động khác). Phỏng vấn chấp nhận câu trả lời "cấu trúc giống, ý định khác".

Phỏng vấn hỏi "em đã dùng design pattern nào trong dự án?" thì trả lời sao?

Câu 1: Em đã dùng pattern nào?

🇻🇳 Tiếng Việt:

Trong hệ thống bán hàng, em dùng Strategy cho các chính sách giảm giá, mỗi chính sách một bean implement DiscountPolicy; Factory để chọn cổng thanh toán theo loại, Spring inject List<PaymentGateway> rồi em gom thành Map; Builder để tạo Order; Observer qua ApplicationEvent để gửi email, cộng điểm sau khi đặt hàng. Nhờ vậy thêm cổng hay khuyến mãi mới không phải sửa OrderService.

🇬🇧 English:

In an e-commerce backend I used Strategy for discount policies, each one a bean implementing DiscountPolicy; a Factory to pick the payment gateway by type, built from an injected List<PaymentGateway>; a Builder for Order; and Observer through ApplicationEvent for emails and loyalty points after an order is placed. Adding a new gateway or promotion didn't require touching OrderService.

Câu 2: Singleton có nhược điểm gì? Viết thread-safe thế nào?

🇻🇳 Tiếng Việt:

Singleton tự viết tạo phụ thuộc cứng, state global, khó test. Thread-safe thì dùng enum, holder class, hoặc double-checked locking với volatile. Trong Spring em để container quản lý singleton bean và inject qua constructor.

🇬🇧 English:

A hand-written singleton creates hard dependencies and global state and is hard to test. For thread safety use an enum, a holder class, or double-checked locking with volatile. In Spring I let the container manage singleton beans and inject them through constructors.

Câu 3: Khi nào không nên dùng pattern?

🇻🇳 Tiếng Việt:

Khi chưa có điểm thay đổi thật. Pattern thêm class và tầng gián tiếp; nếu chỉ có một cài đặt thì code đơn giản dễ đọc hơn. Em thêm pattern khi xuất hiện biến thể thứ hai hoặc thứ ba.

🇬🇧 English:

When there is no real variation yet. Patterns add classes and indirection; with only one implementation, plain code is easier to read. I introduce a pattern when the second or third variant shows up.

Cách nhớ ngắn gọn


Tự kiểm tra

Thêm cổng ZaloPay vào Factory dùng List<PaymentGateway> cần sửa gì?

Chỉ thêm class @Component ZaloPayGateway; Factory tự nhận qua list inject.

Vì sao enum là cách viết Singleton an toàn nhất?

JVM đảm bảo tạo một lần, thread-safe, chống reflection và serialization tạo bản thứ hai.

Builder giải quyết vấn đề gì của constructor?

Constructor nhiều tham số (nhất là tùy chọn) dễ truyền nhầm thứ tự; builder đặt tên từng field và validate ở build().

BufferedInputStream là pattern gì?

Decorator: bọc một InputStream khác, cùng interface, thêm buffer.

Adapter khác Decorator thế nào?

Adapter đổi interface này sang interface khác; Decorator giữ nguyên interface, thêm hành vi.