Chương 04 · Creational Patterns

Creational Patterns — 5 mẫu khởi tạo

Factory Method · Abstract Factory · Builder · Prototype · Singleton. Cách giấu logic khởi tạo, tách concrete class khỏi client code.

1. Tổng quan — Creational Patterns

Creational patterns tập trung vào cách tạo object. Chúng giúp:

  • Giấu logic phức tạp khi khởi tạo.
  • Cho phép tạo object thuộc family liên quan mà không khóa cứng concrete class.
  • Kiểm soát số lượng instance.
  • Tái sử dụng đối tượng có sẵn (clone) thay vì tạo mới đắt đỏ.

5 pattern trong nhóm:

PatternMột câuKhi nào dùng
Factory MethodSubclass quyết định class nào instantiateCó nhiều biến thể, khởi tạo phụ thuộc context
Abstract FactoryTạo "family" object liên quanCần consistency giữa nhóm object (vd: UI Mac/Win)
BuilderXây object phức tạp từng bướcObject có nhiều tham số, nhiều bước khởi tạo
PrototypeClone object có sẵnKhởi tạo đắt, có "template"
SingletonĐúng 1 instance toàn appHiếm — thường có cách tốt hơn

2. Factory Method

PATTERN №1 — CREATIONAL

Factory Method

Định nghĩa interface để tạo object, nhưng để subclass quyết định class cụ thể nào instantiate. Cho phép class trì hoãn việc khởi tạo cho subclass.

Vấn đề Code có chỗ new ConcreteThing() rải rác → khó đổi sang AnotherConcreteThing mà không sửa khắp nơi. Lợi ích Tách "logic dùng object" khỏi "logic tạo object" — Single Responsibility + Open/Closed. Đánh đổi Tạo thêm class/method, code thêm gián tiếp.

2.1. Vấn đề

// ❌ Code phải sửa khi thêm shipping provider mới:
class OrderProcessor {
  ship(order: Order, country: string) {
    let provider: ShippingProvider;
    if (country === 'VN') provider = new VietnamPost();
    else if (country === 'US') provider = new FedEx();
    else if (country === 'JP') provider = new JapanPost();
    // Mỗi provider mới → sửa class này

    provider.ship(order);
  }
}

2.2. Cấu trúc UML

┌────────────────────┐ ┌──────────────────┐ │ Creator │ │ Product │ │ (abstract) │ │ (interface) │ ├────────────────────┤ ├──────────────────┤ │ + factoryMethod() │ ──── creates ────► │ │ : Product │ │ + operation() │ │ + someOperation() │ └────▲─────────────┘ └─────────▲──────────┘ │ │ │ │ extends │ implements │ │ ┌────────┴─────────┐ ┌──────────┴─────────┐ │ ConcreteCreator │ │ ConcreteProduct │ ├──────────────────┤ ├────────────────────┤ │ + factoryMethod()│ │ + operation() │ │ : returns │ └────────────────────┘ │ ConcreteProduct│ └──────────────────┘

2.3. Implementation

// Product
interface ShippingProvider {
  ship(order: Order): void;
}

class VietnamPost implements ShippingProvider { ship(o: Order) {} }
class FedEx       implements ShippingProvider { ship(o: Order) {} }
class JapanPost   implements ShippingProvider { ship(o: Order) {} }

// Creator
abstract class OrderProcessor {
  abstract createProvider(): ShippingProvider;   // factory method

  ship(order: Order) {
    const provider = this.createProvider();
    provider.ship(order);
  }
}

// Concrete creators
class VnOrderProcessor extends OrderProcessor {
  createProvider() { return new VietnamPost(); }
}

class UsOrderProcessor extends OrderProcessor {
  createProvider() { return new FedEx(); }
}

2.4. Cách dùng đơn giản hơn — static factory method

class ShippingProviderFactory {
  static forCountry(country: string): ShippingProvider {
    switch (country) {
      case 'VN': return new VietnamPost();
      case 'US': return new FedEx();
      case 'JP': return new JapanPost();
      default:   return new InternationalCourier();
    }
  }
}

// Client:
const provider = ShippingProviderFactory.forCountry(order.country);
provider.ship(order);

"Static factory method" thực ra không phải GoF Factory Method gốc, mà là biến thể đơn giản. Trong thực tế đa số dev gọi cả hai là "Factory".

