Chương 05 · Structural Patterns

Structural Patterns — 7 mẫu cấu trúc

Adapter · Bridge · Composite · Decorator · Facade · Flyweight · Proxy. Cách ghép các đối tượng thành cấu trúc lớn linh hoạt.

1. Tổng quan Structural Patterns

Nhóm này tập trung vào cách các class và object được tổ chức thành cấu trúc lớn hơn. Mỗi pattern giải một vấn đề tổ chức cụ thể: hợp nhất API không tương thích (Adapter), tách abstraction khỏi implementation (Bridge), xử lý cây (Composite), thêm chức năng động (Decorator), v.v.

PatternMột câu
AdapterChuyển interface không tương thích → tương thích
BridgeTách abstraction (cái gì) khỏi implementation (cách)
CompositeCây object — leaf và composite cùng interface
DecoratorThêm trách nhiệm động bằng cách bao bọc
FacadeMột interface đơn giản che hệ con phức tạp
FlyweightChia sẻ object để tiết kiệm memory
ProxyObject đại diện kiểm soát truy cập đến object thật

2. Adapter

PATTERN №6 — STRUCTURAL

Adapter

Chuyển interface của một class thành interface khác mà client mong đợi. Cho phép class hoạt động cùng nhau mà không sửa code.

Vấn đề Có code legacy hoặc thư viện third-party với interface không tương thích với code mới. Lợi ích Tích hợp không sửa source. Bảo vệ code mới khỏi đặc tính của code cũ. Đánh đổi Thêm 1 lớp gián tiếp.

2.1. Use case kinh điển

App của bạn dùng PaymentProcessor nội bộ. Sếp bảo tích hợp Stripe — nhưng Stripe SDK có signature khác:

// Interface app dùng
interface PaymentProcessor {
  pay(amount: number, currency: string): Promise<{ id: string }>;
}

// Stripe SDK (third-party)
class StripeAPI {
  async createCharge(opts: { amountCents: number; currency: string; source: string }) {
    return { charge_id: 'ch_' + Date.now() };
  }
}

// Adapter
class StripeAdapter implements PaymentProcessor {
  constructor(private stripe: StripeAPI, private source: string) {}

  async pay(amount: number, currency: string) {
    const result = await this.stripe.createCharge({
      amountCents: Math.round(amount * 100),
      currency: currency.toLowerCase(),
      source: this.source,
    });
    return { id: result.charge_id };
  }
}

// App dùng adapter, không biết về Stripe
const processor: PaymentProcessor = new StripeAdapter(new StripeAPI(), 'tok_xxx');
await processor.pay(100, 'USD');

2.2. Class adapter vs Object adapter

  • Class adapter — kế thừa từ adaptee + implement target interface. Cần multiple inheritance.
  • Object adapter — chứa adaptee như field (composition). Linh hoạt hơn — TypeScript chỉ làm được cách này.

2.3. Khi nào dùng

  • Tích hợp third-party API có signature khác.
  • Migrate dần từ legacy class sang interface mới.
  • Test: tạo Adapter cho mock implementation.

3. Bridge

PATTERN №7 — STRUCTURAL

Bridge

Tách abstraction khỏi implementation để cả hai có thể vary độc lập.

Vấn đề Hierarchy chéo: M loại × N implementation = M×N class. Vd: Shape × Renderer. Lợi ích Tránh class explosion. Đổi implementation runtime. Đánh đổi Phức tạp hơn 1 hierarchy thông thường.

3.1. Vấn đề: class explosion

App vẽ shape (Circle, Square, Triangle) trên nhiều platform render (Canvas, SVG, WebGL). Cách "naive":

Shape / | \ Circle Square Triangle / | / | / | ... ... ... ... ... ... Mỗi shape × mỗi renderer = 1 class → 3 × 3 = 9 class. Thêm Pentagon → 3 class mới. Thêm WebGPU → 3 class mới.

3.2. Bridge giải quyết

