TypeScript Advanced – Phần 4 – Design Patterns – Behavioral Patterns

🧠 Tổng quan

Phần này tập trung vào các Behavioral Design Patterns giúp quản lý trạng thái và hành vi của đối tượng theo thời gian – bằng cách tách biệt logic trạng thái ra khỏi hành vi của đối tượng.

📌 Các mẫu thiết kế sẽ được đề cập:

  • Iterator
  • Memento
  • State
  • Template Method
  • Visitor

🔁 Iterator Pattern – Mẫu thiết kế Lặp

🎯 Mục đích

Cung cấp một cách tiếp cận thống nhất để duyệt qua các phần tử của một collection mà không cần biết chi tiết cấu trúc bên trong của nó.


🔍 Khái niệm chính

📦 1. Trừu tượng hóa việc duyệt

  • Logic duyệt tách biệt hoàn toàn với cấu trúc của collection.

🧩 2. Giao diện thống nhất

  • Cho phép viết code chung để làm việc với các loại collection khác nhau (mảng, cây, đồ thị…).

🔐 3. Giấu chi tiết bên trong

  • Collection có thể thay đổi cách triển khai mà không ảnh hưởng tới code client.

📌 Ví dụ: Bạn có danh sách phim yêu thích nằm ở nhiều thư mục khác nhau, nhưng trong ứng dụng UI, bạn chỉ cần bấm “next” để xem từng phim một, không cần biết chính xác file đó ở đâu trên ổ cứng.


📌 Khi nào nên dùng Iterator Pattern?

Tình huống sử dụngMô tả
🌲 Cấu trúc phức tạpVí dụ như cây, đồ thị, hoặc collection tùy chỉnh
🔁 Nhiều cách duyệtNhư in-order, pre-order, post-order trong cây
🔄 Tách biệt client & collectionClient không cần biết chi tiết collection
🧵 Duyệt song songNhiều con trỏ duyệt cùng lúc
💤 Duyệt lười (Lazy Evaluation)Chỉ sinh phần tử khi cần
🤝 Giao diện thống nhấtCode có thể hoạt động với mọi collection

📐 Cấu trúc UML

Các thành phần chính:

  • Iterator: Giao diện định nghĩa hasNext()next()
  • Aggregate: Giao diện định nghĩa createIterator()
  • ConcreteIterator: Hiện thực việc duyệt, lưu trữ vị trí hiện tại
  • ConcreteAggregate: Collection cụ thể, có thể tạo ra Iterator
  • Client: Gọi createIterator() và dùng next() để duyệt

💡 Lợi ích: Client chỉ cần quan tâm đến interface, không phụ thuộc vào chi tiết nội bộ collection.


👨‍💻 Cài đặt cụ thể

🧱 Khởi tạo các class cơ bản:

class Song {
  constructor(public title: string, public artist: string, public duration: number) {}

  toString(): string {
    return `${this.title} by ${this.artist} (${this.duration} seconds)`;
  }
}

interface Iterator<T> {
  hasNext(): boolean;
  next(): T | null;
}

interface Playlist {
  createIterator(): Iterator<Song>;
}

🔄 Triển khai Iterator:

class PlaylistIterator implements Iterator<Song> {
  private currentIndex = 0;
  constructor(private playlist: Song[]) {}

  hasNext(): boolean {
    return this.currentIndex < this.playlist.length;
  }

  next(): Song | null {
    if (this.hasNext()) {
      return this.playlist[this.currentIndex++] || null;
    }
    return null;
  }

  private loadMoreSongs(): void {
    // Có thể load thêm từ API
  }
}

class MusicPlaylist implements Playlist {
  private songs: Song[] = [];

  addSong(song: Song): void {
    this.songs.push(song);
  }

  createIterator(): Iterator<Song> {
    return new PlaylistIterator(this.songs);
  }
}

▶️ Sử dụng:

const myPlaylist = new MusicPlaylist();
myPlaylist.addSong(new Song("Bohemian Rhapsody", "Queen", 354));
myPlaylist.addSong(new Song("Stairway to Heaven", "Led Zeppelin", 482));
myPlaylist.addSong(new Song("Imagine", "John Lennon", 183));

const iterator = myPlaylist.createIterator();
console.log("My Playlist:");
while (iterator.hasNext()) {
  const song = iterator.next();
  if (song) {
    console.log(song.toString());
  }
}