2.5. Khi NÀO dùng

  • Có nhiều biến thể của 1 product, chọn theo context.
  • Logic khởi tạo phức tạp (nhiều argument, validate, lookup).
  • Muốn cache instance, hoặc có pool.
  • Bạn cần hide concrete class khỏi client.

2.6. Khi KHÔNG cần

  • Chỉ có 1 cách tạo, không có biến thể: dùng new đủ rồi.
  • Constructor đã đủ — đừng tạo factory chỉ để wrap new.

3. Abstract Factory

PATTERN №2 — CREATIONAL

Abstract Factory

Cung cấp interface để tạo các family object liên quan mà không cần chỉ định class cụ thể.

Vấn đề Có nhiều "family" sản phẩm phải đi cùng nhau (vd: UI MacOS gồm MacButton + MacCheckbox; Windows gồm WinButton + WinCheckbox). Trộn lẫn = bug. Lợi ích Đảm bảo các sản phẩm cùng family được tạo cùng nhau — consistency. Đánh đổi Thêm 1 family product mới phải sửa interface — vi phạm OCP một phần.

3.1. Cấu trúc

// Abstract products
interface Button   { render(): void; }
interface Checkbox { render(): void; }

// Concrete products — 2 family
class MacButton    implements Button   { render() { console.log('Mac button'); } }
class MacCheckbox  implements Checkbox { render() { console.log('Mac checkbox'); } }

class WinButton    implements Button   { render() { console.log('Windows button'); } }
class WinCheckbox  implements Checkbox { render() { console.log('Windows checkbox'); } }

// Abstract factory
interface UIFactory {
  createButton(): Button;
  createCheckbox(): Checkbox;
}

// Concrete factories
class MacFactory implements UIFactory {
  createButton()   { return new MacButton(); }
  createCheckbox() { return new MacCheckbox(); }
}

class WinFactory implements UIFactory {
  createButton()   { return new WinButton(); }
  createCheckbox() { return new WinCheckbox(); }
}

// Client — không biết đang ở Mac hay Win
class App {
  constructor(private factory: UIFactory) {}

  render() {
    this.factory.createButton().render();
    this.factory.createCheckbox().render();
  }
}

// Composition root chọn factory:
const factory: UIFactory = process.platform === 'darwin' ? new MacFactory() : new WinFactory();
new App(factory).render();

3.2. Khác Factory Method ở đâu?

Factory MethodAbstract Factory
TạoMột productFamily nhiều product
CáchMethod (override trong subclass)Object factory chứa nhiều method
Mức trừu tượngMethod-levelClass/Object-level

3.3. Use cases thực tế

  • Multi-DB driver: PostgresFactory tạo connection + transaction + query builder Postgres-style; MySQLFactory tương tự cho MySQL.
  • Theme system: DarkTheme tạo button/input/card với màu tối; LightTheme với màu sáng.
  • Cross-platform UI library (cũ): React Native, Flutter dùng kiểu này nội bộ.

4. Builder

PATTERN №3 — CREATIONAL

Builder

Tách quá trình xây object phức tạp ra khỏi class chính, cho phép tạo nhiều biểu diễn khác nhau bằng cùng quá trình xây.

Vấn đề Constructor có 10+ tham số (telescoping constructor), nhiều tham số optional, dễ swap nhầm. Lợi ích Code tạo object đọc như câu nói; tham số có tên rõ; dễ thêm/bớt param. Đánh đổi Code nhiều hơn so với constructor đơn giản.

4.1. Vấn đề: Telescoping Constructor

class Pizza {
  constructor(
    size: number,
    cheese: boolean,
    pepperoni: boolean,
    mushrooms: boolean,
    onions: boolean,
    olives: boolean,
    extraSauce: boolean,
    glutenFree: boolean,
  ) {}
}

// ❌ Khó đọc:
new Pizza(12, true, false, true, false, false, true, false);
//        ↑    ↑     ↑      ↑     ↑      ↑      ↑      ↑
//       size  ?     ?      ?     ?      ?      ?      ?

4.2. Cách giải Builder

class Pizza {
  // Private constructor — chỉ Builder gọi
  private constructor(
    public readonly size: number,
    public readonly toppings: ReadonlyArray<string>,
    public readonly extraSauce: boolean,
    public readonly glutenFree: boolean,
  ) {}

  static builder(size: number) {
    return new PizzaBuilder(size);
  }
}

class PizzaBuilder {
  private toppings: string[] = [];
  private extraSauce = false;
  private glutenFree = false;

  constructor(private size: number) {}

  addTopping(t: string): this {
    this.toppings.push(t);
    return this;
  }