// Implementation hierarchy (Renderer)
interface Renderer {
  drawCircle(x: number, y: number, r: number): void;
  drawRect(x: number, y: number, w: number, h: number): void;
}

class CanvasRenderer implements Renderer {
  drawCircle(x: number, y: number, r: number) { console.log(`Canvas: circle (${x},${y}) r=${r}`); }
  drawRect(x: number, y: number, w: number, h: number) { console.log(`Canvas: rect`); }
}

class SvgRenderer implements Renderer {
  drawCircle(x: number, y: number, r: number) { console.log(`SVG: <circle/>`); }
  drawRect(x: number, y: number, w: number, h: number) { console.log(`SVG: <rect/>`); }
}

// Abstraction hierarchy (Shape)
abstract class Shape {
  constructor(protected renderer: Renderer) {}
  abstract draw(): void;
}

class Circle extends Shape {
  constructor(renderer: Renderer, private x: number, private y: number, private r: number) {
    super(renderer);
  }
  draw() { this.renderer.drawCircle(this.x, this.y, this.r); }
}

class Square extends Shape {
  constructor(renderer: Renderer, private x: number, private y: number, private size: number) {
    super(renderer);
  }
  draw() { this.renderer.drawRect(this.x, this.y, this.size, this.size); }
}

// Sử dụng:
const canvas = new CanvasRenderer();
const svg = new SvgRenderer();

new Circle(canvas, 10, 10, 5).draw();
new Circle(svg, 10, 10, 5).draw();   // cùng circle, khác renderer

3 shape + 3 renderer = 6 class (thay vì 9). Thêm Pentagon → 1 class. Thêm WebGL renderer → 1 class.

3.3. Bridge vs Adapter

Adapter: fix sau — class đã tồn tại không tương thích, ta wrap. Bridge: thiết kế trước — biết hệ sẽ có 2 chiều biến đổi, tách từ đầu.

4. Composite

PATTERN №8 — STRUCTURAL

Composite

Compose object thành cấu trúc cây để biểu diễn hierarchy part-whole. Client xử lý leaf và composite một cách đồng nhất.

Vấn đề Có cây/đệ quy: file system (file + folder), UI (button + panel chứa button), org chart (employee + team). Lợi ích Client code đơn giản — không phân biệt leaf/composite. Đánh đổi Interface chung có thể "phình" để hỗ trợ cả leaf và composite.

4.1. Implementation

interface FileSystemNode {
  name: string;
  size(): number;
  print(indent?: string): void;
}

class File implements FileSystemNode {
  constructor(public name: string, private bytes: number) {}
  size() { return this.bytes; }
  print(indent = '') { console.log(`${indent}📄 ${this.name} (${this.bytes}B)`); }
}

class Folder implements FileSystemNode {
  private children: FileSystemNode[] = [];

  constructor(public name: string) {}

  add(node: FileSystemNode): this {
    this.children.push(node);
    return this;
  }

  size(): number {
    return this.children.reduce((sum, c) => sum + c.size(), 0);
  }

  print(indent = '') {
    console.log(`${indent}📁 ${this.name}/ (${this.size()}B total)`);
    this.children.forEach(c => c.print(indent + '  '));
  }
}

// Sử dụng:
const root = new Folder('project')
  .add(new File('package.json', 500))
  .add(new Folder('src')
    .add(new File('index.ts', 1500))
    .add(new File('utils.ts', 800))
  );

root.print();
console.log('Total size:', root.size());

4.2. Use cases thực tế

  • File system (như trên).
  • UI tree: Component với Container chứa Component con.
  • HTML/XML DOM.
  • Tổ chức công ty (employee, team, department).
  • Math expression: số (leaf) và biểu thức (composite).
  • Menu hierarchy.

4.3. Variant: Composite với uniform vs safe interface

  • Uniform: cả leaf và composite có cùng API (kể cả add, remove) — gọi file.add(...) không lỗi compile, nhưng có thể no-op hoặc throw.
  • Safe: chỉ composite có add/remove — type-safe nhưng client phải check type.

5. Decorator

PATTERN №9 — STRUCTURAL

