🧱 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
statictrong 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ử
newtrự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
Prototypecó 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.createcho 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 khaiVehicleVehicleFactory: interface cócreateVehicle()CarFactory,TruckFactory: trả vềCarhoặ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 literalhoặcFactory Mapcho 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.