1. Module Pattern là gì?

Module Pattern là một mẫu thiết kế được sử dụng để đóng gói dữ liệu và hành vi liên quan vào một đơn vị độc lập. Pattern này giúp tạo ra namespace riêng và giảm thiểu xung đột tên biến trong ứng dụng JavaScript. Module Pattern cũng cung cấp cách để triển khai tính đóng gói (encapsulation) trong JavaScript.

Các đặc điểm chính của Module Pattern:

  • Đóng gói (Encapsulation): Ẩn các chi tiết triển khai
  • Namespace: Tạo không gian tên riêng cho code
  • Tái sử dụng (Reusability): Code có thể được tái sử dụng dễ dàng
  • Loose Coupling: Giảm sự phụ thuộc giữa các module

Structure

Module Pattern có cấu trúc như sau:


  classDiagram
    class Module {
        -privateVariable
        -privateMethod()
        +publicMethod()
        +publicProperty
    }

    note for Module "Module được triển khai bằng IIFE hoặc ES modules<br>Chỉ export các phương thức và thuộc tính public"

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

  • Private Variables/Functions: Biến và hàm chỉ có thể truy cập trong phạm vi module
  • Public API: Các phương thức và thuộc tính được export ra bên ngoài
  • Closure: Sử dụng closure để duy trì trạng thái riêng tư và đóng gói dữ liệu

Trong JavaScript, Module Pattern có thể được triển khai bằng nhiều cách:

  1. IIFE (Immediately Invoked Function Expression): Hàm được khai báo và gọi ngay lập tức
  2. ES Modules: Sử dụng cú pháp import/export của ES6+
  3. CommonJS: Sử dụng module.exports và require() (phổ biến trong Node.js)

2. Triển khai trong JavaScript

2.1 IIFE (Immediately Invoked Function Expression)

const Calculator = (function () {
  // Private variables
  let result = 0;

  // Private functions
  function validate(number) {
    return typeof number === "number" && !isNaN(number);
  }

  // Public interface
  return {
    add(number) {
      if (!validate(number)) {
        throw new Error("Invalid number");
      }
      result += number;
      return this;
    },

    subtract(number) {
      if (!validate(number)) {
        throw new Error("Invalid number");
      }
      result -= number;
      return this;
    },

    multiply(number) {
      if (!validate(number)) {
        throw new Error("Invalid number");
      }
      result *= number;
      return this;
    },

    divide(number) {
      if (!validate(number)) {
        throw new Error("Invalid number");
      }
      if (number === 0) {
        throw new Error("Cannot divide by zero");
      }
      result /= number;
      return this;
    },

    getResult() {
      return result;
    },

    clear() {
      result = 0;
      return this;
    },
  };
})();

// Usage
Calculator.add(5).multiply(2).subtract(3).divide(2);

console.log(Calculator.getResult()); // 3.5
console.log(Calculator.result); // undefined (private)

2.2 Revealing Module Pattern

const UserManager = (function () {
  // Private state
  const users = new Map();

  // Private functions
  function validateUser(user) {
    return (
      user &&
      typeof user === "object" &&
      typeof user.id === "string" &&
      typeof user.name === "string"
    );
  }

  function generateId() {
    return Math.random().toString(36).substr(2, 9);
  }

  // Public functions
  function addUser(name) {
    const id = generateId();
    const user = { id, name };
    users.set(id, user);
    return user;
  }

  function getUser(id) {
    return users.get(id);
  }

  function updateUser(id, updates) {
    const user = users.get(id);
    if (!user) {
      throw new Error("User not found");
    }

    const updatedUser = { ...user, ...updates };
    if (!validateUser(updatedUser)) {
      throw new Error("Invalid user data");
    }

    users.set(id, updatedUser);
    return updatedUser;
  }

  function deleteUser(id) {
    return users.delete(id);
  }

  function getAllUsers() {
    return Array.from(users.values());
  }

  // Reveal public interface
  return {
    add: addUser,
    get: getUser,
    update: updateUser,
    delete: deleteUser,
    getAll: getAllUsers,
  };
})();

// Usage
const user1 = UserManager.add("John Doe");
const user2 = UserManager.add("Jane Smith");

console.log(UserManager.getAll());
// [{id: "abc123", name: "John Doe"}, {id: "def456", name: "Jane Smith"}]

UserManager.update(user1.id, { name: "John Smith" });
console.log(UserManager.get(user1.id));
// {id: "abc123", name: "John Smith"}

UserManager.delete(user2.id);
console.log(UserManager.getAll());
// [{id: "abc123", name: "John Smith"}]

3. Triển khai trong TypeScript