Decorator

Thêm trách nhiệm cho object động bằng cách bao bọc trong wrapper. Linh hoạt hơn nhiều inheritance.

Vấn đề Cần thêm chức năng (logging, caching, retry, validation) cho object mà không muốn subclass cho mọi tổ hợp. Lợi ích Tổ hợp tự do nhiều decorator. Theo OCP — thêm chức năng không sửa class gốc. Đánh đổi Có thể tạo nhiều layer khó debug.

5.1. Vấn đề: combinatorial inheritance

class Coffee { cost() { return 5; } }
class CoffeeWithMilk extends Coffee { cost() { return super.cost() + 1; } }
class CoffeeWithSugar extends Coffee { cost() { return super.cost() + 0.5; } }
class CoffeeWithMilkAndSugar extends CoffeeWithMilk { /* + 0.5 */ }
class CoffeeWithMilkAndSugarAndCaramel extends ... ;
// Combinatorial explosion! 3 topping = 8 class

5.2. Decorator giải

interface Beverage {
  cost(): number;
  description(): string;
}

class Coffee implements Beverage {
  cost() { return 5; }
  description() { return 'Coffee'; }
}

class Tea implements Beverage {
  cost() { return 3; }
  description() { return 'Tea'; }
}

// Decorator base
abstract class BeverageDecorator implements Beverage {
  constructor(protected wrapped: Beverage) {}
  abstract cost(): number;
  abstract description(): string;
}

class WithMilk extends BeverageDecorator {
  cost() { return this.wrapped.cost() + 1; }
  description() { return this.wrapped.description() + ' + milk'; }
}

class WithSugar extends BeverageDecorator {
  cost() { return this.wrapped.cost() + 0.5; }
  description() { return this.wrapped.description() + ' + sugar'; }
}

class WithCaramel extends BeverageDecorator {
  cost() { return this.wrapped.cost() + 2; }
  description() { return this.wrapped.description() + ' + caramel'; }
}

// Tổ hợp tự do:
let drink: Beverage = new Coffee();
drink = new WithMilk(drink);
drink = new WithSugar(drink);
drink = new WithCaramel(drink);

console.log(drink.description());   // Coffee + milk + sugar + caramel
console.log(drink.cost());           // 8.5

5.3. Use cases mạnh

  • Java I/O: BufferedInputStream(GzipInputStream(FileInputStream(file))) — combo decorator.
  • Express middleware: app.use(logger).use(auth).use(cors) — chuỗi handler.
  • HOC trong React: withAuth(withLogging(MyComponent)).
  • Logging/caching/retry quanh function call.

5.4. Decorator vs Inheritance

Inheritance "đúng" với is-a chuyên biệt; Decorator "đúng" khi muốn mix-and-match feature. Decorator có thể thêm/bỏ runtime, inheritance không.

6. Facade

PATTERN №10 — STRUCTURAL

Facade

Cung cấp interface đơn giản và thống nhất cho một subsystem phức tạp.

Vấn đề Subsystem có nhiều class, client phải gọi đúng thứ tự, biết quá nhiều chi tiết. Lợi ích Client viết ít code, ít coupling với subsystem. Đánh đổi Facade có thể trở thành "god object" nếu không kiểm soát.

6.1. Ví dụ

// Subsystem phức tạp với nhiều class
class VideoFile { constructor(public path: string) {} }
class CodecFactory { static extract(file: VideoFile) { return new MPEG4Codec(); } }
class MPEG4Codec {}
class OggCodec {}
class BitrateReader { static read(file: VideoFile, codec: any) { return Buffer.alloc(1024); } }
class AudioMixer { fix(buffer: Buffer) { return buffer; } }

// Facade
class VideoConverter {
  convert(filename: string, format: string): Buffer {
    const file = new VideoFile(filename);
    const sourceCodec = CodecFactory.extract(file);
    const destCodec = format === 'mp4' ? new MPEG4Codec() : new OggCodec();
    const buffer = BitrateReader.read(file, sourceCodec);
    const mixer = new AudioMixer();
    return mixer.fix(buffer);
  }
}

