TypeScript Advanced – Phần 2 – Design Patterns – Creational Patterns

🧱 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ự:
    1. Vấn đề cần giải quyết
    2. Giải pháp cụ thể + UML
    3. Mã nguồn mẫu
    4. Ứng dụng thực tế
    5. Đ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')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 APIDùng cho CompilerHost, CompilerOptions
AngularDịch vụ như HttpClient, Router là Singleton
RxJSCác Scheduler dùng pattern này để điều phối
NestJSModule 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ạo Car
  • (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ó method clone()
  • 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ọi factory.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ặc createProduct()
  • 🏗 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()stopEngine()
  • Car, Truck: triển khai Vehicle
  • VehicleFactory: interface có createVehicle()
  • CarFactory, TruckFactory: trả về Car hoặc Truck

⚙️ 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 codeViết lặp đi lặp lại create() cho từng loại sản phẩm
⚠️ Lạm dụngDễ dùng quá mức dù không cần → dẫn đến overengineering
📉 Tăng độ phức tạpQuả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ặc Factory Map cho project nhỏ

🌐 Real-world Examples

Hệ thốngFactory Method được dùng ở đâu
DOM APIdocument.createElement(), createTextNode()
ReactReact.createElement() dùng để khởi tạo component
AngularComponentFactory tạo component runtime
Game DevTạ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ể.
  • “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ợpGiải thích
🧬 Tạo các họ sản phẩm liên quanVí 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 runtimePhù 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 nhauTrá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ớpTạo nhiều interface + lớp → nặng cho project nhỏ
🧠 Abstraction quá sớmDễ lạm dụng khi chưa cần thiết
🧹 Refactor tốn côngViệ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.jsAbstractWsAdapter tạo IoAdapter, SocketIoAdapter
Inversify.jsIoC container tạo dịch vụ từ interface (WarriorNinja)
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

PatternMục tiêu
🧍‍♂️ SingletonDuy trì 1 instance duy nhất
🧬 PrototypeTạo object mới bằng clone
🧱 BuilderXây object phức tạp theo từng bước
🏭 Factory MethodTạo 1 loại sản phẩm, hoán đổi đơn lẻ
🧰 Abstract FactoryTạ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