TypeScript cung cấp các tính năng module tích hợp, giúp việc triển khai Module Pattern trở nên rõ ràng và an toàn hơn:

// types.ts
export interface Task {
  id: string;
  title: string;
  completed: boolean;
  createdAt: Date;
  completedAt?: Date;
}

export interface TaskManager {
  addTask(title: string): Task;
  getTask(id: string): Task | undefined;
  updateTask(id: string, updates: Partial<Task>): Task;
  deleteTask(id: string): boolean;
  getAllTasks(): Task[];
  getCompletedTasks(): Task[];
  getPendingTasks(): Task[];
}

// task-manager.ts
import { Task, TaskManager } from "./types";

class TaskManagerImpl implements TaskManager {
  private tasks: Map<string, Task> = new Map();

  private generateId(): string {
    return Math.random().toString(36).substr(2, 9);
  }

  addTask(title: string): Task {
    const task: Task = {
      id: this.generateId(),
      title,
      completed: false,
      createdAt: new Date(),
    };

    this.tasks.set(task.id, task);
    return task;
  }

  getTask(id: string): Task | undefined {
    return this.tasks.get(id);
  }

  updateTask(id: string, updates: Partial<Task>): Task {
    const task = this.tasks.get(id);
    if (!task) {
      throw new Error("Task not found");
    }

    const updatedTask = { ...task, ...updates };

    if (updates.completed && !task.completed) {
      updatedTask.completedAt = new Date();
    } else if (updates.completed === false) {
      delete updatedTask.completedAt;
    }

    this.tasks.set(id, updatedTask);
    return updatedTask;
  }

  deleteTask(id: string): boolean {
    return this.tasks.delete(id);
  }

  getAllTasks(): Task[] {
    return Array.from(this.tasks.values());
  }

  getCompletedTasks(): Task[] {
    return this.getAllTasks().filter((task) => task.completed);
  }

  getPendingTasks(): Task[] {
    return this.getAllTasks().filter((task) => !task.completed);
  }
}

// Singleton instance
export const TaskManager = new TaskManagerImpl();

// Usage
import { TaskManager } from "./task-manager";

const task1 = TaskManager.addTask("Learn TypeScript");
const task2 = TaskManager.addTask("Learn Design Patterns");

console.log(TaskManager.getAllTasks());
// [{id: "abc123", title: "Learn TypeScript", ...}, {id: "def456", title: "Learn Design Patterns", ...}]

TaskManager.updateTask(task1.id, { completed: true });
console.log(TaskManager.getCompletedTasks());
// [{id: "abc123", title: "Learn TypeScript", completed: true, ...}]

console.log(TaskManager.getPendingTasks());
// [{id: "def456", title: "Learn Design Patterns", completed: false, ...}]

4. Ví dụ thực tế: Logger Module

Hãy xem xét một ví dụ thực tế về việc triển khai một logger module:

// logger.ts
export enum LogLevel {
  DEBUG = "DEBUG",
  INFO = "INFO",
  WARN = "WARN",
  ERROR = "ERROR",
}

export interface LogEntry {
  level: LogLevel;
  message: string;
  timestamp: Date;
  context?: Record<string, any>;
}

export interface LogTransport {
  log(entry: LogEntry): void;
}

class ConsoleTransport implements LogTransport {
  private colors = {
    [LogLevel.DEBUG]: "\x1b[36m", // Cyan
    [LogLevel.INFO]: "\x1b[32m", // Green
    [LogLevel.WARN]: "\x1b[33m", // Yellow
    [LogLevel.ERROR]: "\x1b[31m", // Red
    reset: "\x1b[0m",
  };

  log(entry: LogEntry): void {
    const color = this.colors[entry.level];
    const reset = this.colors.reset;
    const timestamp = entry.timestamp.toISOString();
    const context = entry.context ? JSON.stringify(entry.context) : "";

    console.log(
      `${color}[${entry.level}]${reset} ${timestamp} - ${entry.message} ${context}`
    );
  }
}

class FileTransport implements LogTransport {
  private filePath: string;

  constructor(filePath: string) {
    this.filePath = filePath;
  }

  log(entry: LogEntry): void {
    const timestamp = entry.timestamp.toISOString();
    const context = entry.context ? JSON.stringify(entry.context) : "";
    const logLine = `[${entry.level}] ${timestamp} - ${entry.message} ${context}\n`;

    // In a real implementation, we would write to file asynchronously
    // For demonstration purposes, we'll just console.log
    console.log(`Writing to ${this.filePath}: ${logLine}`);
  }
}

class Logger {
  private static instance: Logger;
  private transports: LogTransport[] = [];
  private minLevel: LogLevel = LogLevel.INFO;