// Client chỉ cần:
const converter = new VideoConverter();
const result = converter.convert('video.avi', 'mp4');

6.2. Use cases hiện đại

  • SDK: AWS SDK s3.upload() ẩn nhiều bước (multipart, retry, signing).
  • Service layer trong app: OrderService.checkout() orchestrate Repository, Mailer, Inventory, Payment.
  • API gateway: 1 endpoint gọi nhiều microservice phía sau.

Facade là pattern tự nhiên — bạn đã viết Facade rất nhiều lần mà không gọi tên.

7. Flyweight

PATTERN №11 — STRUCTURAL

Flyweight

Chia sẻ object để hỗ trợ số lượng cực lớn object nhỏ một cách hiệu quả về memory.

Vấn đề Cần triệu+ object giống nhau (particles, cây 3D forest, character trong text editor). Lợi ích Giảm RAM cực mạnh — chia sẻ phần "intrinsic" giống nhau. Đánh đổi Code phức tạp; tách "intrinsic" (sharable) vs "extrinsic" (per-instance) khó.

7.1. Khái niệm intrinsic vs extrinsic

  • Intrinsic state — không đổi, có thể chia sẻ (vd: sprite của particle, mesh của tree).
  • Extrinsic state — riêng từng instance (vd: vị trí, rotation của particle).

7.2. Implementation

// Heavy data — share được
class TreeType {
  constructor(
    public readonly name: string,
    public readonly color: string,
    public readonly texture: ArrayBuffer,   // 1 MB texture
  ) {}

  draw(x: number, y: number) {
    // Render với x, y (extrinsic) + texture (intrinsic)
  }
}

// Flyweight factory đảm bảo TreeType chia sẻ
class TreeTypeFactory {
  private static cache = new Map<string, TreeType>();

  static get(name: string, color: string, texture: ArrayBuffer): TreeType {
    const key = `${name}|${color}`;
    if (!this.cache.has(key)) {
      this.cache.set(key, new TreeType(name, color, texture));
    }
    return this.cache.get(key)!;
  }
}

// Tree thật — chỉ có position
class Tree {
  constructor(
    public x: number,
    public y: number,
    public type: TreeType,   // shared!
  ) {}

  draw() { this.type.draw(this.x, this.y); }
}

// Forest 1 triệu cây nhưng chỉ 5 loại
const forest: Tree[] = [];
for (let i = 0; i < 1_000_000; i++) {
  const type = TreeTypeFactory.get('Oak', 'green', oakTexture);
  forest.push(new Tree(Math.random() * 1000, Math.random() * 1000, type));
}

// 1M tree × 16 byte (2 number + ref) = 16 MB
// Nếu mỗi tree giữ texture 1MB → 1 TB!

7.3. Use cases thực

  • Game: particle system, terrain, characters.
  • Text editor: glyph cache (1 char "A" có cùng glyph data, khác chỉ position).
  • Web browser: tab cache shared state.
  • String interning trong Java/.NET — string literal giống nhau dùng chung 1 instance.

Trong app web thông thường ít gặp. Khi gặp → giảm RAM 100×.

8. Proxy

PATTERN №12 — STRUCTURAL

Proxy

Cung cấp object đại diện kiểm soát truy cập đến object khác. Cùng interface với object thật.

Vấn đề Cần kiểm soát truy cập: lazy loading, caching, access control, logging, remote. Lợi ích Thêm kiểm soát mà client không biết. SOLID OCP. Đánh đổi Thêm latency từ proxy.

8.1. Các loại Proxy

  • Virtual Proxy — lazy initialization (chỉ khởi tạo khi cần).
  • Protection Proxy — kiểm soát quyền truy cập.
  • Caching Proxy — lưu kết quả gọi.
  • Logging Proxy — ghi log mọi call.
  • Remote Proxy — đại diện object trên server khác (RPC).
  • Smart Reference — count reference, cleanup tự động.

8.2. Virtual Proxy — lazy loading

