🧱 Design Patterns trong TypeScript
❓ Tại sao cần?
- Tăng khả năng tái sử dụng, giảm coupling
- Giải quyết các vấn đề lặp lại như tạo đối tượng (Factory), quản lý trạng thái (State),…
🔎 Relevance hiện nay
- Dù đã ra đời từ 1994, các design pattern vẫn hữu ích
- TypeScript giúp viết pattern sạch hơn, dễ kiểm soát hơn Java/C++
- Mỗi pattern được giới thiệu theo trình tự:
- Vấn đề cần giải quyết
- Giải pháp cụ thể + UML
- Mã nguồn mẫu
- Ứng dụng thực tế
- Điểm mạnh/yếu cần cân nhắc
🏗️ Creational Patterns – Nhóm khởi tạo
🧍♂️ Singleton Pattern – Mẫu Thiết kế Đơn thể
🧠 Khái niệm cốt lõi
- Đảm bảo chỉ có một đối tượng duy nhất của một lớp tồn tại trong toàn bộ ứng dụng.
- Cung cấp điểm truy cập toàn cục đến đối tượng đó.
- Dùng khi đối tượng tốn tài nguyên để tạo, hoặc không hợp lý nếu có nhiều instance (ví dụ: logger, config, DB connection).
📝 Ví dụ điển hình:
- 🛠 Logging Service
- 🔌 Database Connection Pool
- ⚙️ Application Configuration
- 🚦 Thread Pool hoặc Object Pool
📌 Đặc điểm chính
- 🧷 Global Access Point: Toàn bộ chương trình dùng cùng một điểm truy cập (
getInstance()
). - 🧠 Instance Caching: Thể hiện được cache (thường là biến
static
trong class). - 💤 Lazy Initialization: Chỉ tạo khi cần dùng, tránh khởi tạo sớm tốn tài nguyên.
- 🧬 Unique per Class: Mỗi lớp có Singleton riêng.
⛳ Khi nào nên dùng Singleton
- ⚙️ Quản lý trạng thái toàn cục hoặc cấu hình cần truy cập ở nhiều nơi.
- 🌐 Điều phối truy cập tài nguyên bên ngoài (DB, API, filesystem).
- 🧠 Dùng như cache layer hoặc layer trung gian.
- 📜 Quản lý log/error xử lý đồng nhất toàn hệ thống.
⚠️ Cảnh báo: Dùng quá đà có thể dẫn tới:
- Coupling mạnh giữa các phần của hệ thống
- Khó test unit
- Vấn đề đa luồng nếu không đồng bộ hoá
📐 UML Diagram cho Singleton