  withExtraSauce(): this { this.extraSauce = true; return this; }
  glutenFreeOption(): this { this.glutenFree = true; return this; }

  build(): Pizza {
    if (this.size < 8 || this.size > 16) throw new Error('invalid size');
    return new Pizza(this.size, [...this.toppings], this.extraSauce, this.glutenFree);
  }
}

// ✓ Tự nhiên như nói chuyện:
const pizza = Pizza.builder(12)
  .addTopping('cheese')
  .addTopping('pepperoni')
  .addTopping('mushrooms')
  .withExtraSauce()
  .build();

4.3. Builder vs Plain Object Argument

Trong TypeScript/JS, "named parameters" qua object thường thay được Builder cho case đơn giản:

// Đơn giản hơn cho 5-7 field:
new Pizza({
  size: 12,
  toppings: ['cheese', 'pepperoni'],
  extraSauce: true,
  glutenFree: false,
});

Builder thắng khi:

  • Cần validate giữa các bước xây.
  • Có method composite (vd: withMargheritaPreset() set nhiều field).
  • Cần nhiều biểu diễn xuất ra (build → Pizza, build → PizzaOrder, build → ToppingsList...).
  • Object thực sự phức tạp (SQL query, HTTP request có nhiều tầng nested).

4.4. Director (variant)

GoF gốc có thêm "Director" gói các bước build phổ biến:

class PizzaDirector {
  static margherita(builder: PizzaBuilder) {
    return builder.addTopping('mozzarella').addTopping('basil').build();
  }
  static pepperoniDeluxe(builder: PizzaBuilder) {
    return builder.addTopping('cheese').addTopping('pepperoni').addTopping('extra-pepperoni').build();
  }
}

Hiếm gặp trong code thực tế — preset thường là static method của Builder.

4.5. Use case kinh điển

  • StringBuilder trong Java — nối string hiệu quả.
  • SQL Query Builder (Knex.js, jOOQ).
  • HTTP Request Builder (axios, OkHttp).
  • UI Form Builder.

5. Prototype

PATTERN №4 — CREATIONAL

Prototype

Tạo object mới bằng cách clone một object có sẵn (prototype) thay vì gọi constructor.

Vấn đề Khởi tạo object đắt (load từ DB, tính toán nặng); muốn tạo nhiều object "giống" 1 template. Lợi ích Tránh tạo lại từ đầu; code không phụ thuộc concrete class. Đánh đổi Deep clone phức tạp khi object có nested reference.

5.1. Cấu trúc

interface Cloneable<T> {
  clone(): T;
}

class Document implements Cloneable<Document> {
  constructor(
    public title: string,
    public content: string,
    public tags: string[],
    public metadata: Record<string, any>,
  ) {}

  clone(): Document {
    return new Document(
      this.title,
      this.content,
      [...this.tags],                            // shallow copy array
      JSON.parse(JSON.stringify(this.metadata)), // deep clone object
    );
  }
}

// Sử dụng:
const template = new Document('Report', 'Default content...', ['draft'], { author: 'system' });
const doc1 = template.clone();
doc1.title = 'Q1 Report';

const doc2 = template.clone();
doc2.title = 'Q2 Report';

5.2. Deep clone — pitfall

Shallow clone copy reference, deep clone copy đệ quy. JSON.parse(JSON.stringify(x)) đơn giản nhưng:

  • Mất Date, RegExp, Map, Set, Function (chuyển thành object/null/undefined).
  • Vỡ với circular reference.
  • Mất prototype chain.

Cleaner: structuredClone() (browser/Node 17+):

const copy = structuredClone(original);
// Hỗ trợ Date, Map, Set, ArrayBuffer, circular reference

5.3. JavaScript prototype-based — Prototype "miễn phí"

JS có Object.create tạo object mới với prototype được chỉ định:

const carPrototype = {
  drive() { console.log(`${this.brand} is driving`); }
};

const myCar = Object.create(carPrototype);
myCar.brand = 'Toyota';
myCar.drive();   // "Toyota is driving"

5.4. Use case

  • Game: spawn 1000 enemy "giống" template thay vì tạo từ database.
  • Document templates (Word, Excel templates).
  • Object cache: load 1 lần, clone cho user khác.
  • Configuration "preset" + customize.

6. Singleton

PATTERN №5 — CREATIONAL

Singleton

Đảm bảo 1 class chỉ có đúng 1 instance, cung cấp global access point đến nó.