interface Image {
  display(): void;
}

class HighResImage implements Image {
  constructor(private filename: string) {
    console.log(`Loading ${filename} from disk...`);   // chậm
  }
  display() { console.log(`Displaying ${this.filename}`); }
}

class ImageProxy implements Image {
  private real: HighResImage | null = null;

  constructor(private filename: string) {}

  display() {
    if (!this.real) {
      this.real = new HighResImage(this.filename);   // lazy load
    }
    this.real.display();
  }
}

// Tạo 100 ảnh — không load disk
const images = Array.from({ length: 100 }, (_, i) => new ImageProxy(`img${i}.jpg`));

// Khi user scroll đến ảnh 5 → load
images[5].display();

8.3. Caching Proxy

interface UserApi {
  getUser(id: string): Promise<User>;
}

class HttpUserApi implements UserApi {
  async getUser(id: string) {
    return fetch(`/api/users/${id}`).then(r => r.json());
  }
}

class CachedUserApi implements UserApi {
  private cache = new Map<string, User>();

  constructor(private real: UserApi) {}

  async getUser(id: string) {
    if (this.cache.has(id)) return this.cache.get(id)!;
    const user = await this.real.getUser(id);
    this.cache.set(id, user);
    return user;
  }
}

// Client không biết có cache:
const api: UserApi = new CachedUserApi(new HttpUserApi());

8.4. JavaScript Proxy native

JS có Proxy built-in cho meta-programming:

const target = { name: 'Alice', age: 30 };

const proxy = new Proxy(target, {
  get(obj, prop) {
    console.log(`Reading ${String(prop)}`);
    return obj[prop];
  },
  set(obj, prop, value) {
    if (prop === 'age' && value < 0) throw new Error('age >= 0');
    obj[prop] = value;
    return true;
  },
});

proxy.name;       // logs "Reading name"
proxy.age = -5;   // throws

Vue 3 reactivity, Mobx, Immer dùng Proxy native cho dirty tracking.

8.5. Proxy vs Decorator

Cấu trúc giống — đều wrap object cùng interface. Khác ở ý đồ:

  • Decorator: thêm chức năng (cộng dồn).
  • Proxy: kiểm soát truy cập (gating).

Trong thực tế ranh giới mờ — chọn tên theo intent.

9. So sánh nhanh 7 Structural Patterns

PatternCấu trúcIntent
AdapterWrap với interface khácTương thích interface
Bridge2 hierarchy song song nối qua compositionTách abstraction khỏi implementation
CompositeCây với leaf và composite cùng interfaceXử lý đồng nhất phần và toàn thể
DecoratorWrap cùng interface, cộng dồnThêm chức năng động
Facade1 class che subsystemĐơn giản hóa giao diện
FlyweightChia sẻ object intrinsicTiết kiệm memory
ProxyWrap cùng interface, gatingKiểm soát truy cập
Adapter vs Decorator vs Proxy — phân biệt nhanh Cả 3 đều wrap. Khác ở intent: Adapter = chuyển interface (A → B). Decorator = thêm feature (A → A++). Proxy = kiểm soát/lazy (A → A nhưng có gate).

10. Bài tập

  1. Adapter: bạn có legacy class OldLogger.write(level, msg, time). App mới mong đợi Logger.log(msg). Viết adapter.
  2. Bridge: app vẽ Shape (Circle, Square) trên Canvas/SVG. Implement Bridge với 2 hierarchy. Thêm Triangle và WebGL renderer — đếm số class mới.
  3. Composite: file system với File và Folder. Method find(name) trả về node có tên đó (recursive). print() với indentation.
  4. Decorator: Coffee với 3 topping (milk, sugar, caramel). Implement Decorator. Tạo "Latte triple sugar" và in ra cost + description.
  5. Facade: bạn có 4 class OrderRepository, InventoryService, PaymentGateway, EmailService. Tạo CheckoutFacade với 1 method checkout(order) orchestrate.
  6. Flyweight: app render 100k dot trong canvas, mỗi dot có (x, y, color). Có 5 màu khác nhau. Áp dụng Flyweight để chia sẻ Color object.
  7. Proxy: tạo CachedHttpProxy wrap fetch API. Cache TTL 60 giây. Demo benefit.
  8. Phân biệt với ví dụ: Adapter vs Decorator vs Proxy.