  private constructor() {
    // Private constructor to enforce singleton
  }

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

  addTransport(transport: LogTransport): void {
    this.transports.push(transport);
  }

  setMinLevel(level: LogLevel): void {
    this.minLevel = level;
  }

  private shouldLog(level: LogLevel): boolean {
    const levels = Object.values(LogLevel);
    return levels.indexOf(level) >= levels.indexOf(this.minLevel);
  }

  private log(
    level: LogLevel,
    message: string,
    context?: Record<string, any>
  ): void {
    if (!this.shouldLog(level)) {
      return;
    }

    const entry: LogEntry = {
      level,
      message,
      timestamp: new Date(),
      context,
    };

    this.transports.forEach((transport) => transport.log(entry));
  }

  debug(message: string, context?: Record<string, any>): void {
    this.log(LogLevel.DEBUG, message, context);
  }

  info(message: string, context?: Record<string, any>): void {
    this.log(LogLevel.INFO, message, context);
  }

  warn(message: string, context?: Record<string, any>): void {
    this.log(LogLevel.WARN, message, context);
  }

  error(message: string, context?: Record<string, any>): void {
    this.log(LogLevel.ERROR, message, context);
  }
}

// Export singleton instance
export const logger = Logger.getInstance();

// Initialize with default transports
logger.addTransport(new ConsoleTransport());
logger.addTransport(new FileTransport("app.log"));

// Usage
import { logger, LogLevel } from "./logger";

// Set minimum log level
logger.setMinLevel(LogLevel.DEBUG);

// Log messages with different levels
logger.debug("Debugging information", { module: "auth", userId: 123 });
logger.info("User logged in successfully", { userId: 123 });
logger.warn("Rate limit approaching", { currentRate: 95 });
logger.error("Failed to process payment", {
  orderId: 456,
  error: "Insufficient funds",
});

// Output:
// [DEBUG] 2024-04-23T10:00:00.000Z - Debugging information {"module":"auth","userId":123}
// [INFO] 2024-04-23T10:00:00.000Z - User logged in successfully {"userId":123}
// [WARN] 2024-04-23T10:00:00.000Z - Rate limit approaching {"currentRate":95}
// [ERROR] 2024-04-23T10:00:00.000Z - Failed to process payment {"orderId":456,"error":"Insufficient funds"}

5. Khi nào nên sử dụng Module Pattern

Module Pattern phù hợp trong các tình huống sau:

  1. Khi cần đóng gói logic liên quan
  2. Khi muốn tránh xung đột tên biến
  3. Khi cần tạo API công khai cho module
  4. Khi muốn ẩn chi tiết triển khai
  5. Khi cần tổ chức code thành các đơn vị độc lập

Ví dụ thực tế:

  • Quản lý trạng thái ứng dụng
  • Xử lý logging và debugging
  • Quản lý cấu hình
  • Xử lý authentication
  • Quản lý cache

6. So sánh với các Pattern khác

So sánh với Singleton Pattern

Module PatternSingleton Pattern
Tập trung vào tổ chức codeTập trung vào instance duy nhất
Có thể có nhiều instanceChỉ có một instance
Linh hoạt trong việc exportStrict về việc truy cập
Dễ test hơnKhó test hơn

So sánh với Namespace Pattern

Module PatternNamespace Pattern
Private và public membersChỉ có public members
Closure để bảo vệ stateGlobal object
Modern JavaScriptLegacy approach
Better encapsulationLimited encapsulation

7. Ưu điểm và nhược điểm

Ưu điểm:

  • Đóng gói tốt cho dữ liệu private
  • Tổ chức code rõ ràng và có cấu trúc
  • Tránh xung đột tên biến
  • Dễ bảo trì và mở rộng
  • Tái sử dụng code hiệu quả

Nhược điểm:

  • Khó truy cập private members khi cần
  • Khó mở rộng functionality sau khi định nghĩa
  • Memory usage cao hơn với closure
  • Khó debug private state
  • Không hỗ trợ circular dependencies tốt

8. Kết luận

Module Pattern là một công cụ quan trọng trong JavaScript và TypeScript để tổ chức code thành các đơn vị độc lập và có thể tái sử dụng. Pattern này đặc biệt hữu ích trong việc triển khai tính đóng gói và tạo API rõ ràng cho các module.

Khi quyết định sử dụng Module Pattern, hãy cân nhắc yêu cầu về tính đóng gói và tổ chức code. Đối với các ứng dụng hiện đại, bạn có thể kết hợp Module Pattern với các tính năng module của ES6+ và TypeScript để có được lợi ích tốt nhất của cả hai thế giới.