⚙️ Phiên bản async – Iterator bất đồng bộ

class AsyncSongIterator {
  private currentIndex = 0;
  private songs: string[] = [];

  private async fetchSongs(): Promise<string[]> {
    await new Promise((resolve) => setTimeout(resolve, 1000));
    return ["Song 1", "Song 2", "Song 3"];
  }

  async next(): Promise<{ value: string | null; done: boolean }> {
    if (this.currentIndex === 0) {
      this.songs = await this.fetchSongs();
    }

    if (this.currentIndex < this.songs.length) {
      return { value: this.songs[this.currentIndex++], done: false };
    } else {
      return { value: null, done: true };
    }
  }

  [Symbol.asyncIterator]() {
    return this;
  }
}

✅ Sử dụng với for await:

const songIterator = new AsyncSongIterator();
for await (const song of songIterator) {
  console.log(song);
}

🧪 Kiểm thử

📋 Những gì cần test:

  • Gọi next()hasNext() trả về đúng phần tử tiếp theo
  • createIterator() phải trả về đúng thể hiện ConcreteIterator

📦 Chạy test:

npm run test --iterator

⚠️ Hạn chế

Điểm hạn chếGiải thích
🧩 Không cần thiết cho collection đơn giảnDùng mảng thông thường có thể đủ
🛠️ Tăng số lượng classCần bảo trì nhiều file hơn
📚 Quá mức cho collection chỉ đọcCó thể dùng trực tiếp array hoặc list là đủ

🌐 Ứng dụng thực tế – RxJS

const observable = new Observable<number>((observer) => {
  observer.next(1);
  observer.next(2);
  observer.next(3);
  observer.complete();
});
observable
  .map((x) => x * 10)
  .filter((x) => x > 10)
  .subscribe((value) => console.log(value));

✅ RxJS sử dụng Iterator để quản lý các dòng dữ liệu bất đồng bộ, sử dụng for...of hoặc for await...of nhờ vào các thuộc tính Symbol.iteratorSymbol.asyncIterator.

🧠 Memento Pattern – Mẫu thiết kế Ghi nhớ

🎯 Mục đích

Cho phép lưu lại snapshot (bản chụp trạng thái) của một đối tượng tại một thời điểm cụ thể mà không phá vỡ tính đóng gói (encapsulation).


🔍 Cấu trúc chính

Thành phầnVai trò
🧠 OriginatorĐối tượng có trạng thái cần lưu
🧾 MementoChứa trạng thái được lưu lại (snapshot)
💼 CaretakerQuản lý các memento nhưng không thao tác trực tiếp nội dung của chúng

📌 Ưu điểm:
Tách biệt việc lưu trữ và khôi phục trạng thái khỏi logic xử lý, vẫn đảm bảo tính đóng gói (encapsulation).


Khi nào dùng Memento Pattern?

Tình huốngMô tả
🕰️ Lưu/khôi phục trạng tháiUndo/Redo, checkpoint, quay về trạng thái trước đó
🛡️ Bảo toàn tính đóng góiTrạng thái không bị truy cập trực tiếp từ bên ngoài
🧩 Trạng thái phức tạpKhi logic nội bộ cần checkpoint thường xuyên (biên tập văn bản, game, mô phỏng…)

📐 Sơ đồ UML

Các thành phần:

  • AppState: Giao diện cho trạng thái
  • Originator: Tạo Memento từ trạng thái hiện tại
  • Memento: Lưu giữ trạng thái
  • Caretaker: Quản lý danh sách các memento

Cách hoạt động:

  • Originator.save() → trả về Memento
  • Caretaker lưu lại Memento
  • Caretaker.restore() → truyền Memento ngược lại cho Originator

👨‍💻 Cài đặt

interface TextEditorState {
  content: string;
  cursorPosition: number;
}

class EditorMemento {
  constructor(private readonly state: TextEditorState) {}
  getState(): TextEditorState {
    return this.state;
  }
}

📝 Lưu trạng thái nội dung + vị trí con trỏ.


▶️ Sử dụng trong thực tế

const editor = new TextEditor();
const caretaker = new EditorCaretaker(editor);

editor.type("Hello, ");
caretaker.save();

editor.type("world!");
caretaker.save();

console.log(editor.getContent()); // Hello, world!