- 🔒
private constructor()
→ không cho khởi tạo ngoài class - 🧱
private static instance
→ giữ duy nhất 1 đối tượng - 📞
public static getInstance()
→ điểm truy cập duy nhất
🧱 Classic Implementation (TypeScript)
class Singleton { private static instance: Singleton; private constructor() {} // ngăn new static getInstance(): Singleton { if (!Singleton.instance) { Singleton.instance = new Singleton(); } return Singleton.instance; } }
🎯 Khi gọi Singleton.getInstance()
, mọi lần sau sẽ nhận lại cùng một đối tượng.
🧪 Testing
- ✅ Kiểm tra
getInstance()
luôn trả về cùng một tham chiếu. - ✅ Đảm bảo trạng thái giữ lại giữa các lần gọi.
test('Singleton should return same instance', () => { const a = Singleton.getInstance(); const b = Singleton.getInstance(); expect(a).toBe(b); // Pass });
🚀 Modern Implementations
📦 1. Module Resolution Singleton (Node.js)
- Node.js tự động cache module sau lần import đầu tiên.
class ApiService {} export default new ApiService(); // singleton by module import apiService from './ApiService'; // Luôn là cùng một instance
⚠️ Lưu ý: nếu import bằng 2 path tuyệt đối khác nhau → có thể tạo 2 instance khác nhau.
🧙♂️ 2. Singleton via Decorator
function Singleton<T extends { new (...args: any[]): {} }>(constructor: T) { return class extends constructor { private static _instance: T | null = null; constructor(...args: any[]) { super(...args) if (!(<any>this.constructor)._instance) { ;(<any>this.constructor)._instance = this } return (<any>this.constructor)._instance } } as unknown as T & { _instance: T } } @Singleton class DecoratedSingleton { constructor() { console.log("DecoratedSingleton instance created") }}
✅ Dễ tái sử dụng cho nhiều class, giữ clean code.
🧩 3. Parametric Singleton
- Cho phép tạo Singleton theo tham số đầu vào (chẳng hạn URL API).
class ParametricSingleton { private static instances = new Map<string, ParametricSingleton>(); private constructor(private param: string) {} static getInstance(param: string): ParametricSingleton { if (!this.instances.has(param)) { this.instances.set(param, new ParametricSingleton(param)); } return this.instances.get(param)!; } getParam(): string { return this.param; } }
📦 getInstance('/v1/users')
và getInstance('/v2/users')
tạo hai Singleton khác nhau.
⚠️ Nhược điểm của Singleton
🧼 1. Global Instance Pollution
- Singleton giống như biến toàn cục → khó test, dễ ảnh hưởng bất ngờ giữa các phần.
- Gây tight coupling và khó tách module.
🧪 2. Khó test
- Nếu Singleton gọi API, I/O… sẽ khó mock hoặc kiểm soát test.
- Cần dùng mocking framework như Jest/Vitest cẩn thận.
🧱 3. Cài đặt đúng khó
- Nếu Singleton có state mutable, cần đảm bảo thread-safe.
- Dễ quên lazy init, hoặc quản lý đồng thời sai → bug nguy hiểm.
🌍 Real-World Examples
Hệ thống | Ứng dụng Singleton |
---|---|
TypeScript Compiler API | Dùng cho CompilerHost , CompilerOptions |
Angular | Dịch vụ như HttpClient , Router là Singleton |
RxJS | Các Scheduler dùng pattern này để điều phối |
NestJS | Module và Service mặc định là Singleton |
🧱 Builder Pattern – Mẫu Thiết kế Xây Dựng
🎯 Mục tiêu
- Giúp xây dựng đối tượng phức tạp thông qua các bước rời rạc
- Tách biệt logic xây dựng với class chính
📌 Khi nào nên dùng
- ✅ Đối tượng có nhiều tham số (>=3), trong đó một số là tuỳ chọn
- ✅ Bạn cần nhiều biến thể của object với cùng bước khởi tạo
- ✅ Bạn muốn xây dựng object theo thứ tự linh hoạt, mỗi bước độc lập
📐 UML Builder Pattern

- Class
Car
: sản phẩm cuối cùng - Interface
CarBuilder
: định nghĩa các bước tạo sản phẩm - Class
ConcreteCarBuilder
: triển khai cụ thể cách tạoCar
- (Tùy chọn)
Director
: đóng vai trò là “chỉ huy” điều phối các bước builder
🚗 Ví dụ TypeScript: Tạo xe hơi
class Car { constructor(public engine?: Engine, public wheels?: Wheels) {} } interface CarBuilder { setEngine(engine: Engine): CarBuilder; setWheels(wheels: Wheels): CarBuilder; build(): Car; } class ConcreteCarBuilder implements CarBuilder { private car = new Car(); setEngine(engine: Engine): CarBuilder { this.car.engine = engine; return this; } setWheels(wheels: Wheels): CarBuilder { this.car.wheels = wheels; return this; } build(): Car { const builtCar = this.car; this.car = new Car(); // reset return builtCar; } }
⛓️ API chainable: .setEngine().setWheels().build()
🧪 Testing
- ✅ Đảm bảo
build()
trả lại đúng đối tượng - ✅ Không có side-effect giữa các bước
- ✅ Viết test riêng cho từng builder cụ thể
npm run test --builder
❌ Hạn chế
- 🧱 Quá nhiều class nếu cần nhiều biến thể → tăng maintenance
- 🔁 Lặp lại code nếu mỗi đối tượng cần 1 builder riêng
- 📉 Với object đơn giản → Builder gây quá tải
- ⚠️ Có thể vi phạm Open/Closed Principle khi thêm step mới
- 🧪 Nếu không kiểm soát side-effect → gây lỗi không lường trước
💡 Gợi ý cải tiến
- Dùng TypeScript Generics để tạo builder linh hoạt
- Dùng composition thay vì inheritance
- Thêm bước validation tại mỗi step để giảm lỗi
class GenericBuilder<T> { private obj: Partial<T> = {}; set<K extends keyof T>(key: K, value: T[K]) { this.obj[key] = value; return this; } build(): T { return this.obj as T; } }
🌐 Ứng dụng thực tế
- Lodash.chain(): xây dựng chuỗi thao tác trên mảng/object
const youngestUser = _.chain(users) .sortBy('age') .head() .value();
value()
đóng vai trò nhưbuild()
🧬 Prototype Pattern – Mẫu Thiết kế Nguyên mẫu
🧠 Ý tưởng chính
- Prototype giúp tạo đối tượng mới bằng cách sao chép (clone) từ một đối tượng đã tồn tại, thay vì dùng
new
. - Mục tiêu: tránh tạo lại logic khởi tạo, đặc biệt cho các đối tượng phức tạp.
🧯 Khi nào nên dùng Prototype Pattern
- ✅ Bạn đã có sẵn một loạt đối tượng và muốn sao chép chúng nhanh tại runtime
- ✅ Bạn muốn tránh dùng toán tử
new
trực tiếp (vì nó có thể nặng nề hoặc không cần thiết) - ✅ Đối tượng có cấu trúc phức tạp, lồng nhau, việc clone giúp tiết kiệm công sức tạo lại
🧩 Cấu trúc UML

- Interface
Prototype
có methodclone()
- Các class cụ thể (ConcretePrototype1, ConcretePrototype2) implement interface này
- Client chỉ thao tác với interface, và gọi
clone()
để tạo bản sao
🐶 Ví dụ TypeScript: Tạo động vật bằng clone
interface AnimalPrototype { clone(): AnimalPrototype; } function deepClone<T>(obj: T): T { return JSON.parse(JSON.stringify(obj)); } class Dog implements AnimalPrototype { constructor(private breed: string, private age: number) {} clone(): Dog { return deepClone(this); } }
clone()
giúp tạo một bản sao độc lập về nội dung nhưng khác tham chiếu- Có thể dùng thư viện như
lodash.clonedeep
để clone sâu an toàn hơn JSON.stringify
🧪 Testing
- ✅ Kiểm tra clone có giữ giá trị giống bản gốc
- ✅ Đảm bảo clone và gốc là 2 object khác nhau
- ✅ Clone có thể thay đổi độc lập với bản gốc
npm run test --prototype
⚠️ Hạn chế
- ⚠️ Cần type cast sau khi clone → dễ gây lỗi
- ⚠️ Clone method bị lặp lại nhiều lần ở các lớp con
- ⚠️ Nếu dùng
BasePrototype
để kế thừa clone method → vô tình quay lại dùng inheritance (trái với mục tiêu của Prototype) - ⚠️ Gây coupling, khó mở rộng nếu không dùng đúng cách
🌐 Ứng dụng thực tế
- JavaScript prototypical inheritance chính là một dạng thực hiện của Prototype pattern
Object.create
cho phép tạo object mới dựa trên prototype
- React.cloneElement: clone React element với props mới → bản chất là clone + tuỳ chỉnh
🏭 Factory Method Pattern – Mẫu Phương Thức Nhà Máy
🎯 Khái niệm chính
- Mẫu thiết kế tạo ra “giao diện để tạo đối tượng”, cho phép subclass quyết định class cụ thể nào sẽ được khởi tạo.
- Giúp giảm sự phụ thuộc vào các lớp cụ thể → hướng tới loose coupling.
- ✅ Thay vì gọi
new
, ta gọifactory.create()
để tạo đối tượng tương ứng.
📦 Cấu trúc thành phần
- 🧩 Product Interface: định nghĩa các phương thức mà mọi sản phẩm phải có (ví dụ:
Vehicle
) - 🚗 Concrete Products: các class cụ thể (ví dụ:
Car
,Truck
) - 🏭 Factory Interface: khai báo method
create()
hoặccreateProduct()
- 🏗 Concrete Factories: cài đặt
create()
để trả về sản phẩm cụ thể
📌 Khi nào nên dùng Factory Method
- 🔁 Khi quá trình tạo object phức tạp (nhiều bước, nhiều ràng buộc)
- 🎲 Khi không biết rõ class cụ thể cho đến khi runtime
- 🔌 Khi muốn tách biệt logic tạo đối tượng khỏi phần sử dụng
- 🧬 Khi cần kiểm soát vòng đời đối tượng (tạo, huỷ, reuse…)
📐 UML Diagram: Tạo Vehicle

Vehicle
: interface cóstartEngine()
vàstopEngine()
Car
,Truck
: triển khaiVehicle
VehicleFactory
: interface cócreateVehicle()
CarFactory
,TruckFactory
: trả vềCar
hoặcTruck
⚙️ TypeScript Implementation
🛠 Khai báo Interface và Product
interface Vehicle { startEngine(): void; stopEngine(): void; } class Car implements Vehicle { startEngine() { console.log("Starting car engine...") } stopEngine() { console.log("Stopping car engine...") } } class Truck implements Vehicle { startEngine() { console.log("Starting truck engine...") } stopEngine() { console.log("Stopping truck engine...") } }
🏭 Khai báo Factory
interface VehicleFactory { createVehicle(): Vehicle; } class CarFactory implements VehicleFactory { createVehicle(): Vehicle { return new Car(); } } class TruckFactory implements VehicleFactory { createVehicle(): Vehicle { return new Truck(); } }
🧪 Sử dụng
const factories: VehicleFactory[] = [new CarFactory(), new TruckFactory()]; factories.forEach(factory => { const vehicle = factory.createVehicle(); vehicle.startEngine(); vehicle.stopEngine(); });
✅ Output:
nginxCopyEditStarting car engine...
Stopping car engine...
Starting truck engine...
Stopping truck engine...
🧬 Alternative Implementation
🧭 Dùng enum + switch
enum VehicleType { CAR, TRUCK } class VehicleCreator { create(vehicleType: VehicleType): Vehicle { switch (vehicleType) { case VehicleType.CAR: return new Car(); case VehicleType.TRUCK: return new Truck(); default: throw new Error("Unknown type"); } } }
⚠️ Dễ gây phình to switch-case, mất tính mở rộng
✅ Testing
Dùng toBeInstanceOf()
(Vitest) để kiểm tra object đúng loại:
test('CarFactory creates Car', () => { const factory = new CarFactory(); const car = factory.createVehicle(); expect(car).toBeInstanceOf(Car); });
npm run test --factory-method
⚠️ Hạn chế
Vấn đề | Giải thích |
---|---|
🔁 Boilerplate code | Viết lặp đi lặp lại create() cho từng loại sản phẩm |
⚠️ Lạm dụng | Dễ dùng quá mức dù không cần → dẫn đến overengineering |
📉 Tăng độ phức tạp | Quản lý nhiều factory và enum có thể làm hệ thống rối |
💡 Gợi ý cải tiến:
- 🎩 Dùng decorators để auto-register factory
- 🧱 Nếu đối tượng có nhiều bước cấu hình → cân nhắc dùng Builder Pattern
- ✅ Dùng
Object literal
hoặcFactory Map
cho project nhỏ
🌐 Real-world Examples
Hệ thống | Factory Method được dùng ở đâu |
---|---|
DOM API | document.createElement() , createTextNode() |
React | React.createElement() dùng để khởi tạo component |
Angular | ComponentFactory tạo component runtime |
Game Dev | Tạo enemy, item… mà không cần biết loại cụ thể trước |
🧰 Abstract Factory Pattern – Mẫu Nhà Máy Trừu Tượng
🎯 Khái niệm chính
- Cung cấp giao diện để tạo ra một “họ các đối tượng liên quan” mà không cần chỉ định lớp cụ thể.
- Là “factory của các factory” – cho phép hệ thống sử dụng các họ sản phẩm nhất quán mà không cần biết chi tiết bên trong.
💡 Mục tiêu:
“Tách phần sử dụng khỏi phần khởi tạo, và đảm bảo các sản phẩm được tạo tương thích với nhau.”
🧠 Khi nào nên dùng Abstract Factory
Trường hợp | Giải thích |
---|---|
🧬 Tạo các họ sản phẩm liên quan | Ví dụ: nút, menu, scrollbar trong UI toolkit |
🔗 Client chỉ tương tác với interface, không phụ thuộc lớp cụ thể | Dễ thay đổi factory khi cần |
🔄 Muốn hoán đổi factory lúc runtime | Phù hợp với app đa nền tảng hoặc đa cấu hình |
♻️ Đảm bảo các object tạo ra là tương thích với nhau | Tránh lỗi “trộn lẫn” giữa các họ sản phẩm |
📐 Cấu trúc UML

- AbstractFactory: khai báo các phương thức tạo ProductA, ProductB…
- ConcreteFactoryA, B: cài đặt cụ thể, tạo từng nhóm sản phẩm tương ứng
- ProductA, ProductB: interface cho từng loại sản phẩm
- ConcreteProductAX, BX: lớp cụ thể thuộc từng họ
➡️ Client chỉ tương tác với AbstractFactory + interface sản phẩm
⚙️ TypeScript Implementation: Xe của các hãng
🏗 Giao diện AbstractFactory
interface VehicleFactory { createCar(): Car; createMotorcycle(): Motorcycle; }
🚗 Giao diện sản phẩm
interface Car { drive(): void; } interface Motorcycle { ride(): void; }
🏭 Factory cụ thể – CompanyA
class CompanyAFactory implements VehicleFactory { createCar(): Car { return new CompanyACar(); } createMotorcycle(): Motorcycle { return new CompanyAMotorcycle(); } } class CompanyACar implements Car { drive() { console.log("Driving a Company A car"); } } class CompanyAMotorcycle implements Motorcycle { ride() { console.log("Riding a Company A motorcycle"); } }
👨💻 Client code
function produceVehicles(factory: VehicleFactory) { const car = factory.createCar(); const motorcycle = factory.createMotorcycle(); car.drive(); motorcycle.ride(); } produceVehicles(new CompanyAFactory()); // Driving a Company A car // Riding a Company A motorcycle produceVehicles(new CompanyBFactory()); // Driving a Company B car // Riding a Company B motorcycle
➡️ Client không cần biết chi tiết của CompanyA/B, chỉ cần biết VehicleFactory
.
🧪 Testing
✅ Kiểm tra rằng factory tạo đúng loại sản phẩm:
test('CompanyAFactory creates correct vehicles', () => { const factory = new CompanyAFactory(); expect(factory.createCar()).toBeInstanceOf(CompanyACar); expect(factory.createMotorcycle()).toBeInstanceOf(CompanyAMotorcycle); });
npm run test --abstract-factory
⚠️ Nhược điểm
Vấn đề | Giải thích |
---|---|
📦 Code phức tạp, nhiều lớp | Tạo nhiều interface + lớp → nặng cho project nhỏ |
🧠 Abstraction quá sớm | Dễ lạm dụng khi chưa cần thiết |
🧹 Refactor tốn công | Việc chuyển từ code thông thường sang Abstract Factory có thể mất thời gian |
💡 Giải pháp:
- 🧭 Chỉ dùng khi có ít nhất 2 nhóm sản phẩm trở lên
- 🧼 Giữ cấu trúc project sạch, chia module hợp lý
- 📄 Document rõ ràng cho người đọc hiểu từng factory tạo nhóm nào
🌍 Real-world Examples
Framework / Library | Ứng dụng Abstract Factory |
---|---|
Nest.js | AbstractWsAdapter tạo IoAdapter , SocketIoAdapter |
Inversify.js | IoC container tạo dịch vụ từ interface (Warrior → Ninja ) |
UI toolkit (giả lập) | Factory tạo nút, scrollbar, menu tùy theo platform: Windows / Mac / Mobile |
🧩 So sánh nhanh với các mẫu khác
Pattern | Mục tiêu |
---|---|
🧍♂️ Singleton | Duy trì 1 instance duy nhất |
🧬 Prototype | Tạo object mới bằng clone |
🧱 Builder | Xây object phức tạp theo từng bước |
🏭 Factory Method | Tạo 1 loại sản phẩm, hoán đổi đơn lẻ |
🧰 Abstract Factory | Tạo nhiều loại sản phẩm liên quan, theo họ nhất quán |
📌 Tóm tắt
- 🔁 Nắm được 5 Creational Design Patterns: Factory, Singleton, Builder, Prototype, Abstract Factory
- ⚙️ Biết cách áp dụng TypeScript để viết các pattern rõ ràng, chặt chẽ
- 💡 Mỗi pattern giúp giải quyết tình huống cụ thể về việc tạo object
Để lại một bình luận
Bạn phải đăng nhập để gửi bình luận.