Luyện Phỏng Vấn IT — 2000+ Câu Hỏi Phỏng Vấn IT Có Đáp Án 2026
Design Patterns
SRP quy định mỗi class/module chỉ nên có một lý do để thay đổi, tức là chỉ chịu trách nhiệm cho một chức năng duy nhất.
Ví dụ trong TypeScript:
// Vi phạm SRP — 1 class làm 3 việc
class UserService {
handleLogin() { /* ... */ }
log(msg: string) { /* ... */ }
sendEmail() { /* ... */ }
}
// Đúng SRP — tách riêng
class UserService { handleLogin() { /* ... */ } }
class Logger { log(msg: string) { /* ... */ } }
class EmailService { sendEmail() { /* ... */ } }Lợi ích: code dễ test, dễ maintain.
Dấu hiệu vi phạm: class có nhiều method không liên quan nhau hoặc file dài hàng nghìn dòng.
OCP quy định một module phải mở để mở rộng nhưng đóng để sửa đổi — thêm tính năng mới mà không cần chỉnh sửa code hiện có.
Ví dụ TypeScript:
interface DiscountStrategy {
apply(price: number): number
}
class SeasonalDiscount implements DiscountStrategy {
apply(price: number) { return price * 0.8 }
}
class LoyaltyDiscount implements DiscountStrategy {
apply(price: number) { return price * 0.9 }
}
// Thêm loại discount mới: chỉ cần tạo class mới, không sửa calculatePrice()OCP thường được triển khai qua Strategy pattern, Template Method hoặc polymorphism.
Vi phạm OCP thường biểu hiện ở switch/case hay if/else if dài dần theo thời gian.
Ba nguyên tắc cơ bản của software engineering:
- DRY (Don't Repeat Yourself): mỗi piece of knowledge chỉ nên có một đại diện duy nhất — trùng lặp logic nên được extract thành function/module tái sử dụng.
- KISS (Keep It Simple, Stupid): ưu tiên giải pháp đơn giản nhất có thể hoạt động, tránh over-engineering.
- YAGNI (You Aren't Gonna Need It): không viết code cho tính năng chưa cần ngay bây giờ.
Khi KHÔNG áp dụng DRY quá mức: đôi khi hai đoạn code trông giống nhau nhưng thuộc về hai domain khác nhau, ép DRY tạo coupling không cần thiết ('wrong abstraction' — Sandi Metz). YAGNI đặc biệt quan trọng trong startup/agile — code thừa tăng độ phức tạp và maintenance cost.
SOLID giúp viết code có khả năng bảo trì cao (maintainable), dễ mở rộng (extensible) và dễ test (testable) — ba yếu tố quyết định tuổi thọ của một dự án phần mềm.
Khi team scale up, SOLID giúp nhiều developer làm việc song song trên cùng codebase mà ít conflict hơn vì các module có ranh giới rõ ràng. SOLID cũng giảm technical debt: vi phạm SOLID thường dẫn đến 'spaghetti code' cực kỳ khó refactor về sau.
Tuy nhiên, áp dụng SOLID có trade-off: tăng số lượng file/class, có thể gây over-engineering nếu áp dụng mù quáng cho codebase nhỏ. Nguyên tắc thực tế: áp dụng SOLID khi codebase có dấu hiệu đau (khó test, khó sửa, nhiều bug regression) chứ không phải từ đầu cho mọi project.
LSP quy định rằng các object của subclass phải có thể thay thế object của superclass mà không làm hỏng tính đúng đắn của chương trình.
Ví dụ vi phạm kinh điển: class Rectangle có setWidth/setHeight, class Square extends Rectangle override cả hai để giữ tỷ lệ — khi code gọi setWidth(5) trên Square kỳ vọng height không đổi nhưng thực tế height cũng thay đổi, phá vỡ kỳ vọng. Dấu hiệu vi phạm LSP: subclass override method rồi throw exception, hoặc kiểm tra instanceof trước khi gọi method. Cách sửa: tách interface, dùng composition thay inheritance, hoặc tái thiết kế hierarchy.
ISP quy định không nên bắt client implement những interface mà nó không sử dụng — thay vì một interface lớn, hãy tách thành nhiều interface nhỏ, chuyên biệt.
Ví dụ: thay vì interface Animal { fly(): void; swim(): void; run(): void } bắt Dog phải implement fly(), ta tách thành Flyable, Swimmable, Runnable rồi class Dog implements Runnable, Swimmable. Trong TypeScript, ISP rất tự nhiên vì có thể dùng intersection types và multiple interface implementation. Dấu hiệu vi phạm: class implement interface nhưng để các method trống hoặc throw NotImplementedException. ISP đặc biệt quan trọng khi thiết kế API/SDK public — interface nhỏ giúp người dùng dễ hiểu và ít bị breaking change hơn.
DIP có hai quy tắc:
- module high-level không nên phụ thuộc module low-level, cả hai nên phụ thuộc abstraction
- abstraction không nên phụ thuộc detail, mà detail phụ thuộc abstraction
Ví dụ: UserService không nên new MySQLDatabase() trực tiếp mà nhận DatabaseInterface qua constructor — class UserService { constructor(private db: DatabaseInterface) {} }. Dependency Injection (DI) là cơ chế triển khai DIP: thay vì class tự tạo dependency, dependency được inject từ bên ngoài (qua constructor, setter, hoặc DI container như NestJS IoC).
Lợi ích: dễ swap implementation (đổi từ MySQL sang PostgreSQL không cần sửa UserService), dễ mock trong unit test. DIP là nền tảng của các framework như NestJS, Spring, Angular.
Trong React/Next.js, SRP áp dụng cho component (mỗi component một trách nhiệm), custom hook (tách logic ra khỏi UI), và service layer (tách API call ra file riêng). OCP áp dụng qua component composition và render props/children thay vì if/else trong component. DIP áp dụng qua dependency injection pattern trong custom hook: useUserService(api: ApiClient) nhận API client từ ngoài thay vì hard-code.
Ví dụ thực tế: tách UserProfile thành UserAvatar, UserInfo, UserActions components (SRP); dùng AuthContext inject authService vào app (DIP); tạo useFormValidation(validators: Validator[]) nhận validators từ ngoài (OCP + DIP). SOLID trong frontend thường ít formal hơn backend nhưng nguyên tắc vẫn có giá trị tương đương.
Singleton đảm bảo một class chỉ có duy nhất một instance và cung cấp global access point đến instance đó.
Triển khai TypeScript:
class DatabaseConnection {
private static instance: DatabaseConnection
private constructor() {}
static getInstance() {
if (!this.instance) this.instance = new DatabaseConnection()
return this.instance
}
}Vấn đề:
- khó unit test vì global state — mock singleton phức tạp;
- trong Node.js multi-worker hoặc serverless, mỗi worker/function instance có Singleton riêng, không đảm bảo 'single' instance toàn hệ thống;
- vi phạm SRP và DIP khi class tự quản lý lifecycle của mình;
- hidden dependency — code dùng
Database.getInstance()thay vì inject rõ ràng
Thay thế tốt hơn: dùng DI container (NestJS, InversifyJS) quản lý scope của dependency.
Singleton hợp lý cho: logger, config manager trong môi trường single-process.
Factory Method định nghĩa interface để tạo object nhưng để subclass quyết định class nào sẽ được instantiate — class cha không hard-code class con cụ thể.
Ví dụ TypeScript:
abstract class Notification {
abstract createSender(): Sender
send(msg: string) {
this.createSender().send(msg)
}
}
class EmailNotification extends Notification {
createSender() { return new EmailSender() }
}Khác với Simple Factory (không phải GoF pattern): Simple Factory chỉ là một method/class có logic if/else tạo object — dễ hiểu nhưng vi phạm OCP vì phải sửa khi thêm type mới.
- Factory Method tuân thủ OCP vì thêm type mới chỉ cần tạo subclass mới.
- Dùng Factory Method khi: framework cần cho phép extension mà không biết trước loại object sẽ tạo; khi muốn user override cách tạo object.
- Không dùng khi: hierarchy đơn giản, Simple Factory đủ dùng.
Abstract Factory cung cấp interface để tạo ra các 'family' of related objects mà không cần specify concrete class.
Ví dụ: interface UIFactory { createButton(): Button; createCheckbox(): Checkbox; } với WindowsUIFactory và MacUIFactory implement — đảm bảo Button và Checkbox luôn cùng theme. Khác Factory Method: Factory Method tạo một loại product, Abstract Factory tạo nhiều loại product có quan hệ với nhau. Dùng Abstract Factory khi: cần đảm bảo consistency giữa các component liên quan (UI theme, cross-platform widgets, database driver + connection pool); khi hệ thống cần support nhiều 'variant' và bạn muốn swap toàn bộ variant cùng lúc. Không dùng khi: chỉ có một loại product cần tạo (Factory Method đủ); khi family chỉ có một variant. Trong React, pattern này xuất hiện dưới dạng Context + Provider cung cấp nhiều service liên quan.
Builder tách quá trình xây dựng object phức tạp thành các bước riêng biệt, cho phép tạo nhiều biểu diễn khác nhau của cùng một object.
Ví dụ TypeScript với method chaining:
const query = new QueryBuilder()
.select('*')
.from('users')
.where('age > 18')
.orderBy('name')
.limit(10)
.build()
// thay vì: new Query('*', 'users', 'age > 18', 'name', 10, undefined, undefined)Ưu điểm so với constructor nhiều tham số: không cần nhớ thứ tự tham số, tham số optional không cần truyền undefined, code readable hơn, có thể validate từng bước.
- Dùng khi: object có nhiều optional parameter (Telescoping Constructor anti-pattern); khi quá trình khởi tạo phức tạp có nhiều bước; khi cần tạo nhiều variant của cùng object.
- Trong Go, Builder thường implement bằng functional options pattern:
NewServer(WithPort(8080), WithTimeout(30*time.Second)).
Prototype cho phép copy (clone) object hiện có mà không phụ thuộc vào class của nó — tạo object mới bằng cách sao chép prototype. JavaScript đặc biệt phù hợp vì có prototype-based inheritance tự nhiên.
Ví dụ: const config = { timeout: 3000, retries: 3 }; const apiConfig = { ...config, baseURL: '/api' } — spread operator là Shallow Prototype. Dùng khi: khởi tạo object tốn kém (DB query, file I/O) và muốn clone thay vì tạo mới; khi có nhiều preset configuration chỉ khác nhau vài thuộc tính. Chú ý Shallow vs Deep clone: Object.assign() và spread {...} chỉ shallow clone, object nested vẫn shared reference — dùng structuredClone() (ES2022) hoặc JSON.parse(JSON.stringify()) cho deep clone. Không dùng khi object có circular reference hoặc function (JSON.stringify sẽ mất).
Object Pool duy trì một tập các object đã được khởi tạo sẵn để tái sử dụng thay vì tạo/hủy liên tục — tối ưu cho object tốn kém để khởi tạo. Ứng dụng phổ biến nhất: database connection pool — thay vì tạo connection mới cho mỗi request, pool duy trì N connections sẵn sàng.
- Trong Node.js với
pg(PostgreSQL):const pool = new Pool({ max: 10, min: 2, idleTimeoutMillis: 30000 })— pool tự quản lý lifecycle của connections. - Trong Go,
sync.Poolđược dùng cho object allocation thường xuyên:var bufPool = sync.Pool{ New: func() interface{} { return new(bytes.Buffer) } }. - Dùng khi: object expensive to create (DB connections, thread, large buffers); high throughput applications.
- Không dùng khi: object khởi tạo nhanh, pool overhead > benefit; khi pool quá nhỏ gây contention; khi object có state phức tạp khó reset.
DI là technique mà object nhận dependencies từ bên ngoài thay vì tự tạo — là cơ chế triển khai DIP.
Ba loại DI:
// (1) Constructor Injection (phổ biến nhất)
class UserService {
constructor(private userRepo: UserRepository) {}
}
// (2) Setter/Property Injection (cho optional dependency)
class UserService {
setRepository(repo: UserRepository) { this.repo = repo }
}
// (3) Method Injection (chỉ cần cho một operation)
class UserService {
getUser(id: string, repo: UserRepository) { return repo.find(id) }
}Framework DI: NestJS dùng IoC container với decorators @Injectable(), @Inject() — tự động resolve dependency tree. Trong Go không có framework DI phổ biến, thường dùng manual DI qua wire (Google) hoặc constructor functions.
Lợi ích: loose coupling, dễ test (inject mock), dễ swap implementation. Anti-pattern: Service Locator (class tự gọi container.get('UserRepo')) — tạo hidden dependency.
Creational patterns giải quyết vấn đề tạo object một cách linh hoạt. Cách chọn:
- Singleton: cần đúng một instance shared toàn app (logger, config) — nhưng ưu tiên DI container thay vì tự implement.
- Factory Method: framework cần cho phép subclass override cách tạo object.
- Abstract Factory: cần tạo nhóm object liên quan phải consistent với nhau.
- Builder: object có nhiều optional params hoặc construction process phức tạp nhiều bước.
- Prototype: clone nhanh hơn tạo mới (object expensive to initialize).
- Object Pool: object expensive to create và cần reuse ở high throughput.
Quy tắc thực tế: bắt đầu simple (new Constructor()), refactor sang pattern khi thấy pain point rõ ràng. YAGNI áp dụng cho cả design patterns.
Adapter cho phép các interface incompatible làm việc cùng nhau bằng cách bọc một object trong wrapper cung cấp interface mà client kỳ vọng.
Ví dụ thực tế: tích hợp third-party payment SDK:
interface PaymentProvider {
charge(cents: number): Promise<Receipt>
}
class PayPalAdapter implements PaymentProvider {
constructor(private sdk: PayPalSDK) {}
charge(cents: number) {
return this.sdk.makePayment(cents / 100, 'USD')
}
}Hai loại Adapter: Object Adapter (composition, prefer này) và Class Adapter (multiple inheritance — không có trong TS/JS).
- Dùng khi: tích hợp legacy code hoặc third-party library có interface không phù hợp; khi không thể sửa source code của class cần adapt.
- Trong frontend, Adapter thường dùng để normalize API response format khác nhau về một schema thống nhất.
Bridge tách abstraction khỏi implementation để cả hai có thể thay đổi độc lập — giải quyết 'Cartesian product' explosion khi có nhiều dimension.
Ví dụ: Shape (Circle, Square) × Renderer (SVGRenderer, CanvasRenderer) = 4 class nếu dùng inheritance, nhưng với Bridge chỉ cần 2+2 class:
class Circle {
constructor(private renderer: Renderer, private radius: number) {}
draw() {
this.renderer.renderCircle(this.radius)
}
}
class Square {
constructor(private renderer: Renderer, private side: number) {}
draw() {
this.renderer.renderSquare(this.side)
}
}- Khác Adapter: Adapter giải quyết incompatibility giữa existing interfaces (fix sau); Bridge thiết kế từ đầu để tách abstraction-implementation (design upfront).
- Dùng Bridge khi: muốn tránh class explosion do kết hợp nhiều dimension; khi abstraction và implementation cần thay đổi độc lập; khi muốn share implementation giữa nhiều object.
- Không dùng khi: chỉ có một dimension biến đổi — over-engineering.
Composite cho phép compose objects thành tree structures để biểu diễn part-whole hierarchies — client xử lý individual objects và compositions đồng nhất qua cùng một interface.
Ví dụ: file system:
interface FileSystemItem {
getSize(): number
}
class File implements FileSystemItem {
constructor(private size: number) {}
getSize() { return this.size }
}
class Folder implements FileSystemItem {
private children: FileSystemItem[] = []
add(item: FileSystemItem) { this.children.push(item) }
getSize() {
return this.children.reduce((sum, c) => sum + c.getSize(), 0)
}
}Client gọi getSize() trên cả File và Folder mà không cần biết loại.
- Trong React, component tree là ứng dụng tự nhiên của Composite — component con và cha đều là React component.
- Dùng khi: cần biểu diễn tree hierarchy; khi muốn client treat leaf và composite đồng nhất.
- Không dùng khi: hierarchy phẳng hoặc interface chung quá generic, khó type-safe.
Decorator đính kèm thêm behavior vào object tại runtime bằng cách bọc chúng trong decorator objects — thay thế cho inheritance khi cần linh hoạt.
Ví dụ TypeScript:
interface Coffee { cost(): number }
class SimpleCoffee implements Coffee {
cost() { return 10 }
}
class MilkDecorator implements Coffee {
constructor(private coffee: Coffee) {}
cost() { return this.coffee.cost() + 2 }
}
class SugarDecorator implements Coffee {
constructor(private coffee: Coffee) {}
cost() { return this.coffee.cost() + 1 }
}
// Stack decorators:
const myCoffee = new SugarDecorator(new MilkDecorator(new SimpleCoffee()))
myCoffee.cost() // 13Khác Inheritance: Decorator thêm behavior tại runtime và có thể stack nhiều lớp; inheritance static tại compile time và có thể gây explosion.
- TypeScript
@decoratorsyntax là Decorator pattern nhưng dành cho class/method metadata, không hẳn là GoF Decorator. - Dùng khi: cần thêm behavior mà không muốn sửa class gốc; khi cần combine nhiều behavior tùy chọn.
- Không dùng khi: decorator stack quá sâu gây khó debug.
Facade cung cấp simplified interface cho một hệ thống phức tạp, subsystem hoặc library — giảm dependency giữa client code và internals phức tạp.
Ví dụ: thay vì client gọi trực tiếp 5 service (AuthService, UserService, ProfileService, CacheService, LogService), ta tạo UserFacade với method đơn giản như getUserProfile(id) tự phối hợp các service. Trong frontend: custom hook là Facade xuất sắc — useAuth() ẩn đi chi tiết của JWT storage, API call, state management; component chỉ gọi const { user, login, logout } = useAuth(). Facade không ngăn client access subsystem trực tiếp nếu cần — khác với Proxy. Dùng khi: có subsystem phức tạp cần đơn giản hóa; khi muốn layer hóa architecture (presentation → service facade → domain). Không dùng khi: tạo ra 'God Facade' ôm quá nhiều thứ — vi phạm SRP.
Proxy cung cấp surrogate object thay thế cho object khác — control access đến object gốc và có thể thêm logic trước/sau.
Các loại phổ biến:
- Virtual Proxy (lazy initialization): chỉ tạo object nặng khi thực sự cần — ví dụ lazy load image;
- Protection Proxy (access control): kiểm tra permission trước khi delegate;
- Caching Proxy: cache result của expensive operation;
- Logging Proxy: ghi log mọi request đến object
JavaScript Proxy object là triển khai native:
const handler = {
get(obj, prop) {
console.log(`Getting ${String(prop)}`)
return obj[prop]
}
}
const proxy = new Proxy(target, handler)Trong NestJS, Guards và Interceptors là Proxy pattern.
- Khác Decorator: Proxy thường quản lý lifecycle của subject; Decorator thêm behavior mà client biết.
- Dùng khi: cần access control, lazy init, caching, logging mà không sửa class gốc.
Flyweight giảm memory usage bằng cách chia sẻ state chung (intrinsic state) giữa nhiều objects tương tự — chỉ lưu state riêng (extrinsic state) trong object cụ thể.
Ví dụ game: render 10,000 cây trong rừng — thay vì mỗi Tree object lưu texture/mesh riêng, tạo TreeType flyweight lưu shared data, Tree chỉ lưu position/scale. Trong JavaScript: string interning, Symbol, React key reconciliation có elements của Flyweight.
Dùng khi: app cần số lượng rất lớn objects tương đồng và memory là bottleneck. Không dùng khi: số lượng object ít; khi overhead quản lý flyweight > memory saved. Flyweight tăng code complexity đáng kể — chỉ dùng khi profiling chứng minh memory là vấn đề thực sự.
Structural patterns giải quyết cách compose class và object thành larger structures. Chọn theo use case:
- Adapter: tích hợp incompatible interface (thường là third-party lib)
- Bridge: thiết kế mới cần tách abstraction-implementation để scale độc lập
- Composite: dữ liệu có dạng tree, cần treat leaf/branch đồng nhất
- Decorator: thêm behavior tại runtime mà không sửa class gốc
- Facade: đơn giản hóa subsystem phức tạp cho client
- Proxy: control access hoặc add cross-cutting concern (logging, caching, auth)
- Flyweight: memory optimization critical với nhiều objects tương đồng
Nhiều pattern Structural có thể kết hợp — Facade có thể dùng bên trong các Proxy; Decorator và Composite hay đi cùng nhau trong UI tree.
Observer định nghĩa one-to-many dependency: khi một object (Subject/Observable) thay đổi state, tất cả dependents (Observers) được notify tự động.
Ví dụ TypeScript:
class EventEmitter {
private listeners = new Map<string, Function[]>()
on(event: string, cb: Function) {
if (!this.listeners.has(event)) this.listeners.set(event, [])
this.listeners.get(event)!.push(cb)
}
emit(event: string, data?: unknown) {
this.listeners.get(event)?.forEach(cb => cb(data))
}
}- Trong JavaScript:
EventEmittercủa Node.js,addEventListenertrong DOM, RxJS Observable đều là Observer pattern. - React:
useEffectvới dependency array là lazy Observer; Redux store notify components khi state thay đổi. - Pub/Sub là biến thể: thêm message broker trung gian, subject và observer không biết nhau trực tiếp (khác Observer thuần).
- Dùng khi: cần event-driven architecture; khi một thay đổi trigger nhiều phản ứng không biết trước.
- Không dùng khi: dependency graph phức tạp gây 'observer hell' — khó trace execution flow.
Strategy định nghĩa family of algorithms, encapsulate mỗi cái, và làm chúng interchangeable — cho phép algorithm thay đổi độc lập với client sử dụng nó.
Ví dụ TypeScript:
interface SortStrategy {
sort(data: number[]): number[]
}
class QuickSort implements SortStrategy {
sort(data: number[]) { /* ... */ return data }
}
class MergeSort implements SortStrategy {
sort(data: number[]) { /* ... */ return data }
}
class Sorter {
constructor(private strategy: SortStrategy) {}
sort(data: number[]) { return this.strategy.sort(data) }
}So với if/else: Strategy tuân thủ OCP — thêm algorithm mới không cần sửa Sorter; if/else vi phạm OCP, dài dần theo thời gian.
- Dùng Strategy khi: có nhiều variant của một algorithm; khi muốn swap algorithm tại runtime; khi muốn isolate algorithm logic để dễ test riêng từng cái.
- Trong React, strategy hay xuất hiện dưới dạng render props hoặc component props nhận function:
<DataTable sortFn={quickSort} filterFn={fuzzyFilter} />. - Không dùng khi chỉ có 2-3 strategy ít thay đổi — if/else đủ đơn giản hơn.
Command encapsulate một request thành object, cho phép parameterize clients với different requests, queue/log requests, và support undoable operations.
Cấu trúc:
interface Command {
execute(): void
undo(): void
}
class CommandManager {
private historyStack: Command[] = []
private redoStack: Command[] = []
run(cmd: Command) {
cmd.execute()
this.historyStack.push(cmd)
this.redoStack = []
}
undo() {
const cmd = this.historyStack.pop()
if (cmd) { cmd.undo(); this.redoStack.push(cmd) }
}
redo() {
const cmd = this.redoStack.pop()
if (cmd) { cmd.execute(); this.historyStack.push(cmd) }
}
}Ứng dụng thực tế: text editor (Ctrl+Z), transaction trong database, task queue (Bull, BullMQ), HTTP request retry.
- Trong Redux, mỗi
dispatch(action)là Command —redux-undolibrary implement Undo/Redo bằng Command pattern. - Dùng khi: cần undo/redo, transaction, audit log, retry; khi muốn queue và schedule requests.
- Không dùng khi: simple method call đủ dùng — Command overhead không cần thiết.
Template Method định nghĩa skeleton của algorithm trong method của superclass, để subclass override các bước cụ thể mà không thay đổi cấu trúc tổng thể.
Ví dụ:
abstract class DataProcessor {
// Template method — skeleton cố định
process() {
this.readData()
this.parseData()
this.analyze()
this.report()
}
abstract readData(): void
abstract parseData(): void
analyze() { console.log('Analyzing...') } // hook có default
report() { console.log('Done') }
}
class CSVProcessor extends DataProcessor {
readData() { /* đọc file CSV */ }
parseData() { /* parse CSV */ }
}Khác Strategy: Template Method dùng inheritance (lúc compile time); Strategy dùng composition (lúc runtime).
- Template Method khi biết trước structure cố định nhưng detail thay đổi theo subclass; Strategy khi muốn thay đổi toàn bộ algorithm tại runtime.
- Dùng khi: nhiều class share cùng algorithm skeleton nhưng differ ở implementation chi tiết; khi muốn tránh code duplication trong step chung.
- Không dùng khi: cần thay đổi algorithm tại runtime (dùng Strategy); khi hierarchy quá sâu gây khó hiểu flow.
Chain of Responsibility cho phép pass request qua chain of handlers — mỗi handler quyết định xử lý hoặc pass cho handler tiếp theo.
- Express.js middleware là ví dụ hoàn hảo:
app.use(authMiddleware, rateLimitMiddleware, validationMiddleware, routeHandler)— mỗi middleware gọinext()để pass request.
Ví dụ TypeScript:
interface Handler {
setNext(h: Handler): Handler
handle(request: number): string | null
}
abstract class AbstractHandler implements Handler {
protected nextHandler?: Handler
setNext(h: Handler) {
this.nextHandler = h
return h
}
handle(r: number): string | null {
return this.nextHandler?.handle(r) ?? null
}
}
class AuthHandler extends AbstractHandler {
handle(r: number) {
if (r < 0) return 'Unauthorized'
return super.handle(r)
}
}- Dùng khi: nhiều object có thể handle request và muốn decouple sender từ receiver; khi handlers nên được configurable hoặc reorderable tại runtime.
- Khác Observer: Chain of Responsibility chỉ một handler xử lý; Observer notify tất cả.
- Không dùng khi: chain quá dài gây performance issue; khi không rõ handler nào sẽ xử lý gây khó debug.
State pattern cho phép object thay đổi behavior khi state nội tại thay đổi — object dường như thay đổi class. Thay vì if (state === 'idle') ... else if (state === 'loading') ... khắp codebase, mỗi state là một class với behavior riêng.
Ví dụ: TrafficLight có states RedState, GreenState, YellowState — mỗi state implement interface TrafficLightState { next(light: TrafficLight): void; getColor(): string }. State pattern và FSM (Finite State Machine) rất gần nhau: FSM là concept toán học (states, transitions, inputs), State pattern là implementation OOP của FSM. Thư viện XState cho JavaScript implement FSM/Statechart theo cách declarative, phù hợp hơn State pattern thuần cho complex flows. Dùng khi: object có behavior phụ thuộc rõ ràng vào state; khi state transitions phức tạp và cần maintainable. Không dùng khi: chỉ 2-3 states đơn giản — enum + switch/case readable hơn.
Iterator cung cấp cách access tuần tự các elements của collection mà không expose underlying representation.
- JavaScript triển khai Iterator Protocol tự nhiên: object là iterator nếu có
next()method trả về{ value, done }.
Ví dụ custom iterator:
class Range {
constructor(private start: number, private end: number) {}
[Symbol.iterator]() {
let current = this.start
const end = this.end
return {
next() {
return current <= end
? { value: current++, done: false as const }
: { value: undefined, done: true as const }
}
}
}
}
// Dùng trong for...of, spread, destructuring:
const range = new Range(1, 5)
for (const n of range) console.log(n) // 1 2 3 4 5
console.log([...new Range(1, 3)]) // [1, 2, 3]Generator functions (function*) là sugar syntax tạo iterator/iterable.
- Dùng khi: cần traverse collection mà không expose structure; khi muốn lazy evaluation (generate values on demand — tiết kiệm memory cho large datasets).
- Trong React, Suspense và streaming server rendering dùng iterator concept.
Ba architectural patterns tách UI logic:
- MVC (Model-View-Controller): Controller xử lý input, update Model, View render Model — Controller và View biết nhau; truyền thống trong server-side (Rails, Laravel, Express).
- MVP (Model-View-Presenter): Presenter xử lý logic thay Controller, View passive hơn chỉ nhận data từ Presenter và forward events — testable hơn vì Presenter pure class, không phụ thuộc UI.
- MVVM (Model-View-ViewModel): ViewModel expose Observable state, View bind tự động qua data binding — Vue.js, Angular, WPF, React với hooks.
Trong React hiện đại: Component = View, Custom Hook = ViewModel (logic + state), Service/Store = Model. Data flow: MVC hai chiều; MVVM data binding tự động; MVP qua interface contract.
Mediator giảm chaotic dependencies giữa objects bằng cách làm chúng communicate qua mediator object thay vì trực tiếp — từ many-to-many thành many-to-one. Ví dụ UI: thay vì các form components tham chiếu nhau trực tiếp, chúng communicate qua FormMediator quản lý validation và enable/disable logic.
Trong microservices: Message Broker (RabbitMQ, Kafka) là Mediator — service A không gọi service B trực tiếp mà publish message lên broker, broker route đến service B.
Khác Observer: Observer cho phép Subject notify Observers (biết có observers); Mediator giúp objects communicate mà không biết về nhau. Trong NestJS: EventEmitter2, CQRS CommandBus, EventBus là Mediator.
Lợi ích microservices:
- Loose coupling giữa services.
- Dễ scale từng service độc lập.
- Có thể add consumer mới mà không thay đổi producer.
Không dùng khi: Mediator trở thành 'God Object' biết quá nhiều — tạo single point of failure.
Visitor cho phép thêm operation mới vào object structure mà không cần sửa class của các objects đó — tách algorithm khỏi data structure nó operates on.
- Cấu trúc:
interface Visitor { visitCircle(c: Circle): void; visitRectangle(r: Rectangle): void }— mỗi shape cóaccept(visitor: Visitor)gọi đúng visit method. - Vấn đề Visitor giải quyết: khi có stable object hierarchy (ít thêm class mới) nhưng cần thêm nhiều operations mới — với Visitor, thêm operation = thêm Visitor class mới mà không sửa Shape classes.
- Trade-off vs SOLID: Visitor tuân thủ OCP cho operations nhưng vi phạm OCP cho elements — khi thêm
Triangle, phải sửa tất cả Visitor. - Dùng khi: AST (Abstract Syntax Tree) processing — compiler, code analyzer, transformer; document model với nhiều export format.
- TypeScript compiler dùng Visitor cho AST traversal.
- Không dùng khi: object hierarchy thay đổi thường xuyên; khi chỉ cần 1-2 operations — polymorphism đơn giản hơn.
Anti-patterns là solutions thoạt nhìn hợp lý nhưng thực ra gây hại về lâu dài.
- God Object/Class: một class biết và làm quá nhiều — vi phạm SRP, khó test, bottleneck khi scale
- Spaghetti Code: logic phân tán, tangled dependencies, không có structure rõ ràng
- Golden Hammer: dùng quen một tool/pattern cho mọi vấn đề dù không phù hợp
- Premature Optimization: tối ưu trước khi có evidence về bottleneck — lãng phí thời gian, tăng complexity
- Copy-Paste Programming: vi phạm DRY, bug fix ở một chỗ không fix chỗ khác
- Magic Numbers/Strings: hardcode
if (status === 3)thay vìif (status === OrderStatus.SHIPPED) - Shotgun Surgery: một thay đổi require sửa nhiều class nhỏ — ngược lại God Object
- Callback Hell trong JavaScript: Promise chain và async/await giải quyết
Nhận biết: code smell là dấu hiệu sớm của anti-pattern.