caretaker.undo();
console.log(editor.getContent()); // Hello,

caretaker.redo();
console.log(editor.getContent()); // Hello, world!

🧪 Kiểm thử

🔍 Các trường hợp cần test:

  • Gọi save() lưu đúng trạng thái
  • Gọi undo()redo() khôi phục đúng trạng thái
  • Không thay đổi nội dung Memento từ Caretaker

📦 Lệnh chạy test:

npm run test --memento

⚠️ Hạn chế

Hạn chếGiải thích
⚙️ Tăng phức tạpThêm nhiều lớp và quan hệ
🧠 Ngốn bộ nhớLưu nhiều snapshot có thể gây tốn RAM
🐢 Ảnh hưởng hiệu năngSnapshot/phục hồi liên tục với trạng thái phức tạp có thể làm chậm ứng dụng

🌐 Ứng dụng thực tế – Visual Studio Code

Thành phầnVai trò
ITextSnapshot🧾 Memento: snapshot bất biến của văn bản
TextModel🧠 Originator: tạo snapshot
EditStack💼 Caretaker: lưu các bước chỉnh sửa

🧠 State Pattern – Mẫu thiết kế Trạng thái

🎯 Mục đích

Cho phép đối tượng thay đổi hành vi khi trạng thái nội bộ thay đổi, không cần dùng nhiều câu lệnh if-else hoặc switch-case.


🔍 Cấu trúc chính

Thành phầnVai trò
🧠 ContextGiữ trạng thái hiện tại và ủy quyền xử lý
🧾 StateGiao diện khai báo các hành vi
🧩 ConcreteStateA/B/…Thể hiện hành vi cụ thể theo từng trạng thái

📌 Ý tưởng: Đối tượng Context không tự xử lý hành vi theo trạng thái, mà giao cho State xử lý.


Khi nào dùng State Pattern?

Trường hợpLợi ích
🤹 Hành vi thay đổi theo trạng tháiTránh lặp if/else lớn
🔁 Thay đổi trạng thái linh hoạtDễ quản lý, kiểm soát logic chuyển trạng thái
🧽 Giảm lặp mãTách biệt xử lý cho từng trạng thái
🔧 Thay đổi lúc runtimeCó thể hoán đổi trạng thái trong khi chạy
🧱 Mở rộng dễ dàngThêm trạng thái mới mà không sửa code Context

📐 Sơ đồ UML

  • Context sử dụng State (composition)
  • State là interface
  • ConcreteStates hiện thực State, định nghĩa hành vi cụ thể
  • Context.changeState() → đổi trạng thái hiện tại

👨‍💻 Cài đặt mẫu

interface State {
  handle(): void;
}

class Context {
  private state: State;
  constructor(initialState: State) {
    this.state = initialState;
  }

  request(): void {
    this.state.handle();
  }

  changeState(newState: State): void {
    if (this.state instanceof ConcreteStateA && !(newState instanceof ConcreteStateB)) {
      throw new Error("Invalid state transition");
    }
    this.state = newState;
  }
}

class ConcreteStateA implements State {
  handle(): void {
    console.log("Handling request in ConcreteStateA");
  }
}

class ConcreteStateB implements State {
  handle(): void {
    console.log("Handling request in ConcreteStateB");
  }
}

▶️ Dùng thử

const context = new Context(new ConcreteStateA());
context.request(); // => Handling request in ConcreteStateA
context.changeState(new ConcreteStateB());
context.request(); // => Handling request in ConcreteStateB

🧪 Kiểm thử

📋 Các test case cần có:

  • Kiểm tra đúng hành vi của từng ConcreteState
  • Đảm bảo changeState() chuyển đổi đúng
  • Kiểm tra lỗi khi chuyển trạng thái không hợp lệ

📦 Lệnh chạy test:

bashCopyEditnpm run test --state

⚠️ Hạn chế

Hạn chếDiễn giải
⚙️ Tăng lớpCó thể tạo ra nhiều class nếu trạng thái không khác nhau nhiều
🧩 Có thể over-engineerTrường hợp đơn giản thì dùng enum/switch-case có thể gọn hơn
🪤 Khó định nghĩa rõ ràng trạng tháiNếu sự khác biệt nhỏ thì dễ tạo ra nhiều mã boilerplate

🌐 Ứng dụng thực tế – React Router