Vấn đề Có resource toàn app (logger, config, DB pool) — muốn 1 instance duy nhất. Lợi ích Kiểm soát số lượng instance; lazy initialization. Đánh đổi Global state ẩn → khó test, khó parallel, dễ bị lạm dụng. Trong nhiều case có cách tốt hơn (DI).

6.1. Implementation cơ bản

class Logger {
  private static _instance: Logger | null = null;

  private constructor() {}   // private → ai đó new sẽ lỗi

  static getInstance(): Logger {
    if (!this._instance) {
      this._instance = new Logger();
    }
    return this._instance;
  }

  log(msg: string) {
    console.log(`[${new Date().toISOString()}] ${msg}`);
  }
}

// Dùng:
Logger.getInstance().log('hello');

6.2. Thread-safe (Java)

// Bill Pugh idiom — lazy + thread-safe + no synchronized cost
public class Logger {
    private Logger() {}

    private static class Holder {
        private static final Logger INSTANCE = new Logger();
    }

    public static Logger getInstance() {
        return Holder.INSTANCE;
    }
}

// Hoặc dùng enum (Bloch khuyên):
public enum Logger {
    INSTANCE;
    public void log(String msg) { /* ... */ }
}

6.3. Vì sao Singleton bị "ghét"?

  • Hidden dependency — gọi Logger.getInstance() bên trong method = dependency ẩn, không thấy ở constructor signature.
  • Global state — test khó: 1 test set state → test khác bị ảnh hưởng.
  • Khó parallel — 2 thread cùng set config → race condition.
  • Khóa cứng implementation — không thể swap với mock dễ dàng.

6.4. Thay thế: DI + 1 instance ở composition root

// Composition root tạo 1 instance:
const logger = new Logger(/* config */);

// Inject mọi nơi:
const userService = new UserService(logger, ...);
const orderService = new OrderService(logger, ...);

// Test: tạo mock dễ dàng
const mockLogger = { log: jest.fn() };
const svc = new UserService(mockLogger, ...);

Một instance qua DI có cùng hiệu ứng "1 instance app" mà không có hidden global state. Đây là cách hiện đại.

6.5. Khi Singleton thực sự hữu ích

  • Resource thực sự duy nhất ở OS level: file system handle, hardware driver.
  • Cache với invariant strict: phải có đúng 1 cache để consistency.
  • Thư viện không có DI container (script đơn giản, prototype).
Lời khuyên Default: KHÔNG dùng Singleton. Dùng DI + register instance vào container ở scope "singleton". Cùng hiệu ứng, không nhược điểm.

7. Pattern hiện đại thay thế Creational

7.1. Dependency Injection thay Factory

DI container biết cách wire instance — ít cần Factory class viết tay.

// Thay vì:
class ShippingFactory {
  create(country: string): ShippingProvider { /* switch */ }
}

// DI register:
container.register('VN', VietnamPost);
container.register('US', FedEx);

// Resolve:
const provider = container.resolve(order.country);

7.2. Tagged Union / Discriminated Union thay Polymorphism Factory

FP-style cho data variant:

type Shape =
  | { type: 'circle';    radius: number }
  | { type: 'rectangle'; width: number; height: number }
  | { type: 'triangle';  base: number; height: number };

function area(s: Shape): number {
  switch (s.type) {
    case 'circle':    return Math.PI * s.radius ** 2;
    case 'rectangle': return s.width * s.height;
    case 'triangle':  return s.base * s.height / 2;
  }
}

Ưu: đơn giản, type-safe, không cần class. Nhược: thêm variant phải sửa area (vi phạm OCP một phần). Trade-off — chọn tùy bối cảnh.

7.3. Object literals và Configuration

Cho object đơn giản, đừng dùng Builder — dùng plain object:

interface PizzaConfig {
  size: number;
  toppings: string[];
  extraSauce?: boolean;
  glutenFree?: boolean;
}

function makePizza(c: PizzaConfig): Pizza {
  return new Pizza(c.size, c.toppings, c.extraSauce ?? false, c.glutenFree ?? false);
}

