23 Design Patterns cho Vibe Coder
Bản đồ 23 mẫu thiết kế Gang of Four cho người code cùng AI: nhận diện qua triệu chứng, ví dụ TypeScript trước/sau, hình minh hoạ và prompt AI mẫu. Tiếng Việt, cho người mới.
Contents
Bản đồ 23 mẫu thiết kế Gang of Four viết lại cho người code cùng AI. Mục tiêu: không học thuộc — chỉ cần nhìn ra “code có dạng như vậy” là gọi đúng tên pattern để prompt AI.
Tại sao nên biết TÊN pattern?
Bạn không cần nhớ định nghĩa hàn lâm. Bạn cần cái tên — vì tên pattern là từ vựng nén để nói chuyện với AI. Prompt “refactor đi” thì AI đoán mò; “tách phần tính phí thành Strategy” thì AI biết chính xác cấu trúc bạn muốn.
Nguyên tắc vàng: pattern dùng để giải một vấn đề đã xuất hiện, không phải rắc lên code cho “pro”. Thấy đau ở đâu, chữa ở đó.
Cách dùng bài này
- Nhận dạng code — thấy triệu chứng lạ (hàm đầy
if/else, đổi provider phải sửa 10 chỗ, service gọi cứng nhau…) thì dừng lại. - Tra bảng triệu chứng → pattern ở mỗi nhóm bên dưới.
- Xem hình “trước/sau” + ví dụ để chắc đúng bệnh.
- Prompt AI bằng tên pattern. Xong.
6 pattern lõi (nắm chắc là xử được phần lớn)
- Factory — gom việc tạo object về một chỗ (“đổi provider = sửa nhiều chỗ”)
- Adapter — app nói một ngôn ngữ, adapter dịch qua lại (“code phải biết định dạng riêng từng service”)
- Facade — UI gọi một hàm, facade lo phần còn lại (“UI tự điều phối cả quy trình”)
- Observer — phát một event, ai quan tâm tự nhận (“service gọi cứng lẫn nhau”)
- Strategy — đổi thuật toán sau một lời gọi (“một hàm đầy if/else”)
- Singleton — mọi nơi chia sẻ đúng một instance (“mỗi module tự tạo một connection”)
4 cặp dễ nhầm nhất
- Adapter vs Facade — Adapter dịch interface không tương thích; Facade đơn giản hoá hệ thống phức tạp.
- Strategy vs State — Strategy: dev chọn thuật toán; State: object tự đổi hành vi theo trạng thái.
- Observer vs Mediator — Observer: một nguồn broadcast rời rạc; Mediator: một trung tâm điều phối 2 chiều.
- Các loại Factory — Simple: 1 hàm chọn. Method: subclass quyết. Abstract: tạo cả một bộ khớp nhau.
Nhóm Creational — Tạo object thế nào cho khỏi dính cứng
Nhóm Creational (khởi tạo) trả lời đúng một câu hỏi: “Khi cần một object mới, tôi tạo nó ở đâu và bằng cách nào để sau này đổi loại, thêm tuỳ chọn hay dùng lại mà không phải sửa khắp nơi?” Thay vì rải lệnh new và if khắp code, bạn gom việc tạo object vào một chỗ có tên rõ ràng. Nhờ vậy khi khách đổi cổng thanh toán, đổi kho ảnh, thêm tuỳ chọn mới… bạn sửa 1 nơi thay vì 20 nơi.
Mục lục
| Pattern | Dùng khi | Link |
|---|---|---|
| Simple Factory | Nhiều chỗ tự new từng loại kèm if/switch | #1 |
| Factory Method | Flow chung, mỗi loại tạo dependency khác nhau | #2 |
| Abstract Factory | Cần cả một bộ object phải hợp nhau | #3 |
| Builder | Constructor quá nhiều tham số, dễ điền nhầm | #4 |
| Prototype | Dựng lại từ đầu tốn công, cấu hình gần giống | #5 |
| Singleton | Cần đúng một instance duy nhất toàn app | #6 |
1. Simple Factory — nhà máy đơn giản
Một câu: Thay vì mỗi chỗ tự tay lắp ráp object theo loại, bạn đưa cho một “quầy nhận đơn” cái tên loại, nó trả về object đã lắp sẵn.
Dấu hiệu bạn đang cần nó
- Bạn thấy
if (type === 'stripe') new StripeClient() else if (type === 'paypal')...lặp lại ở nhiều file. - Thêm một cổng thanh toán mới (VietQR) phải đi sửa 5–6 chỗ giống hệt nhau.
- Code gọi trực tiếp
newcủa từng lớp cụ thể, nên khó đổi/khó test.
Hình dung nhanh: Như quầy bán vé: bạn nói “cho tôi vé tàu”, nhân viên tự biết in loại vé đúng — bạn không cần biết máy in nào. Trong dự án web: gọi createPaymentProvider('vietqr') và nhận về đối tượng thanh toán đã sẵn sàng, không quan tâm bên trong lắp thế nào.
Ẩn dụ (Design Patterns for Humans): Xây nhà cần cửa — thay vì tự mua gỗ, đinh, keo về đóng và bày bừa, bạn gọi xưởng đặt cửa làm sẵn giao tận nơi. Cái xưởng đó chính là factory.
Khi CHƯA có pattern
// Đoạn này bị COPY ở checkout.ts, refund.ts, admin.ts...
function pay(type: string, amount: number) {
let provider;
if (type === "stripe") provider = new StripeClient(process.env.STRIPE_KEY!);
else if (type === "paypal") provider = new PaypalClient(process.env.PP_ID!);
else if (type === "vietqr") provider = new VietQrClient(process.env.VQR_KEY!);
else throw new Error("Cổng không hỗ trợ");
return provider.charge(amount);
}
Khi ÁP DỤNG pattern
// payment-factory.ts — CHỈ MỘT nơi biết cách tạo
export function createPaymentProvider(type: string): PaymentProvider {
switch (type) {
case "stripe": return new StripeClient(process.env.STRIPE_KEY!);
case "paypal": return new PaypalClient(process.env.PP_ID!);
case "vietqr": return new VietQrClient(process.env.VQR_KEY!);
default: throw new Error("Cổng không hỗ trợ: " + type);
}
}
// Mọi nơi khác chỉ cần:
const provider = createPaymentProvider(type);
provider.charge(amount);
Prompt AI mẫu: "Code tôi đang if/else khởi tạo Stripe/PayPal/VietQR ở nhiều file. Gom lại thành một Simple Factory createPaymentProvider(type) trả về interface PaymentProvider chung, rồi thay các chỗ new trực tiếp bằng lời gọi factory."
Đừng dùng khi: Bạn chỉ có đúng 1 loại và không có kế hoạch thêm — thì cứ new thẳng, đừng bọc.
Dễ nhầm với: Factory Method (dưới) — Simple Factory là một hàm quyết định loại; Factory Method để lớp con tự quyết định loại nó tạo.
2. Factory Method — phương thức nhà máy
Một câu: Bạn có một quy trình chung, nhưng để cho từng “phiên bản” của lớp tự quyết định nó cần tạo ra loại phụ tùng nào.
Dấu hiệu bạn đang cần nó
- Nhiều loại chạy cùng một flow (gửi chiến dịch), nhưng mỗi loại cần một thành phần con khác (email vs push).
- Bạn muốn thêm loại mới mà không sửa flow chung — chỉ viết thêm một lớp con.
- Bạn thấy giữa flow lại có
if (loại này) dùng cái này, else dùng cái kia.
Hình dung nhanh: Cùng công thức “pha đồ uống” nhưng quán trà tạo ra ly trà, quán cà phê tạo ly cà phê — các bước còn lại y hệt. Trong dự án web: Campaign có flow run() chung, nhưng EmailCampaign tạo EmailSender, PushCampaign tạo PushSender.
Ẩn dụ (Design Patterns for Humans): Trưởng phòng tuyển dụng không thể tự phỏng vấn mọi vị trí; tuỳ tin tuyển dụng, bà giao các bước phỏng vấn cho người phù hợp. Việc “tạo” được uỷ cho lớp con quyết định.
Khi CHƯA có pattern
class Campaign {
run(type: string, users: string[]) {
// flow chung
let sender;
if (type === "email") sender = new EmailSender(); // if lọt vào giữa flow
else sender = new PushSender();
const msg = this.render();
for (const u of users) sender.send(u, msg);
}
render() { return "Nội dung..."; }
}
Khi ÁP DỤNG pattern
abstract class Campaign {
// flow chung — không đổi khi thêm loại mới
run(users: string[]) {
const sender = this.createSender(); // lớp con tự quyết
const msg = this.render();
for (const u of users) sender.send(u, msg);
}
render() { return "Nội dung..."; }
protected abstract createSender(): Sender; // <- factory method
}
class EmailCampaign extends Campaign {
protected createSender() { return new EmailSender(); }
}
class PushCampaign extends Campaign {
protected createSender() { return new PushSender(); }
}
Prompt AI mẫu: "Tách Campaign thành lớp trừu tượng có flow run() chung và một factory method abstract createSender(). Cho EmailCampaign và PushCampaign kế thừa, mỗi lớp override createSender trả về sender riêng, để thêm loại mới không phải sửa run()."
Đừng dùng khi: Chỉ có 1–2 biến thể đơn giản và flow ngắn — một Simple Factory hoặc tham số truyền vào là đủ, khỏi đẻ cả cây kế thừa.
Dễ nhầm với: Simple Factory — Simple Factory tập trung việc chọn loại vào một hàm; Factory Method phân tán việc chọn loại xuống từng lớp con để mở rộng bằng kế thừa.
3. Abstract Factory — nhà máy trừu tượng
Một câu: Khi bạn cần cả một bộ đồ ăn khớp với nhau (không lẫn lộn), bạn chọn một “nhà máy” và nó giao cho bạn trọn bộ cùng hệ.
Dấu hiệu bạn đang cần nó
- Bạn tạo ra nhiều object phải cùng một “họ” và không được trộn lẫn (client + verifier + refund của Stripe).
- Đang có nguy cơ ghép nhầm: dùng
StripeClientnhưng lỡ dùngPaypalVerifier. - Muốn đổi cả bộ (từ Stripe sang PayPal) chỉ bằng cách đổi một chỗ chọn nhà máy.
Hình dung nhanh: Mua nội thất “trọn bộ Bắc Âu” — ghế, bàn, đèn cùng tông, không lo cọc cạch. Trong dự án web: StripeFactory giao cho bạn payment client + verifier + refund client đều của Stripe, đảm bảo cả 3 nói cùng “ngôn ngữ”.
Ẩn dụ (Design Patterns for Humans): Cửa gỗ phải đi với thợ mộc, cửa sắt phải đi với thợ hàn — bạn cần lấy nguyên MỘT BỘ (cửa + đúng loại thợ lắp) khớp nhau, không lắp lẫn lộn.
Khi CHƯA có pattern
// Tự ghép tay — dễ lỡ trộn nhầm nhà cung cấp
const client = new StripeClient(key);
const verifier = new PaypalVerifier(); // lệch họ, sẽ verify sai
const refund = new StripeRefund(key);
Khi ÁP DỤNG pattern
interface PaymentFactory {
createClient(): PaymentClient;
createVerifier(): Verifier;
createRefund(): RefundClient;
}
class StripeFactory implements PaymentFactory {
constructor(private key: string) {}
createClient() { return new StripeClient(this.key); }
createVerifier() { return new StripeVerifier(this.key); } // luôn cùng họ
createRefund() { return new StripeRefund(this.key); }
}
// Dùng: đổi cả bộ bằng cách đổi 1 dòng
const factory: PaymentFactory = new StripeFactory(key);
const client = factory.createClient();
const verifier = factory.createVerifier(); // chắc chắn khớp client
Prompt AI mẫu: "Tôi cần payment client, verifier và refund luôn cùng một nhà cung cấp. Tạo interface PaymentFactory với createClient/createVerifier/createRefund, rồi StripeFactory và PaypalFactory implement để không bao giờ trộn nhầm các thành phần khác họ."
Đừng dùng khi: Bạn chỉ cần tạo một loại object, không có “bộ” nào phải khớp nhau — dùng Simple Factory cho nhẹ.
Dễ nhầm với: Simple Factory tạo một object; Abstract Factory tạo một nhóm object cùng họ. Nếu chỉ có 1 sản phẩm thì đừng lên tới Abstract Factory.
4. Builder — thợ lắp ráp
Một câu: Thay vì nhồi mười mấy tham số vào một constructor dễ điền nhầm thứ tự, bạn lắp object từng bước bằng những câu có tên rõ ràng.
Dấu hiệu bạn đang cần nó
- Constructor có quá nhiều tham số (nhất là nhiều
boolean/undefinedxếp hàng dễ nhầm chỗ). - Nhiều tuỳ chọn không bắt buộc, nhìn
new X(a, undefined, true, false, undefined...)không hiểu gì. - Bạn muốn tạo object theo nhiều “công thức” khác nhau nhưng dùng chung các bước.
Hình dung nhanh: Gọi món ở tiệm bánh mì: “cho ổ bánh mì, thêm pate, không hành, thêm ớt” — nói từng ý thay vì đọc một dãy số khó hiểu. Trong dự án web: dựng cấu hình một sản phẩm WooCommerce có chục tuỳ chọn (giá, tồn kho, biến thể, thuế, vận chuyển…).
Ẩn dụ (Design Patterns for Humans): Gọi combo có sẵn “Big Hardee” thì họ đưa luôn (đó là simple factory). Nhưng làm bánh Subway kiểu của bạn — chọn loại bánh, sốt, phô mai qua từng bước — thì cần Builder.
Khi CHƯA có pattern
// Ai nhớ nổi tham số thứ 7 là gì? Dễ đảo nhầm true/false
const product = new WooProduct(
"Áo thun", 199000, 50, true, false, "kg", 0.3, true, undefined, "VN"
);
Khi ÁP DỤNG pattern
class WooProductBuilder {
private p: any = {};
name(v: string) { this.p.name = v; return this; }
price(v: number) { this.p.price = v; return this; }
stock(v: number) { this.p.stock = v; return this; }
taxable(v: boolean) { this.p.taxable = v; return this; }
weightKg(v: number) { this.p.weightKg = v; return this; }
build() { return this.p as WooProduct; }
}
// Đọc là hiểu, khỏi đếm vị trí tham số:
const product = new WooProductBuilder()
.name("Áo thun").price(199000).stock(50)
.taxable(true).weightKg(0.3)
.build();
Prompt AI mẫu: "Constructor WooProduct đang có hơn 10 tham số rất dễ điền nhầm. Viết một WooProductBuilder theo kiểu chuỗi (fluent) với các method name/price/stock/taxable/weightKg trả về this, và build() ở cuối trả về object hoàn chỉnh."
Đừng dùng khi: Object chỉ có 2–3 tham số rõ ràng — builder khi đó chỉ làm code dài ra vô ích.
Dễ nhầm với: Factory — Factory chọn loại nào để tạo; Builder tập trung lắp một object phức tạp từng bước. Hai cái có thể dùng chung nhau.
5. Prototype — nhân bản mẫu
Một câu: Khi dựng object mới từ số 0 tốn công mà cấu hình gần như y hệt cái đã có, bạn cứ nhân bản (clone) cái mẫu rồi chỉnh vài chỗ.
Dấu hiệu bạn đang cần nó
- Tạo object mới phải setup rất nhiều bước, nhưng 90% giống một object đã dựng sẵn.
- Bạn đang copy tay từng field từ object cũ sang object mới — dài dòng, dễ sót.
- Muốn có nhiều “phiên bản gần giống” từ một mẫu chuẩn (template).
Hình dung nhanh: Photocopy một tờ mẫu rồi điền vài ô khác nhau, thay vì viết lại cả tờ từ đầu. Trong dự án web: một mẫu email marketing chuẩn (header, footer, style), mỗi chiến dịch chỉ clone rồi đổi tiêu đề và nút CTA.
Ẩn dụ (Design Patterns for Humans): Nhớ cừu Dolly được nhân bản chứ? Cốt lõi là CLONE từ một bản đã có, không dựng lại từ đầu.
Khi CHƯA có pattern
// Dựng lại từ đầu mỗi lần — lặp cả đống cấu hình giống nhau
const promo = new EmailTemplate();
promo.header = "D-Solutions"; promo.footer = "© 2026 D-Solutions";
promo.font = "IBM Plex"; promo.brandColor = "#F5B301";
promo.layout = "single-column"; /* ...còn 10 dòng nữa... */
promo.title = "Sale tháng 8"; // thứ DUY NHẤT thật sự khác
Khi ÁP DỤNG pattern
class EmailTemplate {
header = ""; footer = ""; font = ""; brandColor = "";
layout = ""; title = "";
clone(): EmailTemplate { return Object.assign(new EmailTemplate(), this); }
}
// Dựng MẪU CHUẨN một lần
const base = new EmailTemplate();
Object.assign(base, {
header: "D-Solutions", footer: "© 2026 D-Solutions",
font: "IBM Plex", brandColor: "#F5B301", layout: "single-column",
});
// Mỗi chiến dịch chỉ clone rồi đổi phần khác biệt
const promo = base.clone(); promo.title = "Sale tháng 8";
const news = base.clone(); news.title = "Bản tin tháng 9";
Prompt AI mẫu: "Tôi có một EmailTemplate cấu hình rất nhiều field nhưng đa số chiến dịch chỉ khác title và CTA. Thêm method clone() (dùng Object.assign) để tôi tạo một base template rồi nhân bản cho từng chiến dịch, chỉ override phần khác."
Đừng dùng khi: Object rẻ tiền, tạo mới nhanh gọn — clone chỉ thêm rối. Và cẩn thận clone nông (shallow) khi bên trong có object lồng nhau (cần deep clone).
Dễ nhầm với: Builder — Builder lắp từ đầu theo từng bước; Prototype sao chép một cái có sẵn rồi sửa nhẹ. Chọn Prototype khi “cái mẫu” đã tồn tại và đắt để dựng lại.
6. Singleton — một bản duy nhất
Một câu: Có những thứ cả app chỉ nên có đúng một cái (một kết nối, một config) — Singleton đảm bảo mọi nơi đều xài chung một bản, không ai tự đẻ thêm.
Dấu hiệu bạn đang cần nó
- Nhiều module mỗi chỗ tự tạo một kết nối (DB pool, analytics client) → tốn tài nguyên, số liệu lệch.
- Config đọc từ file/env bị load lại nhiều lần ở nhiều nơi.
- Bạn cần một điểm chung để mọi phần của app cùng đọc/ghi một trạng thái.
Hình dung nhanh: Cả toà nhà dùng chung một đồng hồ điện tổng, không phải mỗi phòng lắp một cái. Trong dự án web: một DbPool duy nhất cho toàn site, hay một AnalyticsClient để mọi trang gửi event về cùng một chỗ.
Ẩn dụ (Design Patterns for Humans): Một đất nước tại một thời điểm chỉ có một tổng thống; hễ việc nước cần là gọi đúng người đó. Tổng thống ở đây là singleton.
Khi CHƯA có pattern
// db.ts được import ở nhiều nơi, mỗi import lại new một pool mới
export function getPool() {
return new DbPool(process.env.DATABASE_URL!); // tạo mới mỗi lần gọi
}
// checkout.ts: const pool = getPool();
// report.ts: const pool = getPool(); // pool KHÁC → cạn kết nối
Khi ÁP DỤNG pattern
class DbPool {
private static instance: DbPool | null = null;
private constructor(url: string) { /* mở kết nối tốn kém 1 lần */ }
static get(): DbPool {
if (!DbPool.instance) {
DbPool.instance = new DbPool(process.env.DATABASE_URL!);
}
return DbPool.instance; // luôn trả về CÙNG một bản
}
}
// Mọi nơi: DbPool.get() → cùng một pool duy nhất
const pool = DbPool.get();
Prompt AI mẫu: "Mỗi module đang tự new một DbPool riêng làm cạn kết nối. Chuyển DbPool thành Singleton: constructor private, một static get() tạo instance lần đầu rồi tái sử dụng, để toàn app dùng chung một pool."
Đừng dùng khi: Chỉ để làm “biến toàn cục cho tiện” — Singleton dùng bừa khiến code khó test và khó lần vết. Chỉ dùng khi thật sự cần đúng một bản dùng chung.
Dễ nhầm với: Không phải “factory”. Factory tạo nhiều object; Singleton đảm bảo chỉ một. Nếu framework của bạn đã có dependency injection, thường nên đăng ký “một instance dùng chung” ở đó thay vì tự viết Singleton.
Nhóm Structural — Lắp ghép & tổ chức các mảnh
Nhóm Creational lo chuyện tạo ra đồ vật. Nhóm Structural này lo chuyện tiếp theo: ghép các mảnh code lại với nhau sao cho gọn gàng — làm sao để những thứ không cùng “khuôn” vẫn nói chuyện được, làm sao để một đống thành phần lồng nhau vẫn dễ điều khiển, và làm sao che bớt cái rối bên trong để bên ngoài dùng cho nhẹ đầu.
Nói ngắn: nếu bạn thấy code đang rối như mớ dây, hoặc lặp lại vì mỗi thứ một kiểu, hoặc một chỗ phải biết quá nhiều thứ — thì câu trả lời thường nằm ở nhóm này.
Mục lục nhanh
| Pattern | Dùng khi | |
|---|---|---|
| Adapter | Nhiều service, mỗi cái một cú pháp — muốn code chỉ nói một thứ tiếng | ↓ |
| Bridge | Một tính năng có 2 trục thay đổi độc lập, đang nhân nhau ra quá nhiều class | ↓ |
| Composite | Dữ liệu dạng cây — muốn đối xử “ô lẻ” và “nhóm chứa nhiều ô” như nhau | ↓ |
| Decorator | Muốn thêm/bớt tính năng theo lớp, thay vì nhồi hết vào một class | ↓ |
| Facade | Một thao tác gồm nhiều bước rối — muốn gói lại sau một hàm | ↓ |
| Flyweight | Có rất nhiều object trùng lặp dữ liệu, tốn RAM | ↓ |
| Proxy | Muốn kiểm soát lối vào một object (cache / quyền / lazy) mà không sửa nơi gọi | ↓ |
1. Adapter — bộ chuyển đổi
Một câu: Giống cục chuyển ổ cắm khi đi nước ngoài — thiết bị không đổi, chỉ cần một miếng dịch giữa phích và ổ.
Dấu hiệu bạn đang cần nó
- Code của bạn phải nhớ cú pháp riêng của từng service (Cloudinary gọi kiểu này, S3 gọi kiểu kia).
- Đổi nhà cung cấp là phải đi sửa rải rác khắp nơi.
- Bạn muốn “sau này thích đổi qua service khác cho rẻ” mà không đập lại cả app.
Hình dung nhanh: Ngoài đời là cục chuyển ổ điện — laptop cứ cắm bình thường, cục adapter lo phần khác chuẩn. Trong dự án web: bạn có 3 kho lưu ảnh (Cloudinary, S3, Supabase), mỗi cái một hàm upload khác nhau; bạn viết 3 adapter, tất cả cùng lộ ra upload(file) trả về URL chuẩn.
Ẩn dụ (Design Patterns for Humans): Muốn chuyển ảnh từ thẻ nhớ sang máy tính, bạn cần đầu đọc thẻ để cắm vừa cổng máy. Đầu đọc thẻ chính là adapter — giống phích 3 chân cần cục chuyển để cắm vào ổ 2 chấu.
Khi CHƯA có pattern
// App phải biết chi tiết từng service → dính chặt
async function saveAvatar(file: File, provider: string) {
if (provider === "cloudinary") {
const r = await cloudinary.uploadImg(file, { folder: "avatars" });
return r.secure_url;
} else if (provider === "s3") {
const r = await s3.putObject({ Bucket: "avatars", Body: file });
return `https://cdn/${r.Key}`; // mỗi service trả một kiểu
} // thêm Supabase lại phải sửa hàm này...
}
Khi ÁP DỤNG pattern
interface Storage {
upload(file: File): Promise<string>; // ai cũng phải nói tiếng này
}
const cloudinaryStorage: Storage = {
async upload(file) {
const r = await cloudinary.uploadImg(file, { folder: "avatars" });
return r.secure_url; // dịch về chuẩn chung: một cái URL
},
};
// App chỉ cần biết Storage, không quan tâm bên dưới là ai
async function saveAvatar(file: File, storage: Storage) {
return storage.upload(file);
}
Prompt AI mẫu: "Tôi có nhiều dịch vụ lưu ảnh với API khác nhau. Hãy tạo một interface Storage chung với hàm upload(file) trả về URL, rồi viết Adapter cho từng dịch vụ để app chỉ phụ thuộc vào interface đó."
Đừng dùng khi: bạn chắc chắn cả đời chỉ xài đúng một service — thêm adapter chỉ tổ rườm rà.
Dễ nhầm với: Facade (Facade gói nhiều bước phức tạp lại; Adapter chỉ dịch một cái sang đúng khuôn khác mà không thêm bước nào). Proxy giữ nguyên khuôn cũ nhưng chặn/kiểm soát; Adapter thì đổi khuôn.
2. Bridge — cây cầu nối
Một câu: Khi một thứ vừa có “loại nội dung” vừa có “cách gửi”, đừng nhân chúng ra thành một rừng class — tách làm hai danh sách rồi bắc cầu.
Dấu hiệu bạn đang cần nó
- Một tính năng có hai trục thay đổi độc lập (ví dụ: nội dung thông báo × kênh gửi).
- Bạn thấy tên class kiểu
OrderEmail,OrderSMS,PromoEmail,PromoSMS… đang nhân nhau ra nhiều. - Thêm một kênh mới là phải viết lại cho mọi loại nội dung.
Hình dung nhanh: Ngoài đời như thực đơn nhà hàng: món ăn (một trục) và cách chế biến — chiên/hấp/nướng (trục kia); bạn không viết ra “gà-chiên”, “gà-hấp”… mà ghép tự do. Trong web: gửi thông báo — nội dung (đơn hàng / khuyến mãi / cảnh báo) và kênh (email / SMS / push) là hai trục riêng.
Ẩn dụ (Design Patterns for Humans): Web có nhiều trang và nhiều theme. Thay vì nhân bản mỗi trang cho từng theme, bạn tách “theme” ra riêng rồi nạp theo lựa chọn người dùng — nội dung và giao diện đổi độc lập.
Khi CHƯA có pattern
// 3 nội dung × 3 kênh = 9 class, thêm 1 kênh là +3 class nữa
class OrderEmail { send() {/*...*/} }
class OrderSMS { send() {/*...*/} }
class OrderPush { send() {/*...*/} }
class PromoEmail { send() {/*...*/} }
class PromoSMS { send() {/*...*/} }
// ... và cứ thế nổ ra
Khi ÁP DỤNG pattern
// Trục 1: kênh gửi
interface Channel { deliver(text: string): void; }
const email: Channel = { deliver: (t) => sendMail(t) };
const sms: Channel = { deliver: (t) => sendSms(t) };
// Trục 2: nội dung — cầm theo một Channel để "bắc cầu"
class Notification {
constructor(private channel: Channel) {}
send(text: string) { this.channel.deliver(text); } // gửi qua cầu
}
// Ghép tự do, không nổ class:
new Notification(sms).send("Đơn #123 đã xác nhận");
new Notification(email).send("Giảm 20% cuối tuần");
Prompt AI mẫu: "Tính năng gửi thông báo của tôi có 2 trục độc lập: loại nội dung và kênh gửi (email/SMS/push). Hãy dùng Bridge pattern để tách 2 trục này, tránh nổ tổ hợp class, sao cho thêm một kênh mới chỉ cần thêm một class."
Đừng dùng khi: chỉ có một trục thay đổi (ví dụ mãi mãi chỉ gửi email) — lúc đó Bridge là vẽ vời thừa.
Dễ nhầm với: Strategy (nhóm Behavioral) — Strategy đổi một hành vi; Bridge tách hai trục để ghép chéo. Nhìn thấy “cái này × cái kia” là dấu hiệu của Bridge.
3. Composite — cây lồng nhau
Một câu: Khi món đồ này có thể là “một cái lẻ” hoặc “một nhóm chứa nhiều cái”, hãy cho cả hai cùng một cách dùng để khỏi phân biệt.
Dấu hiệu bạn đang cần nó
- Dữ liệu của bạn có dạng cây / lồng nhau: khối chứa khối, thư mục chứa thư mục.
- Code đầy
if (là nhóm) { lặp qua từng con } else { xử lý cái lẻ }. - Bạn muốn gọi một lệnh ở gốc và để cả cây tự lo (ví dụ
render()toàn trang).
Hình dung nhanh: Ngoài đời là thư mục máy tính — bạn “xoá” một thư mục thì mọi file bên trong đi theo, thao tác y như xoá một file lẻ. Trong web: page builder (Bricks, Gutenberg) — một Section chứa nhiều block con, mà bản thân nó cũng là một block; gọi render() ở trang là cả cây tự vẽ.
Ẩn dụ (Design Patterns for Humans): Một tổ chức gồm nhiều nhân viên; ai cũng có lương, trách nhiệm, có thể có hoặc không có cấp dưới — “lá” (nhân viên lẻ) và “cành” (quản lý có cấp dưới) được đối xử như nhau.
Khi CHƯA có pattern
// Nơi nào dùng cũng phải tự phân biệt lá / nhánh
function render(node: any): string {
if (node.type === "section") {
return node.children.map(render).join(""); // nhánh: lặp thủ công
}
return `<${node.tag}>${node.text}</${node.tag}>`; // lá
}
// Mọi hàm muốn đụng vào cây đều phải lặp lại đống if này
Khi ÁP DỤNG pattern
interface Block { render(): string; } // lá và nhánh cùng "khuôn" này
class TextBlock implements Block { // lá
constructor(private text: string) {}
render() { return `<p>${this.text}</p>`; }
}
class Section implements Block { // nhánh: cũng là Block
constructor(private children: Block[]) {}
render() { return this.children.map((c) => c.render()).join(""); }
}
// Gọi một lần ở gốc, cả cây tự vẽ — không cần biết ai là lá:
new Section([new TextBlock("Xin chào"), new Section([...])]).render();
Prompt AI mẫu: "Tôi có cấu trúc dạng cây kiểu page builder: block lẻ và section chứa nhiều block con. Hãy dùng Composite pattern để lá và nhánh cùng một interface có render(), sao cho gọi render() ở gốc là cả cây tự dựng ra."
Đừng dùng khi: dữ liệu của bạn phẳng, chỉ một tầng — ép nó thành cây chỉ làm rối thêm.
Dễ nhầm với: Decorator. Cả hai đều “lồng” nhau, nhưng Composite lồng để tạo cấu trúc nhiều-con (một chứa nhiều), còn Decorator lồng để cộng thêm tính năng lên đúng một thứ (mỗi lớp bọc đúng một cái).
4. Decorator — lớp bọc tính năng
Một câu: Thay vì nhét mọi tính năng vào một class khổng lồ, bạn bọc thêm từng lớp — như xếp hộp quà, mỗi lớp thêm một thứ.
Dấu hiệu bạn đang cần nó
- Một class đang phình to vì gánh đủ thứ tính năng (resize, watermark, nén, đổi định dạng…).
- Bạn muốn bật/tắt hoặc phối tính năng linh hoạt tuỳ trường hợp, không phải class cứng.
- Thêm một tính năng mà không muốn động vào code cũ (sợ vỡ chỗ khác).
Hình dung nhanh: Ngoài đời như ly cà phê — cà phê gốc, thêm lớp sữa, thêm lớp kem, thêm topping; mỗi lớp bọc thêm mà không đổi ly cà phê bên dưới. Trong web: xử lý ảnh upload — bọc lần lượt resize → watermark → nén, muốn bỏ watermark thì gỡ đúng lớp đó ra.
Ẩn dụ (Design Patterns for Humans): Tiệm rửa xe nhiều dịch vụ: chọn 1 dịch vụ rồi cộng dồn giá từng dịch vụ thêm vào cho tới tổng cuối. Mỗi dịch vụ cộng thêm là một decorator.
Khi CHƯA có pattern
// Một class ôm hết, bật/tắt bằng cờ → càng ngày càng rối
class ImageProcessor {
process(img: Buffer, opt: { resize?: boolean; watermark?: boolean; compress?: boolean }) {
let out = img;
if (opt.resize) out = doResize(out);
if (opt.watermark) out = doWatermark(out);
if (opt.compress) out = doCompress(out);
return out; // thêm tính năng = sửa thẳng vào đây
}
}
Khi ÁP DỤNG pattern
type Transform = (img: Buffer) => Buffer; // "khuôn" chung của mọi lớp
const resize: Transform = (img) => doResize(img);
const watermark: Transform = (img) => doWatermark(img);
const compress: Transform = (img) => doCompress(img);
// Bọc lớp bằng cách lồng hàm — thêm/bớt/đổi thứ tự thoải mái:
function pipeline(...steps: Transform[]): Transform {
return (img) => steps.reduce((acc, step) => step(acc), img);
}
const process = pipeline(resize, watermark, compress);
process(anhGoc); // = compress(watermark(resize(anhGoc)))
Prompt AI mẫu: "Tôi đang có một class xử lý ảnh ôm quá nhiều tính năng bật/tắt bằng cờ. Hãy tách thành các Decorator/lớp bọc riêng (resize, watermark, nén) cùng một interface, để tôi ghép chúng linh hoạt mà không sửa lõi."
Đừng dùng khi: chỉ có 1–2 tính năng cố định, không bao giờ đổi — cứ viết thẳng cho nhanh.
Dễ nhầm với: Composite (xem mục 3: Composite tạo cấu trúc cây, Decorator cộng tính năng lên một thứ). Và Proxy — Proxy cũng bọc, nhưng để kiểm soát lối vào chứ không phải thêm chức năng.
5. Facade — mặt tiền che bớt rối
Một câu: Giấu cả một quy trình rắc rối sau một nút bấm đơn giản — người dùng chỉ cần biết cái nút.
Dấu hiệu bạn đang cần nó
- Để làm một việc, nơi gọi phải tự điều phối 5–6 bước với 5–6 module khác nhau.
- Cùng một chuỗi bước đó bị copy đi copy lại ở nhiều chỗ.
- Bạn muốn phần UI “ngu ngơ” nhất có thể: gọi một hàm là xong.
Hình dung nhanh: Ngoài đời như nút khởi động xe — bạn nhấn một nút, bên trong bơm xăng, đánh lửa, khởi động cả loạt thứ mà bạn không cần biết. Trong web: checkout(cart) — bên trong lo kiểm kho, trừ tiền, áp mã giảm giá, tạo vận đơn, gửi email, ghi đơn; UI chỉ gọi đúng một hàm.
Ẩn dụ (Design Patterns for Humans): Bạn bật máy tính chỉ bằng cách “nhấn nút nguồn” — bên trong máy làm cả tá việc, nhưng bạn chỉ thấy một nút đơn giản. Cái nút đó là facade.
Khi CHƯA có pattern
// UI phải tự biết và gọi đúng thứ tự 6 hệ thống
async function onCheckoutClick(cart: Cart) {
await inventory.reserve(cart.items);
const pay = await payment.charge(cart.total);
await coupon.apply(cart.code);
const ship = await shipping.createLabel(cart.address);
await mailer.sendConfirm(cart.email, pay, ship);
await orders.save(cart, pay, ship); // lỡ thiếu/sai thứ tự là hỏng
}
Khi ÁP DỤNG pattern
// Một "mặt tiền" gói hết 6 bước lại
async function checkout(cart: Cart) {
await inventory.reserve(cart.items);
const pay = await payment.charge(cart.total);
await coupon.apply(cart.code);
const ship = await shipping.createLabel(cart.address);
await mailer.sendConfirm(cart.email, pay, ship);
return orders.save(cart, pay, ship);
}
// UI gọn hết mức, không cần biết bên trong:
async function onCheckoutClick(cart: Cart) {
await checkout(cart);
}
Prompt AI mẫu: "Phần thanh toán của tôi đang bắt UI tự gọi 6 bước (kho, trừ tiền, mã giảm giá, vận chuyển, email, lưu đơn). Hãy gom vào một Facade checkout(cart) để UI chỉ gọi một hàm, còn thứ tự và chi tiết giấu bên trong."
Đừng dùng khi: việc chỉ có một bước đơn giản — bọc lại thành facade chỉ là thêm một tầng vô nghĩa.
Dễ nhầm với: Adapter — Adapter dịch một thứ sang khuôn khác (một-đổi-một); Facade gom nhiều thứ lại thành một cửa cho gọn. Facade không đổi khuôn, chỉ đơn giản hoá.
6. Flyweight — dùng chung phần nặng
Một câu: Khi hàng chục nghìn thứ giống nhau, đừng để mỗi cái ôm một bản sao — tách phần chung ra dùng chung, mỗi cái chỉ giữ phần riêng.
Dấu hiệu bạn đang cần nó
- Bạn tạo ra rất nhiều object (hàng nghìn, chục nghìn) mà phần lớn dữ liệu giống hệt nhau.
- Trang ngốn RAM / giật lag khi số lượng tăng lên.
- Mỗi object chỉ thực sự khác nhau ở vài giá trị nhỏ (ví dụ: toạ độ), còn lại (ảnh, màu, style) thì trùng.
Hình dung nhanh: Ngoài đời như quân cờ vua — có 8 con tốt trắng, nhưng bạn không cần 8 “bản thiết kế con tốt”, chỉ cần một hình mẫu dùng chung, mỗi con chỉ khác ô đang đứng. Trong web: bản đồ có 30.000 marker cùng một icon — chia sẻ phần icon, mỗi marker chỉ giữ toạ độ.
Ẩn dụ (Design Patterns for Humans): Quán trà pha sẵn một ấm to dùng chung cho nhiều khách để tiết kiệm gas, thay vì đun riêng từng cốc. Chia sẻ phần dùng chung đó chính là flyweight.
Khi CHƯA có pattern
// Mỗi marker ôm nguyên một bản icon + style → 30.000 bản sao
class Marker {
constructor(
public x: number,
public y: number,
public iconData: Buffer, // 40KB, TRÙNG ở mọi marker
public color: string,
public style: object,
) {}
}
const markers = points.map((p) => new Marker(p.x, p.y, loadIcon(), "#F5B301", {...}));
Khi ÁP DỤNG pattern
// Phần chung (nặng) tách riêng, tạo MỘT lần
const pinIcon = { iconData: loadIcon(), color: "#F5B301", style: {...} };
// Mỗi marker chỉ giữ phần riêng (nhẹ) + trỏ tới phần chung
class Marker {
constructor(
public x: number,
public y: number,
public shared = pinIcon, // dùng chung, không copy
) {}
}
const markers = points.map((p) => new Marker(p.x, p.y)); // nhẹ RAM hẳn
Prompt AI mẫu: "Bản đồ của tôi render mấy chục nghìn marker và bị ngốn RAM vì mỗi marker giữ trùng phần icon/style. Hãy áp dụng Flyweight: tách phần dữ liệu chung ra dùng chung, mỗi marker chỉ giữ toạ độ riêng."
Đừng dùng khi: bạn chỉ có ít object, hoặc dữ liệu mỗi cái thực sự khác nhau — tối ưu kiểu này chỉ làm code khó đọc mà chẳng lợi gì.
Dễ nhầm với: Singleton (Creational) — Singleton là “chỉ có đúng một” của cả hệ thống; Flyweight là “nhiều object nhưng chia nhau xài chung một phần dữ liệu”.
7. Proxy — người gác cổng
Một câu: Đặt một người trung gian đứng trước object thật để kiểm soát lối vào — người gọi cứ tưởng đang nói chuyện thẳng với object thật.
Dấu hiệu bạn đang cần nó
- Bạn muốn thêm cache / kiểm tra quyền / hoãn tải (lazy-load) trước khi chạm tới object thật.
- Bạn không muốn sửa nơi gọi hay bản thân object thật — chỉ chèn một lớp ở giữa.
- Object thật đắt đỏ (gọi API tốn tiền, query DB nặng) và bạn muốn “cùng lắm mới đụng tới”.
Hình dung nhanh: Ngoài đời như lễ tân / bảo vệ — khách cứ hỏi lễ tân như hỏi thẳng giám đốc, nhưng lễ tân lọc trước: ai được vào, câu nào trả lời sẵn được thì trả luôn. Trong web: một lớp bọc quanh API — có cache thì trả ngay, chưa đăng nhập thì chặn, chưa cần thì chưa tải.
Ẩn dụ (Design Patterns for Humans): Cửa mở bằng thẻ từ — cửa vẫn chỉ làm mỗi việc “mở”, nhưng có một lớp kiểm soát (thẻ/quyền) gắn lên trên. Lớp đó là proxy.
Khi CHƯA có pattern
// Rải logic cache/quyền khắp nơi gọi → lặp và dễ quên
async function getUser(id: number) {
return api.fetchUser(id); // luôn gọi API, không cache, không kiểm quyền
}
// Chỗ nào cần cache lại tự viết lại cache; chỗ nào cần quyền lại tự check...
Khi ÁP DỤNG pattern
interface UserApi { getUser(id: number): Promise<User>; }
// Proxy có CÙNG hình dạng object thật, chèn kiểm soát vào giữa
class CachedUserApi implements UserApi {
private cache = new Map<number, User>();
constructor(private real: UserApi) {}
async getUser(id: number) {
if (this.cache.has(id)) return this.cache.get(id)!; // chặn, trả luôn
const user = await this.real.getUser(id); // mới gọi thật
this.cache.set(id, user);
return user;
}
}
// Nơi gọi không đổi một dòng nào:
const api: UserApi = new CachedUserApi(realApi);
await api.getUser(1);
Prompt AI mẫu: "Tôi muốn thêm cache (và kiểm tra quyền) cho lớp gọi API mà không sửa nơi gọi. Hãy tạo một Proxy cùng interface với API thật, xử lý cache/quyền ở giữa rồi mới ủy quyền xuống object thật."
Đừng dùng khi: bạn chẳng cần kiểm soát gì thêm — thêm proxy chỉ là một tầng trung gian vô ích.
Dễ nhầm với: Decorator — cả hai đều bọc và giữ nguyên interface, nhưng Decorator thêm chức năng mới, còn Proxy kiểm soát/điều tiết lối vào (cache, quyền, hoãn tải) chứ không đổi việc object làm. Adapter thì đổi interface; Proxy giữ nguyên interface.
Nhóm Behavioral — Các object nói chuyện & phối hợp thế nào
Nhóm Behavioral trả lời một câu hỏi duy nhất: khi trong app có nhiều mảnh việc, chúng giao tiếp và chia việc cho nhau ra sao cho gọn? Đây là nhóm bạn gặp nhiều nhất khi code lớn dần: một hàm phình to, if/else mọc khắp nơi, các component gọi chéo rối như mì. Mỗi pattern dưới đây là một cách “dàn xếp” lại luồng chạy cho dễ đọc, dễ sửa, dễ thêm.
Đọc theo kiểu tra bảng: thấy dấu hiệu nào giống code của mình thì nhảy vào đúng pattern đó, xem cách prompt AI ở cuối mỗi mục.
| Pattern | Dùng khi | |
|---|---|---|
| Chain of Responsibility | Một hàm dài ôm hết mọi bước kiểm tra/xử lý nối tiếp | ↓ |
| Command | Cần undo/redo, xếp hàng, hoặc ghi lại thao tác | ↓ |
| Iterator | Nhiều nguồn dữ liệu, mỗi nguồn duyệt một kiểu | ↓ |
| Mediator | Nhiều component gọi chéo nhau loạn xạ | ↓ |
| Memento | Cần chụp lại trạng thái để khôi phục (undo layout) | ↓ |
| Observer | Một sự kiện xảy ra, nhiều nơi phải cùng phản ứng | ↓ |
| Visitor | Thêm thao tác mới mà không muốn sửa mọi class | ↓ |
| Strategy | Cùng một việc nhưng nhiều cách làm, dev chọn cách | ↓ |
| State | Hành vi đổi theo trạng thái, object tự chuyển | ↓ |
| Template Method | Quy trình khung cố định, vài bước cho tùy biến | ↓ |
1. Chain of Responsibility — chuỗi trách nhiệm
Một câu: Chuyền yêu cầu qua một hàng người xử lý — ai lo được thì lo, không thì đẩy cho người kế tiếp.
Dấu hiệu bạn đang cần nó
- Một hàm dài ngoằng ôm hết mọi bước kiểm tra, mỗi lần thêm quy tắc mới lại phải chen vào giữa.
- Bạn muốn bật/tắt hoặc đổi thứ tự các bước mà không đụng vào những bước khác.
- Nhiều bước xử lý nối tiếp, bước trước qua thì mới tới bước sau.
Hình dung nhanh: Hồ sơ đi qua nhiều cửa — bảo vệ, lễ tân, rồi quản lý; mỗi cửa xử lý phần của mình rồi chuyển tiếp. Trong web: pipeline validate form đăng ký — kiểm tra ô trống → định dạng email → số điện thoại → lọc spam, mỗi bước là một mắt xích.
Ẩn dụ (Design Patterns for Humans): Bạn có 3 ví A (100$), B (300$), C (1000$), ưu tiên A→B→C. Mua món 210$: thử A không đủ → chuyển sang B đủ tiền thì trừ và dừng chuỗi. Mỗi ví là một mắt xích.
Khi CHƯA có pattern
function validate(form: Form): string | null {
if (!form.name) return "Thiếu tên";
if (!/^\S+@\S+$/.test(form.email)) return "Email sai";
if (!/^0\d{9}$/.test(form.phone)) return "SĐT sai";
if (isSpam(form.message)) return "Nghi spam";
// ...mỗi quy tắc mới lại chèn thêm một if vào đây
return null;
}
Khi ÁP DỤNG pattern
type Check = (f: Form) => string | null;
const notEmpty: Check = f => (f.name ? null : "Thiếu tên");
const validEmail: Check = f => (/^\S+@\S+$/.test(f.email) ? null : "Email sai");
const validPhone: Check = f => (/^0\d{9}$/.test(f.phone) ? null : "SĐT sai");
const chain: Check[] = [notEmpty, validEmail, validPhone]; // thêm/bớt/đổi chỗ tự do
function validate(f: Form): string | null {
for (const check of chain) {
const err = check(f);
if (err) return err; // dừng ngay khi một mắt xích bắt lỗi
}
return null;
}
Prompt AI mẫu: "Tách hàm validate dài này thành chuỗi các handler nhỏ (Chain of Responsibility), mỗi quy tắc là một hàm riêng, chạy nối tiếp và dừng ở lỗi đầu tiên."
Đừng dùng khi: Chỉ có 1–2 bước kiểm tra cố định — một cái if thẳng còn dễ đọc hơn cả chuỗi.
Dễ nhầm với: Decorator (Structural) cũng “bọc nối tiếp”, nhưng Decorator cộng dồn thêm khả năng cho mọi lớp; Chain thì mỗi mắt xích có quyền chặn lại và dừng cả chuỗi.
2. Command — đóng gói hành động
Một câu: Biến mỗi hành động thành một “phiếu lệnh” cầm được trên tay — để thực thi, hoàn tác, xếp hàng hay ghi lại lúc nào cũng được.
Dấu hiệu bạn đang cần nó
- Nút bấm trên toolbar gọi thẳng vào logic, nên không tài nào làm undo/redo được.
- Bạn cần lịch sử thao tác, hàng đợi tác vụ, hoặc lặp lại một hành động.
- Cùng một hành động cần gọi từ nhiều nơi (nút, phím tắt, menu chuột phải).
Hình dung nhanh: Order trong nhà hàng — bồi bàn ghi phiếu, bếp cứ theo phiếu mà làm, có thể xếp phiếu thành hàng đợi hoặc hủy phiếu. Trong web: page builder / editor cần undo–redo cho từng thao tác kéo, xóa, đổi màu.
Ẩn dụ (Design Patterns for Humans): Ở nhà hàng, bạn (client) nhờ bồi bàn (invoker) mang món; bồi bàn chuyển yêu cầu (command) cho đầu bếp (receiver) là người biết nấu. Yêu cầu được đóng gói thành một “đơn”.
Khi CHƯA có pattern
button.onClick = () => {
canvas.addBox(x, y); // gọi thẳng, không lưu lại gì
};
// Muốn Ctrl+Z? Không có cách nào lần ngược lại việc vừa làm.
Khi ÁP DỤNG pattern
interface Command { do(): void; undo(): void; }
class AddBox implements Command {
constructor(private canvas: Canvas, private x: number, private y: number) {}
do() { this.canvas.addBox(this.x, this.y); }
undo() { this.canvas.removeLastBox(); }
}
const history: Command[] = [];
function run(cmd: Command) { cmd.do(); history.push(cmd); }
function undo() { history.pop()?.undo(); } // Ctrl+Z hoạt động!
Prompt AI mẫu: "Đóng gói mỗi thao tác của editor thành một Command object có do()/undo(), lưu vào history stack để hỗ trợ undo/redo bằng Ctrl+Z."
Đừng dùng khi: Hành động chạy một phát rồi thôi, không bao giờ cần undo, log hay xếp hàng — thêm Command chỉ tổ rườm rà.
Dễ nhầm với: Memento — Command lưu việc cần làm (và cách làm ngược lại); Memento lưu ảnh chụp trạng thái. Undo bằng Command là “làm ngược thao tác”, undo bằng Memento là “khôi phục nguyên trạng thái cũ”.
3. Iterator — duyệt thống nhất
Một câu: Cho mọi loại danh sách một cách duyệt giống nhau, giấu đi chuyện bên trong nó là mảng, cây hay gọi API.
Dấu hiệu bạn đang cần nó
- Mỗi nguồn dữ liệu bạn phải viết một kiểu vòng lặp riêng: mảng thì
for, API thìfetchtừng trang, database thì cursor. - Code gọi ra ngoài phải biết quá rõ dữ liệu được lưu thế nào bên trong.
- Bạn muốn duyệt qua thứ gì đó mà không quan tâm nó chứa bao nhiêu, lấy từ đâu.
Hình dung nhanh: Cái remote TV — bấm “next” là qua kênh tiếp theo, bạn chẳng cần biết kênh lưu ở đâu. Trong web: một trang hiển thị sản phẩm, dù dữ liệu tới từ mảng cứng, từ API phân trang hay từ cache, phần render chỉ cần for (const item of source).
Ẩn dụ (Design Patterns for Humans): Radio cũ: bạn bấm next/previous để chuyển kênh mà không cần biết kênh lưu bên trong thế nào — nút đó cho bạn một cách “duyệt” thống nhất.
Khi CHƯA có pattern
// Mỗi nguồn một kiểu duyệt, chỗ dùng phải phân biệt từng loại
if (source.type === "array") {
for (const p of source.items) render(p);
} else if (source.type === "api") {
let page = 1, res;
do { res = await fetchPage(page++); res.data.forEach(render); }
while (res.hasNext);
}
Khi ÁP DỤNG pattern
// Mọi nguồn đều trả về một async iterator chung
async function* productStream(source: Source) {
if (source.kind === "array") yield* source.items;
else {
let page = 1, res;
do { res = await fetchPage(page++); yield* res.data; }
while (res.hasNext);
}
}
for await (const product of productStream(source)) render(product); // 1 kiểu duyệt duy nhất
Prompt AI mẫu: "Cho các nguồn dữ liệu (mảng local, API phân trang) một async generator chung để nơi dùng chỉ cần for-await-of, không phải phân biệt nguồn."
Đừng dùng khi: Chỉ có đúng một mảng đơn giản — for...of gốc của JS đã là iterator rồi, khỏi bọc thêm.
Dễ nhầm với: Không mấy khi nhầm. Chỉ cần nhớ: Iterator lo chuyện duyệt qua, không quyết định làm gì với từng phần tử (việc đó để Strategy/Visitor).
4. Mediator — người điều phối trung tâm
Một câu: Thay vì để các thành phần gọi thẳng vào nhau chằng chịt, cho tất cả nói chuyện qua một “tổng đài” trung tâm.
Dấu hiệu bạn đang cần nó
- Năm component gọi chéo nhau: đổi cái A phải nhớ cập nhật B, C, D — sửa một chỗ vỡ ba chỗ.
- Logic phối hợp giữa các phần bị rải rác, không biết chỗ nào “cầm trịch”.
- Bạn muốn thêm/bớt một component mà không phải đi sửa hết những component còn lại.
Hình dung nhanh: Đài kiểm soát không lưu — máy bay không tự gọi nhau, tất cả nghe lệnh từ một đài trung tâm. Trong web: form thanh toán phức tạp, chọn “vùng giao hàng” phải cập nhật phí ship, ẩn/hiện ô địa chỉ, đổi phương thức thanh toán — cho một mediator điều phối thay vì các ô tự gọi nhau.
Ẩn dụ (Design Patterns for Humans): Khi bạn gọi điện thoại, nhà mạng đứng giữa chuyển cuộc gọi thay vì hai người nối thẳng dây với nhau. Nhà mạng ở đây là mediator (giống phòng chat điều phối tin nhắn).
Khi CHƯA có pattern
regionSelect.onChange = () => {
shippingField.update(); // mỗi component phải biết
addressField.toggle(); // tên và cách gọi của
paymentField.refresh(); // tất cả component khác
summaryBox.recalc(); // → rối như mạng nhện
};
Khi ÁP DỤNG pattern
class CheckoutMediator {
notify(sender: string, event: string) {
if (sender === "region" && event === "change") {
this.shipping.update();
this.address.toggle();
this.payment.refresh();
this.summary.recalc();
}
}
}
// Mỗi component chỉ báo cho mediator, không cần biết các component khác
regionSelect.onChange = () => mediator.notify("region", "change");
Prompt AI mẫu: "Các field trong form checkout đang gọi chéo nhau. Gom logic phối hợp vào một CheckoutMediator, mỗi field chỉ notify() cho mediator, không tham chiếu trực tiếp field khác."
Đừng dùng khi: Chỉ có 2 component nói chuyện với nhau — thêm tổng đài ở giữa chỉ làm dài dòng.
Dễ nhầm với: Observer — Observer là phát tin một chiều, quảng bá: nguồn phát không biết ai nghe. Mediator là điều phối hai chiều, có kịch bản: nó biết rõ từng thành phần và quyết định ai làm gì khi có sự kiện.
5. Memento — ảnh chụp trạng thái
Một câu: Chụp lại toàn bộ trạng thái tại một thời điểm, cất đi, để lỡ hỏng thì dán lại nguyên như cũ.
Dấu hiệu bạn đang cần nó
- Người dùng kéo–thả sai một cái là mất luôn layout cũ, không lấy lại được.
- Bạn cần “checkpoint” để quay về: undo, bản nháp tự lưu, hoàn tác nhiều bước.
- Trạng thái phức tạp và bạn không muốn viết tay từng thao tác ngược lại.
Hình dung nhanh: Save game trước con boss — chết thì load lại đúng chỗ đã lưu. Trong web: page builder lưu snapshot layout trước mỗi lần kéo–thả, sai thì “Hoàn tác” trả về nguyên bố cục lúc trước.
Ẩn dụ (Design Patterns for Humans): Máy tính bỏ túi lưu phép tính cuối vào bộ nhớ để bạn bấm nút khôi phục lại kết quả trước. Ảnh chụp trạng thái đó là memento.
Khi CHƯA có pattern
function moveBlock(id: string, to: Position) {
layout.blocks[id].pos = to; // ghi đè thẳng
}
// Kéo nhầm → vị trí cũ biến mất, không có đường lùi.
Khi ÁP DỤNG pattern
type Snapshot = string; // ảnh chụp đông cứng của layout
const save = (layout: Layout): Snapshot => JSON.stringify(layout);
const restore = (snap: Snapshot): Layout => JSON.parse(snap);
const undoStack: Snapshot[] = [];
function moveBlock(id: string, to: Position) {
undoStack.push(save(layout)); // chụp trước khi đổi
layout.blocks[id].pos = to;
}
function undo() {
const snap = undoStack.pop();
if (snap) layout = restore(snap); // dán lại nguyên trạng thái cũ
}
Prompt AI mẫu: "Thêm undo cho page builder bằng Memento: chụp snapshot layout (JSON) trước mỗi thao tác kéo-thả, lưu vào stack, undo thì restore snapshot gần nhất."
Đừng dùng khi: Trạng thái quá to mà thao tác lại dày đặc — chụp toàn bộ liên tục sẽ tốn bộ nhớ; lúc đó cân nhắc Command (lưu thao tác) thay vì snapshot.
Dễ nhầm với: Command — xem mục Command: Command undo bằng cách làm ngược thao tác, Memento undo bằng cách khôi phục ảnh chụp.
6. Observer — phát tin & nhiều nơi lắng nghe
Một câu: Một nơi phát ra sự kiện, nhiều nơi cùng lắng nghe và tự phản ứng — không nơi nào cần biết mặt nơi nào.
Dấu hiệu bạn đang cần nó
- Một chỗ trong code phải “gọi cứng” từng bên liên quan mỗi khi có việc xảy ra.
- Danh sách “những việc cần làm sau khi X” cứ dài thêm, và bạn phải sửa đúng chỗ X mỗi lần.
- Các phản ứng vốn rời rạc, độc lập — chẳng bên nào phụ thuộc kết quả của bên kia.
Hình dung nhanh: Kênh YouTube đăng video mới → tất cả người đăng ký nhận thông báo, kênh chẳng cần biết từng ai. Trong web: đơn hàng chuyển sang paid → trừ kho + gửi email xác nhận + ghi log analytics, ba việc rời rạc cùng chạy nhờ một sự kiện phát ra.
Ẩn dụ (Design Patterns for Humans): Người tìm việc đăng ký một trang tuyển dụng và được báo mỗi khi có việc phù hợp — đăng ký một lần rồi tự nhận thông báo.
Khi CHƯA có pattern
function markPaid(order: Order) {
order.status = "paid";
inventory.reduce(order.items); // payment phải gọi cứng
email.sendReceipt(order); // từng bên một, và mỗi khi
analytics.track("purchase"); // thêm việc lại phải sửa vào đây
}
Khi ÁP DỤNG pattern
type Listener = (order: Order) => void;
const onPaid: Listener[] = [];
const subscribe = (fn: Listener) => onPaid.push(fn);
// Đăng ký rời rạc, ở nơi khác, lúc khác:
subscribe(o => inventory.reduce(o.items));
subscribe(o => email.sendReceipt(o));
subscribe(o => analytics.track("purchase"));
function markPaid(order: Order) {
order.status = "paid";
onPaid.forEach(fn => fn(order)); // chỉ phát 1 event, ai quan tâm tự nhận
}
Prompt AI mẫu: "Khi order chuyển sang paid, thay vì gọi cứng từng service, phát một sự kiện 'order.paid' và cho các listener (trừ kho, gửi email, analytics) tự đăng ký lắng nghe (Observer)."
Đừng dùng khi: Chỉ có đúng một phản ứng cố định — gọi thẳng còn rõ ràng, dễ lần theo hơn là đi tìm ai đang lắng nghe.
Dễ nhầm với: Mediator — xem mục Mediator. Nhớ ngắn gọn: Observer = loa phát thanh (một phát, nhiều tai nghe, không điều phối); Mediator = tổng đài (biết mọi bên, điều phối hai chiều theo kịch bản).
7. Visitor — thêm thao tác không sửa class
Một câu: Muốn thêm một việc mới làm trên cả tập object khác loại, mà không phải mở từng class ra sửa.
Dấu hiệu bạn đang cần nó
- Bạn có nhiều loại object (đoạn văn, ảnh, bảng…) và cứ thêm một thao tác mới (export PDF, kiểm toán, đếm chữ) là phải sửa method vào từng class.
- Các thao tác đó không liên quan đến bản chất object, chỉ là “duyệt qua và làm gì đó”.
- Bạn muốn gom logic của một thao tác vào một chỗ, thay vì rải khắp mọi class.
Hình dung nhanh: Thanh tra thuế đi tới từng loại cửa hàng, mỗi nơi làm một kiểu tính nhưng cùng một “chuyến thăm”. Trong web: một tài liệu gồm heading, paragraph, image — cần export PDF, rồi lại cần đếm chữ, rồi lại cần audit SEO — mỗi việc là một “visitor” đi qua từng phần tử.
Ẩn dụ (Design Patterns for Humans): Khách du lịch Dubai chỉ cần visa để vào; sau đó tự đi thăm bất cứ đâu mà không phải xin phép từng nơi. Visitor cho phép “thêm thao tác thăm” mà không phải sửa từng địa điểm.
Khi CHƯA có pattern
class Heading { text = ""; toPDF() {/*...*/} countWords() {/*...*/} }
class Paragraph { text = ""; toPDF() {/*...*/} countWords() {/*...*/} }
class ImageNode { src = ""; toPDF() {/*...*/} countWords() {/*...*/} }
// Thêm "audit SEO"? Phải mở CẢ BA class ra thêm method mới.
Khi ÁP DỤNG pattern
interface Visitor { heading(h: Heading): void; paragraph(p: Paragraph): void; }
class WordCounter implements Visitor {
total = 0;
heading(h: Heading) { this.total += h.text.split(" ").length; }
paragraph(p: Paragraph) { this.total += p.text.split(" ").length; }
}
// Muốn thêm thao tác mới (PdfExporter, SeoAuditor)? Viết 1 class visitor mới,
// KHÔNG đụng vào Heading/Paragraph/ImageNode.
function walk(nodes: Node[], v: Visitor) { nodes.forEach(n => n.accept(v)); }
Prompt AI mẫu: "Các node tài liệu (heading, paragraph, image) đang phình method mỗi khi thêm thao tác. Áp dụng Visitor: mỗi thao tác (đếm chữ, export PDF, audit) là một visitor riêng, node chỉ cần accept(visitor)."
Đừng dùng khi: Tập class thay đổi liên tục (thêm loại node mới hoài) — vì mỗi loại mới lại buộc sửa mọi visitor; Visitor chỉ đáng khi tập class ổn định còn thao tác thì hay thêm.
Dễ nhầm với: Strategy — Strategy đổi một cách làm cho một việc; Visitor gom nhiều cách xử lý theo từng loại object vào một thao tác duyệt.
8. Strategy — đổi thuật toán nóng
Một câu: Cùng một việc nhưng có nhiều cách làm — tách mỗi cách thành một hộp rời, và dev chọn đổi hộp nào lúc chạy.
Dấu hiệu bạn đang cần nó
- Một hàm đầy
if/else(hoặcswitch) chỉ để chọn cách tính cho cùng một việc. - Bạn muốn thay/thêm một cách làm mà không đụng vào những cách còn lại.
- Cùng một đầu vào, đầu ra, chỉ khác “công thức” bên trong.
Hình dung nhanh: App bản đồ cho chọn phương tiện — đi bộ, ô tô, xe máy — cùng tính đường đi nhưng mỗi kiểu một thuật toán. Trong web: tính phí ship theo vùng (nội thành / ngoại thành / tỉnh), checkout chỉ gọi calculateShipping() chung, còn công thức nào là do dev gắn vào.
Ẩn dụ (Design Patterns for Humans): Ban đầu dùng bubble sort; data lớn lên thì chậm nên đổi sang quick sort. Data nhỏ dùng bubble, data lớn dùng quick — DEV chủ động chọn thuật toán theo tình huống.
Khi CHƯA có pattern
function calculateShipping(order: Order): number {
if (order.region === "inner") return 15000;
else if (order.region === "outer") return 25000;
else if (order.region === "province") return order.weight * 5000 + 20000;
// thêm vùng mới → lại chèn một nhánh else if
return 0;
}
Khi ÁP DỤNG pattern
type ShippingStrategy = (order: Order) => number;
const strategies: Record<string, ShippingStrategy> = {
inner: () => 15000,
outer: () => 25000,
province: o => o.weight * 5000 + 20000, // thêm vùng = thêm 1 dòng
};
function calculateShipping(order: Order): number {
const strategy = strategies[order.region] ?? strategies.inner; // dev chọn công thức
return strategy(order);
}
Prompt AI mẫu: "Hàm calculateShipping đang đầy if/else theo vùng. Tách mỗi cách tính thành một strategy riêng, chọn strategy theo order.region để dễ thêm vùng mới mà không sửa code cũ."
Đừng dùng khi: Chỉ có 2 nhánh và sẽ không bao giờ thêm — một if đơn giản còn dễ hiểu hơn cả bộ strategy.
Dễ nhầm với: State — điểm khác cốt lõi: với Strategy thì dev chủ động chọn cách làm (chọn công thức ship). Với State thì object tự đổi hành vi theo trạng thái của chính nó, và tự chuyển sang trạng thái kế. Xem State.
9. State — hành vi theo trạng thái
Một câu: Object tự đổi cách cư xử tùy trạng thái hiện tại của chính nó — thay vì rải
if statuskhắp app.
Dấu hiệu bạn đang cần nó
if (status === ...)mọc ở khắp nơi, mỗi khi thêm trạng thái mới lại phải soát lại từng chỗ.- Cùng một hành động nhưng cho kết quả khác nhau tùy trạng thái (bấm “Hủy” lúc
pendingkhác lúcshipped). - Có luật chuyển tiếp rõ ràng: từ trạng thái này chỉ được sang vài trạng thái kia.
Hình dung nhanh: Đèn giao thông tự chuyển xanh → vàng → đỏ, mỗi màu “cho phép” hành vi khác nhau. Trong web: vòng đời đơn hàng pending → paid → shipped → done, mỗi trạng thái quyết định được làm gì (còn hủy được không, giao được không) và tự chuyển sang trạng thái kế.
Ẩn dụ (Design Patterns for Humans): Trong app vẽ, bạn chọn cọ; cọ tự đổi hành vi theo màu đang chọn — đỏ thì vẽ đỏ, xanh thì vẽ xanh. OBJECT tự đổi hành vi theo trạng thái của nó.
Khi CHƯA có pattern
function cancel(order: Order) {
if (order.status === "pending") order.status = "cancelled";
else if (order.status === "paid") { refund(order); order.status = "cancelled"; }
else if (order.status === "shipped") throw new Error("Đã giao, không hủy được");
// if status như thế này lặp lại trong ship(), refund(), pay()...
}
Khi ÁP DỤNG pattern
interface OrderState { cancel(o: Order): void; ship(o: Order): void; }
const Pending: OrderState = {
cancel: o => (o.state = Cancelled),
ship: o => { throw new Error("Chưa thanh toán"); },
};
const Paid: OrderState = {
cancel: o => { refund(o); o.state = Cancelled; },
ship: o => (o.state = Shipped), // object tự chuyển trạng thái
};
const Shipped: OrderState = { cancel: () => { throw new Error("Đã giao"); }, ship: () => {} };
const Cancelled: OrderState = { cancel: () => {}, ship: () => {} };
// order.state.cancel(order) — hành vi tự đổi theo state, không còn if rải rác
Prompt AI mẫu: "Vòng đời đơn hàng đang rải if status khắp nơi. Mô hình hóa bằng State: mỗi trạng thái (pending, paid, shipped, cancelled) là một object định nghĩa hành vi và luật chuyển tiếp của nó."
Đừng dùng khi: Chỉ có 2 trạng thái đơn giản (bật/tắt) — một biến boolean là đủ, dựng cả máy trạng thái là thừa.
Dễ nhầm với: Strategy — cấu trúc code trông giống nhau (đều là các object thay thế được), nhưng khác ở “ai điều khiển”: Strategy do dev chọn và thường không tự đổi; State thì object tự chuyển giữa các trạng thái theo luật. Xem Strategy.
10. Template Method — khung quy trình, chừa chỗ trống
Một câu: Định sẵn khung các bước của một quy trình chung, chừa vài ô trống cho từng biến thể tự điền.
Dấu hiệu bạn đang cần nó
- Nhiều quy trình na ná nhau, chỉ khác một hai bước ở giữa, nhưng bạn đang copy–paste cả khung.
- Bạn muốn ép mọi biến thể chạy đúng thứ tự các bước, không cho chúng tự bịa lại trình tự.
- Phần “xương sống” giống hệt nhau, chỉ phần “ruột” là mỗi nơi một khác.
Hình dung nhanh: Công thức pha đồ uống — đun nước, cho nguyên liệu, rót ra, thêm topping; trà hay cà phê chỉ khác bước “cho nguyên liệu”. Trong web: xuất báo cáo — luôn là lấy dữ liệu → định dạng → đóng gói → trả file, chỉ bước “định dạng” là khác giữa PDF, Excel, CSV.
Ẩn dụ (Design Patterns for Humans): Xây nhà luôn theo khung bước cố định: làm móng → xây tường → lợp mái → thêm tầng. Khung không đổi, còn chi tiết mỗi bước thì tuỳ biến được.
Khi CHƯA có pattern
function exportPDF(data: Row[]) { const d = fetchAndClean(data); /*format PDF*/ save(); log(); }
function exportExcel(data: Row[]) { const d = fetchAndClean(data); /*format Excel*/ save(); log(); }
function exportCSV(data: Row[]) { const d = fetchAndClean(data); /*format CSV*/ save(); log(); }
// Khung (fetch → save → log) bị lặp lại 3 lần; sửa bước save() phải sửa cả 3.
Khi ÁP DỤNG pattern
abstract class Exporter {
export(data: Row[]) { // khung cố định, quy định thứ tự các bước
const clean = this.prepare(data);
const file = this.format(clean); // ← chỗ trống cho subclass
this.save(file);
this.log();
}
private prepare(d: Row[]) { return d.filter(Boolean); }
protected abstract format(d: Row[]): Blob; // mỗi biến thể tự điền bước này
private save(f: Blob) {/*...*/}
private log() {/*...*/}
}
class PdfExporter extends Exporter { protected format(d: Row[]) { return toPdf(d); } }
class CsvExporter extends Exporter { protected format(d: Row[]) { return toCsv(d); } }
Prompt AI mẫu: "Ba hàm export (PDF/Excel/CSV) đang lặp cùng khung. Rút thành Template Method: một lớp Exporter giữ khung export(), chừa bước format() abstract cho subclass tự cài."
Đừng dùng khi: Các quy trình khác nhau quá nhiều, không có “khung chung” thật sự — ép chung một khuôn sẽ làm code gượng ép hơn là gọn.
Dễ nhầm với: Strategy — Template Method dùng kế thừa: khung cố định ở lớp cha, subclass điền bước còn trống. Strategy dùng lắp ghép: đổi nguyên cả một cách làm bằng cách thay object khác vào lúc chạy.
Nguồn tham khảo
- Phóng tác & Việt hoá từ “23 Design Patterns cho Vibe Coder” — arealisticdreamer.com.
- Ẩn dụ đời thực từ Design Patterns for Humans (Kamran Ahmed).
- Ví dụ TypeScript & hình minh hoạ do mình biên soạn cho bối cảnh dev web.