Thành phầnVai trò
RouteStateState interface cho các loại khớp đường dẫn
RouteContext, chọn state phù hợp theo path
ConcreteStatesDùng cho so khớp đường dẫn dạng string hoặc regex
interface RouteState {
  match(path: string): boolean;
  render(component: React.ComponentType): React.ReactElement;
}
class Route {
  private state: RouteState;
  constructor(path: string | RegExp) {
    // Chọn state tương ứng
  }

  matches(path: string): boolean {
    return this.state.match(path);
  }
}

🧱 Template Method Pattern – Mẫu Thiết kế Phương Thức Mẫu

🎯 Mục đích

Xác định một khuôn mẫu (template) cho thuật toán trong lớp cơ sở trừu tượng, cho phép lớp con tùy biến một vài bước mà không làm thay đổi cấu trúc tổng thể.


🔍 Thành phần chính

Thành phầnVai trò
📄 Abstract base classĐịnh nghĩa thuật toán tổng thể qua phương thức template
🧩 Template MethodKhung xương của thuật toán (gọi các bước)
🧱 Concrete operationsCác bước được cài sẵn trong lớp cơ sở
🧬 Abstract operationsCác bước để lớp con ghi đè
🔘 Hook methodsBước tùy chọn, có thể ghi đè hoặc không

📌 Ý tưởng:
Định nghĩa một thuật toán chuẩn, và để lớp con lấp vào những chỗ còn trống.


Khi nào dùng Template Method?

Tình huốngLợi ích
📋 Có thuật toán có cấu trúc cố định nhưng vài bước thay đổiVí dụ: xử lý tài liệu PDF, Word, HTML khác nhau nhưng cùng luồng
🧹 Loại bỏ trùng lặp mãCác bước giống nhau được định nghĩa 1 lần
🧰 Xây framework mở rộngCho phép người dùng framework tùy chỉnh hành vi từng bước

📐 Sơ đồ UML

  • DocumentProcessor: lớp cơ sở, chứa processDocument() là template
  • openDocument(), saveDocument(): bước cố định
  • extractContent(), analyzeContent(): bước trừu tượng
  • PDFProcessor, WordProcessor: lớp con triển khai các bước trừu tượng

👨‍💻 Cài đặt cụ thể

abstract class DocumentProcessor {
  public processDocument(): void {
    this.openDocument();
    this.extractContent();
    this.analyzeContent();
    this.saveDocument();
  }
  protected openDocument(): void {
    console.log("Opening document");
  }
  protected abstract extractContent(): void;
  protected abstract analyzeContent(): void;
  protected saveDocument(): void {
    console.log("Saving processed document");
  }
}

class PDFProcessor extends DocumentProcessor {
  protected extractContent(): void {
    console.log("Extracting content from PDF");
  }
  protected analyzeContent(): void {
    console.log("Analyzing PDF content");
  }
}

▶️ Sử dụng

tsCopyEditconst pdfDoc: DocumentProcessor = new PDFProcessor();
pdfDoc.processDocument();

🧪 Kiểm thử

📋 Kiểm tra:

  • Thứ tự gọi các bước
  • Lớp con thực thi đúng bước riêng
  • Không gọi nhầm hook hoặc abstract method

📦 Lệnh test:

npm run test --template-method

⚠️ Hạn chế

Hạn chếGiải thích
🔧 Khó nâng cấp nếu đã phân phốiViệc loại bỏ/đổi thứ tự bước ảnh hưởng đến subclass
🎯 Giới hạn mở rộng linh hoạtNên giữ số bước tối thiểu để tránh phá vỡ subclass

🌐 Ứng dụng thực tế

🔷 React Class Component

class WelcomeHome extends React.Component<{ name: string }> {
  componentDidMount() {
    console.log("Just loaded");
  }
  componentWillUnmount() {
    console.log("Goodbye!");
  }
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}
  • render() là bắt buộc (template)
  • Các hook như componentDidMount là tùy chọn

🟦 Angular Life Cycle

export class UserProfileComponent implements OnInit {
  userData: string;
  ngOnInit(): void {
    this.initialize();
    this.loadData();
  }
  private initialize(): void {
    console.log('Initializing User Profile');
  }
  private loadData(): void {
    this.userData = 'User data loaded from API';
  }
}

