9 min read

Design Patterns: Bí Kíp Luyện Code Của Dân Lập Trình

Design Patterns - “Bí Kíp” Không Thể Thiếu Cho Dân Code

Trong thế giới lập trình, Design Patterns (Mẫu Thiết Kế) được xem như những “bí kíp võ công”, giúp các lập trình viên giải quyết các vấn đề thường gặp trong thiết kế phần mềm một cách hiệu quả và tối ưu. Chúng là các giải pháp đã được “thử lửa” qua thời gian, được đúc kết từ kinh nghiệm của các “cao thủ” đi trước. Bài viết này sẽ cùng bạn khám phá thế giới Design Patterns trong JavaScript, đi sâu vào một số pattern phổ biến và cách áp dụng chúng vào thực tế.

Design Patterns Là Gì?

Hiểu một cách đơn giản, Design Patterns là các giải pháp tổng quát, có thể tái sử dụng cho các vấn đề thường gặp trong thiết kế phần mềm. Chúng không phải là các đoạn code “copy-paste”, mà là các “khuôn mẫu” (template) hướng dẫn cách giải quyết vấn đề, cách tổ chức code sao cho “sạch, đẹp, dễ bảo trì”.

Lợi ích của việc sử dụng Design Patterns:

  • Tăng tốc độ phát triển: Giúp bạn không phải “phát minh lại bánh xe”, tiết kiệm thời gian và công sức.
  • Cải thiện chất lượng code: Code “sạch” hơn, dễ đọc, dễ hiểu, dễ bảo trì và mở rộng.
  • Tăng khả năng tái sử dụng: Các patterns có thể được áp dụng linh hoạt trong nhiều dự án khác nhau.
  • Dễ dàng giao tiếp: Tạo ra “ngôn ngữ chung” cho các lập trình viên, giúp việc trao đổi, thảo luận về thiết kế hiệu quả hơn.

Phân Loại Design Patterns

Design Patterns thường được chia thành ba nhóm chính:

  • Creational Patterns (Nhóm Khởi Tạo): Liên quan đến quá trình tạo (create) đối tượng, giúp việc khởi tạo đối tượng trở nên linh hoạt và hiệu quả hơn.
  • Structural Patterns (Nhóm Cấu Trúc): Tập trung vào cách tổ chức các class và object để tạo thành các cấu trúc lớn hơn, phức tạp hơn, nhưng vẫn đảm bảo tính linh hoạt và hiệu quả.
  • Behavioral Patterns (Nhóm Hành Vi): Quan tâm đến việc giao tiếp và phân chia trách nhiệm giữa các đối tượng.

Một Số Design Patterns Phổ Biến Trong JavaScript (Kèm Ví Dụ)

Dưới đây là một số Design Patterns thông dụng, kèm theo ví dụ minh họa bằng JavaScript:

1. Creational Patterns:

a) Singleton Pattern:

  • Mục đích: Đảm bảo rằng một class chỉ có duy nhất một instance (thể hiện), và cung cấp một điểm truy cập toàn cục đến instance đó.
  • Ví dụ: Quản lý kết nối database, cấu hình ứng dụng.
class Database {
  constructor(connectionString) {
    if (Database.instance) {
      return Database.instance;
    }
    this.connectionString = connectionString;
    Database.instance = this;
  }

  connect() {
    console.log(`Connecting to database: ${this.connectionString}`);
    // Code to connect to the database
  }
}

const db1 = new Database("mongodb://localhost:27017/mydb");
db1.connect(); // Output: Connecting to database: mongodb://localhost:27017/mydb

const db2 = new Database("another-connection-string"); // Returns the same instance as db1
db2.connect(); // Output: Connecting to database: mongodb://localhost:27017/mydb

console.log(db1 === db2); // Output: true
  • Giải thích: Class Database sử dụng một thuộc tính static instance để lưu trữ instance duy nhất. Constructor kiểm tra xem instance đã tồn tại hay chưa, nếu có thì trả về instance đó, nếu không thì tạo mới.

b) Factory Pattern:

  • Mục đích: Định nghĩa một interface để tạo đối tượng, nhưng để cho các lớp con quyết định lớp nào sẽ được khởi tạo. Factory Pattern cho phép một lớp ủy quyền việc khởi tạo đối tượng cho các lớp con.
  • Ví dụ: Tạo các đối tượng “Product” khác nhau (ví dụ: Car, Bike, Truck) dựa trên một tham số đầu vào.
class Car {
  constructor(options) {
    this.doors = options.doors || 4;
    this.state = options.state || "brand new";
    this.color = options.color || "silver";
  }
}

class Truck {
  constructor(options) {
    this.state = options.state || "used";
    this.wheelSize = options.wheelSize || "large";
    this.color = options.color || "blue";
  }
}

class VehicleFactory {
  createVehicle(options) {
    let vehicleClass;
    if (options.vehicleType === "car") {
      vehicleClass = Car;
    } else if (options.vehicleType === "truck") {
      vehicleClass = Truck;
    }
    return new vehicleClass(options);
  }
}

const factory = new VehicleFactory();
const car = factory.createVehicle({
  vehicleType: "car",
  color: "red",
  doors: 4,
});

const truck = factory.createVehicle({
  vehicleType: "truck",
  color: "black",
  wheelSize: "medium",
});

