1. Vì sao cần phụ lục này?

Đi qua gần 30 pattern từ Creational → Async, có một rủi ro lớn: bệnh “thấy búa cái gì cũng là đinh”. Pattern là giải pháp cho một bối cảnh cụ thể; lạm dụng khiến code khó hơn là khi không có pattern.

Bài này tổng hợp các anti-pattern thường gặp khi dùng design patterns trong JS/TS hiện đại (2026), từng gặp trong code review và thực tế production.

2. Những lạm dụng thường gặp theo pattern

2.1 Singleton

Anti-pattern: Biến mọi service thành Singleton vì “chỉ cần 1 instance”.

Vấn đề:

  • Global state ẩn — gây phụ thuộc không tường minh, khó test (mock bằng gì?), khó chạy song song trong test runner.
  • Trong Node request-scoped state phải dùng AsyncLocalStorage, không phải Singleton.
  • JavaScript ES modules tự Singleton — mỗi module được import chỉ evaluate 1 lần, chia sẻ state cho mọi nơi import. Không cần class getInstance().

Nên: Tạo instance ở composition root (container DI / index.ts), inject xuống. Nếu buộc phải Singleton, thừa nhận rõ qua DI framework (Nest provider scope, tsyringe @singleton()).

2.2 Factory / Abstract Factory

Anti-pattern: Factory 1 tầng cho class đã chỉ có 1 loại, “đề phòng tương lai thêm loại khác”.

Vấn đề: YAGNI. Tầng abstraction không được dùng = gia tăng độ phức tạp không cần thiết.

Nên: Viết thẳng new X(). Chỉ thêm Factory khi đã có ≥ 2 variant và cần chọn runtime; lúc đó refactor là dễ.

2.3 Builder

Anti-pattern: Builder cho object 3 field.

Nên: Object literal hoặc named argument đã đủ. Builder chỉ thật sự đáng khi (a) ≥ 5–6 field tuỳ chọn, hoặc (b) có ràng buộc thứ tự/validation giữa các bước.

// Không cần Builder
const user = { name: "Alice", age: 30, role: "admin" };

// Nên Builder
const query = new QueryBuilder()
  .from("users")
  .where({ status: "active" })
  .orderBy("createdAt", "DESC")
  .limit(50)
  .build();

2.4 Observer / Pub-Sub

Anti-pattern 1: “Sự kiện cho mọi thứ”. Mọi thay đổi state đều thành event, trace flow thành rừng.

Nên: Event cho cross-boundary (module A không cần biết module B); trong cùng feature, function call trực tiếp rõ hơn.

Anti-pattern 2: Quên unsubscribe()memory leak.

Nên: Dùng AbortSignal unify với fetch, addEventListener; hoặc { once: true }; hoặc hook framework (React useEffect cleanup, Vue onUnmounted).

2.5 Strategy / State

Anti-pattern: Class Strategy cho 1 function 3 dòng.

Nên: Record + satisfies (xem bài Strategy), hoặc first-class function. Strategy class chỉ đáng khi có state nội bộ hoặc nhiều method.

2.6 Repository

Anti-pattern: Repository chỉ wrap ORM y chang — UserRepository.findById() gọi prisma.user.findUnique() không thêm gì.

Nên: Bỏ Repository, dùng trực tiếp ORM. Chỉ thêm Repository khi bạn thật sự cần abstraction để swap data source hoặc tách domain logic rõ rệt.

2.7 Decorator (pattern, không phải TS decorator)

Anti-pattern: Chain 4–5 decorator cho 1 component đơn giản.

Nên: Decorator là tốt để thêm cross-cutting concerns (logging, caching, auth). Nếu chain chồng lên nhau khó debug, cân nhắc Middleware Pipeline (Koa/Express style) rõ ràng hơn.

2.8 Promise / Async

Anti-pattern 1: .then().then().then() thay vì async/await.

Nên: Mặc định await, chỉ dùng .then() khi phải pipe qua hàm nhận Promise (ví dụ Promise.all(items.map(fetch))).

Anti-pattern 2: Quên xử lý cancellation, mọi fetch không bao giờ bị huỷ → race condition + resource leak.

Nên: Chuẩn forward AbortSignal xuống mọi async function. AbortSignal.timeout() cho request có hạn chót.

Anti-pattern 3: Promise.all cho thao tác có khả năng fail một phần mà vẫn muốn xử lý partial result.

Nên: Promise.allSettled hoặc Promise.any tuỳ semantic.

2.9 Reactive / RxJS

Anti-pattern: Dùng RxJS cho mọi thứ — UI state, form validation, increment counter.

Nên: RxJS mạnh ở stream complex (debounce + switchMap + retry + merge nhiều source). Với state UI đơn giản, Signals (Vue ref, Angular 17+ signals, Solid) hoặc store lib (Zustand, Pinia) dễ hiểu hơn.