11. Quiz

Quiz cuối Chương 5

Adapter pattern phù hợp khi:

  • Cần thêm feature
  • Cần kiểm soát truy cập
  • Có 2 interface không tương thích cần làm việc cùng nhau (vd: third-party SDK)
  • Object quá lớn
Adapter chuyển interface A thành B. Use case kinh điển: tích hợp third-party (Stripe SDK signature khác PaymentProcessor app), migrate dần legacy class.

Bridge pattern khác Adapter ở:

  • Bridge phức tạp hơn
  • Adapter "fix sau" cho interface đã có; Bridge "thiết kế trước" với 2 chiều biến đổi
  • Bridge không có wrapping
  • Bridge chỉ cho UI
Adapter là phản ứng (interface không khớp đã tồn tại). Bridge là thiết kế chủ động (biết trước có 2 chiều biến đổi: Shape × Renderer, OS × UI). Tách 2 hierarchy nối qua composition để tránh class explosion.

Composite pattern hữu ích khi:

  • Object có 1 field
  • Cần Singleton
  • Class abstract
  • Có cấu trúc cây/đệ quy nơi leaf và composite cần xử lý đồng nhất
File system (file + folder), UI tree (component + container), org chart, math expression — leaf và composite cùng interface giúp client code không phân biệt. Recursive size/print/render tự nhiên.

Decorator vs Inheritance:

  • Decorator linh hoạt — combine feature runtime, tránh class explosion combinatorial
  • Inheritance luôn tốt hơn
  • Decorator chỉ cho Java
  • Hai cái như nhau
3 topping qua inheritance = 8 class (combinatorial). Qua decorator = 3 class, combine tự do. Express middleware, React HOC, Java I/O đều dùng pattern này.

Facade pattern:

  • Cấm truy cập subsystem
  • Force Singleton
  • Cung cấp interface đơn giản che subsystem phức tạp; client viết ít code, ít coupling
  • Yêu cầu inheritance
Facade tự nhiên xuất hiện ở: SDK (s3.upload ẩn multipart/retry), service layer (OrderService.checkout orchestrate), API gateway. Bạn đã viết Facade nhiều lần mà không gọi tên.

Flyweight tách "intrinsic" và "extrinsic":

  • Cả hai đều unique
  • Intrinsic (sharable, không đổi như texture); extrinsic (per-instance như x, y)
  • Intrinsic là private, extrinsic là public
  • Không liên quan
1M cây trong forest game. Intrinsic = mesh + texture (5 loại cây = 5 đối tượng). Extrinsic = (x, y, scale, rotation) per cây. Tiết kiệm RAM 100×+. Game engines, text editors dùng nhiều.

Proxy KHÔNG phù hợp dùng cho:

  • Lazy loading object đắt
  • Caching kết quả method call
  • Access control / authentication check
  • Thêm chức năng mới mở rộng API (đó là Decorator)
Proxy gating/kiểm soát, không thêm feature mới. Decorator để thêm. Cấu trúc giống nhau (wrap cùng interface), khác ở intent. Câu hỏi phỏng vấn cổ điển: "Decorator vs Proxy?".

JavaScript Proxy (native):

  • Cho meta-programming: trap get/set/apply, dùng cho Vue reactivity, MobX, Immer
  • Chỉ cho Node.js
  • Bị deprecated
  • Chỉ wrap function
new Proxy(target, handler) với handler có get/set/apply trap. Vue 3 reactivity, MobX state, Immer immutable updates đều xây trên Proxy. Native JS, không phải pattern Proxy GoF nhưng cùng tên.

Hoàn thành Chương 5 (12/23 patterns). Tiếp theo: Chương 6 — Behavioral Part 1 (6 mẫu) →