console.log(car); // Output: Car { doors: 4, state: 'brand new', color: 'red' }
console.log(truck); // Output: Truck { state: 'used', wheelSize: 'medium', color: 'black' }
  • Giải thích: Class VehicleFactory có phương thức createVehicle chịu trách nhiệm tạo các đối tượng Car hoặc Truck dựa trên vehicleType.

2. Structural Patterns:

a) Module Pattern:

  • Mục đích: Ẩn giấu các chi tiết triển khai bên trong (private members) và chỉ public ra ngoài những gì cần thiết, giúp tổ chức code tốt hơn, tránh xung đột tên biến/hàm.
  • Ví dụ: Tạo một module quản lý giỏ hàng.
const ShoppingCart = (function () {
  let items = []; // Private variable

  function addItem(item) {
    items.push(item);
  }

  function getTotal() {
    return items.reduce((total, item) => total + item.price, 0);
  }

  return {
    addItem: addItem, // Public method
    getTotal: getTotal, // Public method
  };
})();

ShoppingCart.addItem({ name: "Product 1", price: 10 });
ShoppingCart.addItem({ name: "Product 2", price: 20 });
console.log(ShoppingCart.getTotal()); // Output: 30
  • Giải thích: Sử dụng IIFE (Immediately Invoked Function Expression) để tạo ra một closure, biến items trở thành biến private, chỉ có thể truy cập thông qua các hàm public addItemgetTotal.

b) Decorator Pattern:

  • Mục đích: Mở rộng chức năng của một đối tượng một cách linh hoạt bằng cách “bọc” nó trong một đối tượng “decorator”.
  • Ví dụ: Thêm các tính năng (như logging, caching) cho một hàm.
function doSomething(name) {
  console.log("Hello, " + name);
}

function loggingDecorator(wrapped) {
  return function () {
    console.log("Starting");
    const result = wrapped.apply(this, arguments);
    console.log("Finished");
    return result;
  };
}

const wrapped = loggingDecorator(doSomething);
wrapped("World");
// Output:
// Starting
// Hello, World
// Finished
  • Giải thích: Hàm loggingDecorator nhận vào một hàm wrapped và trả về một hàm mới, hàm này sẽ log “Starting” trước khi gọi hàm wrapped và log “Finished” sau khi hàm wrapped thực thi xong.

3. Behavioral Patterns:

a) Observer Pattern:

  • Mục đích: Định nghĩa sự phụ thuộc một-nhiều giữa các đối tượng, sao cho khi trạng thái của một đối tượng thay đổi, tất cả các đối tượng phụ thuộc vào nó sẽ được thông báo và cập nhật tự động.
  • Ví dụ: Xây dựng hệ thống thông báo (notification system).
class Subject {
  constructor() {
    this.observers = [];
  }

  subscribe(observer) {
    this.observers.push(observer);
  }

  unsubscribe(observer) {
    this.observers = this.observers.filter((obs) => obs !== observer);
  }

  fire(action) {
    this.observers.forEach((observer) => {
      observer.update(action);
    });
  }
}

class Observer {
  constructor(name) {
    this.name = name;
  }
  update(action) {
    console.log(`${this.name} received action: ${action}`);
  }
}

const subject = new Subject();
const observer1 = new Observer("Observer 1");
const observer2 = new Observer("Observer 2");

subject.subscribe(observer1);
subject.subscribe(observer2);

subject.fire("New message!");
// Output:
// Observer 1 received action: New message!
// Observer 2 received action: New message!
  • Giải thích: Class Subject quản lý danh sách các observers. Khi fire được gọi, nó sẽ thông báo cho tất cả các observers bằng cách gọi phương thức update của chúng.

b) Command Pattern:

  • Mục đích: Đóng gói một yêu cầu (request) dưới dạng một đối tượng, từ đó cho phép parameter hóa các client với các yêu cầu khác nhau, đưa các yêu cầu vào hàng đợi, log lại các yêu cầu, và hỗ trợ các hoạt động có thể undo.
  • Ví dụ: Xây dựng chức năng undo/redo.
class Calculator {
  constructor() {
    this.value = 0;
    this.history = [];
  }

  executeCommand(command) {
    this.value = command.execute(this.value);
    this.history.push(command);
  }

  undo() {
    const command = this.history.pop();
    this.value = command.undo(this.value);
  }
}

class AddCommand {
  constructor(value) {
    this.value = value;
  }

  execute(currentValue) {
    return currentValue + this.value;
  }

  undo(currentValue) {
    return currentValue - this.value;
  }
}

// Using the calculator and commands
const calculator = new Calculator();
const add5 = new AddCommand(5);
const add10 = new AddCommand(10);

calculator.executeCommand(add5);
console.log(calculator.value); // Output: 5

calculator.executeCommand(add10);
console.log(calculator.value); // Output: 15

calculator.undo();
console.log(calculator.value); // Output: 5
  • Giải thích: Class AddCommand đóng gói hành động cộng thêm một giá trị. Class Calculator sử dụng các command để thực hiện các phép tính và lưu trữ lịch sử các command để có thể undo.

Kết Luận

Design Patterns là một phần quan trọng trong hành trang của mỗi lập trình viên. Việc hiểu và áp dụng thành thạo các Design Patterns sẽ giúp bạn nâng cao kỹ năng thiết kế phần mềm, tạo ra những sản phẩm chất lượng, dễ bảo trì và mở rộng. Hãy xem bài viết này như một điểm khởi đầu, và tiếp tục tìm hiểu sâu hơn về các Design Patterns khác để trở thành một “cao thủ” code thực thụ nhé!