2.10 Mixin

Anti-pattern: 5 mixin chain lên 1 class → mâu thuẫn tên method, khó debug thứ tự.

Nên: Ưu tiên composition over inheritance: inject dependency thay vì kế thừa behavior.

3. Anti-pattern liên quan đến JS/TS hiện đại

3.1 Dùng any thay vì unknown

any “tắt type check”, còn unknown buộc phải narrow trước khi dùng. Với catch block, từ TS 4.4 bạn nên bật useUnknownInCatchVariables.

3.2 Không dùng satisfies khi cần

// ❌ widen type — mất literal
const config: Record<string, string> = { env: "prod", region: "us-east-1" };
config.env; // string, không còn "prod" | "dev"

// ✅ giữ literal + check shape
const config = { env: "prod", region: "us-east-1" } satisfies Record<
  string,
  string
>;
config.env; // "prod"

3.3 Return Promise<void> rồi không await

Một hàm async nhưng caller quên await → lỗi silent, không được log. Bật rule ESLint @typescript-eslint/no-floating-promises.

3.4 Không chain Error.cause

Re-throw trần làm mất stack gốc:

// ❌
try {
  await step1();
} catch (e) {
  throw new Error("step1 failed");
}

// ✅
try {
  await step1();
} catch (e) {
  throw new Error("step1 failed", { cause: e });
}

3.5 Memoize bừa bãi trong React

Nhồi useMemo/useCallback/React.memo khắp nơi không profile trước. Từ React 19, React Compiler tự memo hoá — tay thường tệ hơn tự động.

Nên: Profile trước, memo sau. Với code mới React 19+, bật compiler và viết component bình thường.

4. Checklist quick-review khi thấy design pattern trong PR

Trước khi approve/viết một pattern, tự hỏi:

  1. Có ≥ 2 use case thực tế đã biết cho abstraction này chưa? Nếu 1 → không cần pattern.
  2. Có test cho logic bên trong pattern không? (Factory không test được → Factory thừa.)
  3. Pattern có giấu global state / implicit dependency không? Nếu có, đã inject rõ chưa?
  4. Pattern chain dài (Decorator × 4, Middleware × 10): đã document thứ tự và lý do chưa?
  5. Cleanup rõ ràng: subscription/resource có được huỷ khi component unmount / request kết thúc? Có forward AbortSignal không?
  6. Error propagation có dùng Error.cause để trace root cause không?
  7. Type safety có dùng satisfies, discriminated union, exhaustive check ở chỗ nào hợp không?
  8. Hiện có pattern JS native tương đương không (ví dụ new Proxy thay cho class Proxy GoF, EventTarget thay cho Subject thủ công, structuredClone thay deep-copy thủ công)?
  9. Runtime cost: pattern có tạo object/closure/proxy trong hot path không? Profile đã đo chưa?
  10. Có thể xoá bỏ pattern (inline lại) mà code vẫn đọc được không? Nếu có → cân nhắc xoá.

5. “Khi nào không cần design pattern”

Dấu hiệu bạn đang over-engineering:

  • Class có đúng một subclass, và chắc chắn không có thêm.
  • Factory chỉ gọi new X() rồi return.
  • Interface có 1 implementation (không phải cho test mock).
  • any khắp nơi trong TS → pattern không còn giữ đúng contract.
  • File dài nhưng code thực sự xử lý rất ngắn; phần lớn là “hạ tầng pattern”.

Quy tắc đơn giản: viết code thẳng trước, refactor thành pattern sau khi:

  1. Thấy trùng lặp thứ 3 (rule of three).
  2. Thấy điểm mở rộng bị đụng thường xuyên.
  3. Cần mock cho test / swap implementation runtime.

6. Tài liệu tham khảo ngắn

  • Design Patterns — Gamma, Helm, Johnson, Vlissides (Gang of Four, 1994) — nền tảng pattern cổ điển.
  • Head First Design Patterns — Freeman, Robson — học nhập môn.
  • refactoring.guru/design-patterns — tham chiếu diagram đẹp.
  • tc39.es/proposals — theo dõi JS evolution (pipeline, signals, decorators, records/tuples).
  • Patterns of Enterprise Application Architecture — Fowler — Repository, Unit of Work, DDD tactical patterns.

Kết: Design patterns là ngôn ngữ chung để nói về giải pháp, không phải công thức bắt buộc. Viết code đơn giản trước, chỉ khi bắt gặp đúng bối cảnh mới đưa pattern vào. Và luôn ưu tiên công cụ đã có sẵn trong JS/TS/runtime — phần lớn GoF pattern đã có dạng idiomatic 1 dòng trong JavaScript hiện đại.