ngOnInit() như một template method, gọi các bước nhỏ tùy chỉnh

👥 Visitor Pattern – Mẫu Thiết kế Khách Đến Nhà

🎯 Mục đích

Cho phép tách riêng các thao tác (logic) áp dụng cho một tập hợp các đối tượng không cần thay đổi lớp đối tượng đó.


🔍 Thành phần chính

Thành phầnVai trò
🧾 VisitorGiao diện định nghĩa các phương thức visit<Type>
📦 ConcreteVisitorHiện thực các thao tác cụ thể cho từng loại đối tượng
🧱 Element (AcceptsVisitor)Giao diện cho phép chấp nhận visitor
📄 ConcreteElementGọi đúng visitor.visit(this) theo loại

Khi nào dùng Visitor Pattern?

Trường hợpLợi ích
📊 Cần thu thập dữ liệu từ nhiều đối tượng dạng cây/phức tạpTruy xuất sạch sẽ qua visitor
🛠️ Cần thêm logic mới mà không sửa class gốcDễ bảo trì, mở rộng
🔬 Nhiều thao tác khác nhau cho cùng object treeChỉ cần tạo Visitor mới thay vì thay object

📐 Sơ đồ UML

  • DocumentVisitor: Interface khai báo các visit<Type>()
  • PdfDocument, WordDocument: Gọi accept(visitor)
  • CompositeDocument: Chứa nhiều document con, gọi accept theo đệ quy
  • DocumentProcessingVisitor: Xử lý từng loại cụ thể

👨‍💻 Cài đặt ví dụ

export interface DocumentVisitor {
  visitPdfDocument(pdf: PdfDocument): void;
  visitWordDocument(word: WordDocument): void;
  visitCompositeDocument(doc: CompositeDocument): void;
}

export interface AcceptsVisitor {
  accept(visitor: DocumentVisitor): void;
}

export class PdfDocument implements AcceptsVisitor {
  accept(visitor: DocumentVisitor): void {
    visitor.visitPdfDocument(this);
  }
}
export class CompositeDocument implements AcceptsVisitor {
  private documents: AcceptsVisitor[] = [];
  addDocument(doc: AcceptsVisitor) {
    this.documents.push(doc);
  }
  accept(visitor: DocumentVisitor) {
    for (let doc of this.documents) {
      doc.accept(visitor);
    }
    visitor.visitCompositeDocument(this);
  }
}

▶️ Sử dụng

const composite = new CompositeDocument();
composite.addDocument(new PdfDocument());
composite.addDocument(new WordDocument());

const visitor = new DocumentProcessingVisitor();
composite.accept(visitor);

🧪 Kiểm thử

📋 Test cần đảm bảo:

  • Đúng trình tự gọi visit<Type>()
  • Đúng loại visitor được gọi
  • Tập hợp composite thực hiện gọi đầy đủ

📦 Lệnh test:

bashCopyEditnpm run test --visitor

⚠️ Hạn chế

Hạn chếGiải thích
🔗 Ràng buộc chặtThêm 1 loại đối tượng mới → phải sửa toàn bộ visitor
❌ Dễ gọi nhầm visitSai phương thức visit<Type> sẽ lỗi
🔒 Giới hạn truy cậpVisitor chỉ thấy được public, không thấy được trạng thái nội tại

🌐 Ứng dụng thực tế

📌 Static Code Analysis Tool

interface ASTVisitor {
  visitFunctionDeclaration(node: FunctionDeclaration): void;
}
class ComplexityAnalyzer implements ASTVisitor {
  private complexity = 0;
  visitFunctionDeclaration(node: FunctionDeclaration) {
    this.complexity += 1;
  }
}

📂 Được dùng trong:

📚 Tổng kết – Quản lý Trạng thái & Hành Vi

PatternMục tiêu chínhTình huống sử dụng
🔁 IteratorDuyệt qua collectionCấu trúc phức tạp, lazy loading
🧠 MementoLưu/khôi phục trạng tháiUndo/Redo, checkpoint
🔄 StateThay đổi hành vi theo trạng tháiFSM, logic UI phức tạp
🧰 Template MethodTái sử dụng khung thuật toánXử lý tài liệu, hook life cycle
👥 VisitorThêm thao tác cho nhiều đối tượng mà không sửa lớp gốcAST, phân tích mã, composite tree

Để lại một bình luận