8. Bài tập

  1. Implement Factory Method cho hệ thống thanh toán: PaymentProcessor abstract với subclass StripeProcessor, VnPayProcessor, MomoProcessor. Mỗi processor tạo PaymentGateway riêng.
  2. Abstract Factory cho theme: LightThemeDarkTheme mỗi cái tạo Button, Input, Card tương ứng. App dùng theme bất kỳ.
  3. Builder cho HTTP request:
    const req = HttpRequest.builder()
      .url('https://api.example.com/users')
      .method('POST')
      .header('Authorization', 'Bearer xxx')
      .json({ name: 'Alice' })
      .timeout(5000)
      .build();
  4. Prototype: tạo class GameEnemy với template "Goblin" (hp=50, atk=10). Spawn 100 goblin từ template, mỗi cái có thể customize hp/atk khác nhau mà không ảnh hưởng template.
  5. Singleton: cài đặt AppConfig singleton. Sau đó refactor để dùng DI thay thế. So sánh trải nghiệm khi viết test.
  6. Phân biệt Factory Method vs Abstract Factory bằng 1 ví dụ thực tế.
  7. Cho code Pizza ở mục 4.2 — tạo preset method builder.margherita() set sẵn topping mozzarella + basil. Khi nào nên có preset trên Builder?

9. Quiz

Quiz cuối Chương 4

Factory Method giải vấn đề chính:

  • Object quá nhiều RAM
  • Class không có method
  • Tách "logic dùng object" khỏi "logic chọn/tạo concrete class" — client không phụ thuộc concrete
  • Hierarchy quá sâu
Factory Method giấu việc new ConcreteX(). Client gọi factory.create() hoặc creator.factoryMethod() — không biết concrete nào được tạo. Cho phép thêm/bớt biến thể mà không sửa client code (Open/Closed).

Khác biệt cốt lõi giữa Factory Method và Abstract Factory:

  • Không có khác biệt
  • Factory Method tạo 1 product; Abstract Factory tạo "family" nhiều product liên quan
  • Factory Method nhanh hơn
  • Abstract Factory chỉ có ở Java
Factory Method = method (override trong subclass) tạo 1 product. Abstract Factory = object có nhiều method tạo các product cùng "family" (vd: MacFactory tạo MacButton + MacCheckbox phải đi cùng nhau).

Builder pattern phù hợp nhất khi:

  • Object có 2 field
  • Class không có method
  • Class abstract
  • Object phức tạp với nhiều tham số (đặc biệt optional), cần validate giữa các bước, cần fluent API
Builder thắng khi có 5+ tham số (đặc biệt optional). Constructor 10 tham số khó đọc và dễ swap nhầm. Builder cho code "đọc như câu nói" + cho phép validate/ràng buộc giữa các bước. Cho object 2-3 field, plain constructor đủ.

Trong TypeScript, alternative đơn giản cho Builder thường là:

  • Object literal với named parameters
  • Singleton
  • Inheritance
  • Generics
JS/TS có object literal với named field — đủ rõ cho 5-7 param. Builder thắng khi cần validate giữa các bước, có preset method, hoặc object thực sự phức tạp (SQL query, nested config).

Prototype pattern dùng khi:

  • Object có 1 field
  • Cần Singleton
  • Khởi tạo tốn kém (load từ DB, calc nặng) và muốn tạo nhiều object "giống template"
  • Cần inheritance
Prototype clone object có sẵn thay vì tạo từ đầu. Hữu ích cho game (spawn 1000 enemy giống goblin), document template, configuration preset. JS có structuredClone built-in cho deep clone.

Singleton bị chỉ trích vì:

  • Chậm
  • Tốn RAM
  • Khó cài đặt
  • Hidden dependency, global state khó test, khóa cứng implementation
Gọi Logger.getInstance() trong method = dependency không hiện ở constructor signature. Test khó (state shared, hard to mock). Hiện đại: dùng DI, đăng ký 1 instance ở composition root → cùng hiệu quả, không nhược điểm.

Modern alternative thay Factory class trong nhiều case:

  • Inheritance
  • DI container — register với key, resolve theo key
  • Singleton
  • Builder
DI container biết cách wire instance theo key/type. Thay vì Factory class viết tay với switch case, register class vào container. Spring, NestJS, Angular dùng pattern này. Tagged Union (FP-style) cũng là alternative cho data variant.

JSON.parse(JSON.stringify(x)) để clone deep KHÔNG hỗ trợ:

  • Number, string
  • Plain object
  • Date, Map, Set, RegExp, Function, circular reference
  • Array
Date thành string, Map/Set thành {}, RegExp thành {}, Function biến mất, circular ref throw. Modern: structuredClone() (browser/Node 17+) hỗ trợ tất cả + circular.

Hoàn thành Chương 4 (5/23 patterns). Tiếp theo: Chương 5 — Structural Patterns (7 mẫu) →