Một số Design Patterns có thể sử dụng trong Javascript – Phần 1

320

Xin chào các bạn,

Trong bài viết này, mình sẽ chỉ cho các bạn một số design pattern được sử dụng trong javascript, để giúp các bạn viết code tốt hơn, dễ đọc hơn, dễ bảo trì hơn. Để đọc và hiểu bài viết này, thì các bạn cần phải có sẵn kiến thức về javascript nhé, tuy nhiên không cần nhiều đâu, chỉ cần hiểu được một số khái niệm như class, object là được.

Trong phần 1 này, mình sẽ trình bày tới các bạn về tổng quan design pattern, phân loại design pattern và một số mẫu design pattern đầu tiên.

I. Giới thiệu về design pattern

Trong lúc chúng ta lập trình, có rất nhiều vấn đề cứ na ná nhau. Và sau khi giải quyết xong các vấn đề đó, bạn sẽ thấy rằng thường sẽ có một mô hình chung để giải quyết các vấn đề tương tự nhau như vậy. Đó chính là lúc người ta bắt đầu nghĩ về design pattern.

Design pattern là một thuật ngữ được sử dụng trong ngành kỹ nghệ phần mềm nói chung, là giải pháp tái sử dụng cho việc giải quyết các vấn đề giống nhau và thường xuyên ra trong quá trình phát triển phần mềm

Khái niệm về design pattern trong kỹ nghệ phần mềm đã có mặt từ rất sớm, nhưng các khái niệm này không được công nhận một cách chính thức ngay, mà phải về sau này, chúng mới dần dần bắt đầu được công nhận và trở thành một trong những khái niệm chính thức trong ngành kỹ nghệ phần mềm. Đến nay, thì design pattern đã đường đường chính chính trở thành một phần quan trọng trong việc phát triển phần mềm.

Design pattern mang lại rất nhiều lợi ích cho chúng ta trong quá trình code. Một trong những lợi ích lớn nhất là sẽ giúp code của chúng ta dễ hiểu hơn, dễ tái sử dụng hơn.

Design pattern không phải là cách để giải quyết dứt điểm cho một bài toán cụ thể, mà nó chỉ đem đến cho chúng ta hướng giải quyết, hướng tiếp cận vấn đề một cách dễ dàng hơn.

II. Phân loại Design Patterns

2.1 Creational Design Patterns

Creational Design Patterns có nghĩa là những nhóm design patterns về khởi tạo đối tượng. Chúng quản lý việc khởi tạo đối tượng phù hợp với ngữ cảnh của bài toán.

Ở loại này, chúng ta sẽ đi tìm hiểu 4 mẫu design patterns là: Construct pattern, factory pattern, prototype patternsingleton pattern.

2.2 Structural Design Patterns

Đây là nhóm design pattern liên quan tới cấu trúc các thành phần và lớp đối tượng. Chúng giúp ta có thể bổ sung cấu trúc mới hoặc tái cấu trúc dự án theo từng phần mà không làm ảnh hưởng tới các phần khác trong hệ thống.

Củ thể, trong bài viết này chúng ta sẽ đi tìm hiểu về các mẫu structural design pattern sau: Adapter Pattern, Composite Pattern, Decorator Pattern, Façade Pattern, Flyweight Pattern, and Proxy Pattern.

2.3 Behavioral Design Patterns

Đây là nhóm design patterns liên quan tới hành vi, chúng sẽ giúp tăng sự kết nối giữa các đối tượng khác nhau. Cụ thể chúng ta sẽ đi tìm hiểu chi tiết các mẫu design patterns sau: Chain of Responsibility Pattern, Command Pattern, Iterator Pattern, Mediator Pattern, Observer Pattern, State Pattern, Strategy Pattern, và Template Pattern.


III. Constructor Pattern

Trong các ngôn ngữ lập trình hướng đối tượng, constructor vốn là một hàm đặc biệt được sử dụng để tạo ra một đối tượng mới từ một class. Tuy nhiên đối với javascript, thì bạn có thể tạo ra một đối tượng mới mà không nhất thiết cần phải khởi tạo từ một function hay một class nào cả. Chính vì vậy, mình nghĩ việc ưu tiên trình bày về constructor pattern trước sẽ giúp các bạn có kiến thức nền tảng để tìm hiểu các mẫu design pattern khác.

Trong ví dụ dưới đây, mình sẽ định nghĩa một lớp có tên Hero cùng với 2 thuộc tính là namespecialAbility, một phương thức là getDetails(). Sau đó chúng ta sẽ khởi tạo một đối tưởng iRonMan từ lớp này bằng cách sử dụng từ khóa new và truyền các tham số cho đối tượng.

// Đây là cách khởi tạo truyền thống của JS
function Hero(name, specialAbility) {
  // Gán các thuộc tính cho đối tượng
  this.name = name;
  this.specialAbility = specialAbility;

  // Khai báo phương thức cho đối tượng
  this.getDetails = function() {
    return this.name + ' can ' + this.specialAbility;
  };
}

// Đây là khi sử dụng cú pháp ES6
class Hero {
  constructor(name, specialAbility) {
    // Gán các thuộc tính cho đối tượng
    this._name = name;
    this._specialAbility = specialAbility;

    // Khai báo phương thức cho đối tượng
    this.getDetails = function() {
      return `${this._name} can ${this._specialAbility}`;
    };
  }
}

// Tạo một đối tượng mới từ lớp Hero ở trên
const iRonMan = new Hero('Iron Man', 'fly');

console.log(iRonMan.getDetails()); // Iron Man can fly

OK, design pattern này chỉ đơn giản và quen thuộc như vậy thôi

VI. Factory Pattern

Đây là một dạng khác của Creational Pattern. Khi cài đặt Pattern này, bạn sẽ phải tạo một function trả về đối tượng được khởi tạo từ một class khác. Thường thì các class để tạo ra các đối tượng trả về từ function này, sẽ là các class khác nhau nhưng có nhiều điểm giống nhau.

Xét ví dụ sau đây, mình tạo ra một lớp có tên là BallFactory, lớp này có một phưng thức tên là createBall với một tham số là type. Với mỗi giá trị của tham số type, phương thức createBall sẽ tạo ra một đối tượng khác nhau dựa trên các class tương ứng với giá trị của tham số type. Giả sử trường hợp typefoolball, thì createBall sẽ trả về một đối tượng được tạo ra từ lớp Football, còn typebasketball thì createBall sẽ trả về một đối tượng được tạo ra từ lớp Basketball.

class BallFactory {
  constructor() {
    this.createBall = function(type) {
      let ball;
      if (type === 'football' || type === 'soccer') ball = new Football();
      else if (type === 'basketball') ball = new Basketball();
      ball.roll = function() {
        return `The ${this._type} is rolling.`;
      };

      return ball;
    };
  }
}

class Football {
  constructor() {
    this._type = 'football';
    this.kick = function() {
      return 'You kicked the football.';
    };
  }
}

class Basketball {
  constructor() {
    this._type = 'basketball';
    this.bounce = function() {
      return 'You bounced the basketball.';
    };
  }
}

// creating objects
const factory = new BallFactory();

const myFootball = factory.createBall('football');
const myBasketball = factory.createBall('basketball');

console.log(myFootball.roll()); // The football is rolling.
console.log(myBasketball.roll()); // The basketball is rolling.
console.log(myFootball.kick()); // You kicked the football.
console.log(myBasketball.bounce()); // You bounced the basketball.

V. Prototype Pattern

Đây cũng là một pattern thuộc nhóm Creational Pattern. Pattern này sẽ sử dụng một một đối tượng khác như một “khung xương” để khởi tạo nên một đối tượng mới.

Pattern này đặc biệt quan trọng với Javascript, bởi vì Javascript sử dụng Prototype rất nhiều. Chính vì thế pattern này sử dụng với Js mới thật sự phát huy được hết sức mạnh của Js.

Prototype trong Js là một chủ đề tốn giấy mực, nếu bạn chưa hiểu Prototype trong Js là gì thì cũng không sao. Bạn vẫn có thể xem tiếp ví dụ dưới đây để hiểu hơn về pattern này.

Trong ví dụ sau, mình có sẵn một object car, sau đó mình sẽ sử dụng object này kết hợp với hàm Object.create của Javascript để tạo ra một object mới có tên là myCar. Object mới này sẽ bổ sung thêm một thuộc tính mới là ownner.

const car = {
  noOfWheels: 4,
  start() {
    return 'started';
  },
  stop() {
    return 'stopped';
  },
};
// Sử dụng Object.create được khuyên dùng khi sử dụng với cú pháp ES5
// Object.create(proto[, propertiesObject])

const myCar = Object.create(car, { owner: { value: 'John' } });

console.log(myCar.__proto__ === car); // true

VI. Singleton Pattern

Singleton pattern là một pattern đặc biệt trong nhóm creational pattern. Pattern này chỉ cho phép có duy nhất một đối tượng được khởi tạo từ class. Cách hoạt động của nó như sau – Nếu chưa có đối tượng nào được khởi tạo từ class trước đó, nó sẽ khởi tạo đối tượng từ class và trả về đối tượng đó, nhưng nếu đối tượng đã được khởi tạo rồi thì thay vì khởi tạo lần nữa, nó sẽ trả về đối tượng đã được khởi tạo trước đó.

Một ví dụ quen thuộc trong bài toán thực tế đó là Mongooso (một thư viện ODM Nodejs cho MongoDB), thư viện này tận dụng rất tốt và hiểu quả với Pattern này.

Trong ví dụ dưới đây, chúng ta có một class tên là Database – là một class singleton. Đầu tiên, chúng ta khởi tạo một đối tượng mongo bằng việc sử dụng từ khóa new gọi tới class Database như bình thường. Trong lần khởi tạo đầu tiên này, đối tượng sẽ được khởi tạo bởi vì trước đó chưa từng có đối tượng nào khác được khởi tạo từ lớp Database này. Tiếp theo, chúng ta tiếp tục khởi tạo một đối tượng mới tên là mysql, trong lần khởi tạo này, class Database sẽ không khởi tạo thêm đối tượng mới nào nữa, thay vào đó nó sẽ tham chiếu tới đối tượng trước và trả về đối tượng đó, cụ thể trong ví dụ này sẽ trả về đối tượng mongo.

class Database {
  constructor(data) {
    if (Database.exists) {
      return Database.instance;
    }
    this._data = data;
    Database.instance = this;
    Database.exists = true;
    return this;
  }

  getData() {
    return this._data;
  }

  setData(data) {
    this._data = data;
  }
}

// usage
const mongo = new Database('mongo');
console.log(mongo.getData()); // mongo

const mysql = new Database('mysql');
console.log(mysql.getData()); // mongo

VII. Adapter Pattern

Đây là một dạng của structural pattern, được sử dụng khi mà bạn muốn thay thế một class cũ bằng một class mới. Pattern giúp các class có thể hoạt động cùng với nhau ngay cả khi chúng không thực sự tương thích với nhau.

Pattern này thường được sử dụng để tạo ra một lớp mới chứa các APIs mới “bao bọc” lại những APIs cũ. Vì chỉ đơn giản là “bọc” lại các APIs cũ nên các APIs cũ sẽ không bị thay đổi, và đương nhiên vẫn hoạt động bình thường ở những nơi khác trong hệ thống.

Pattern này được áp dụng khi mà chúng ta muốn mở rộng hoặc cải thiện một thành phần nào đó trong hệ thống mà không làm ảnh hưởng tới hệ thống hiện tại cho dù hệ thống hiện tại vẫn đang sử dụng các thành phần cũ đó.

Trong ví dụ dưới đây, chúng ta có một API cũ là class OldCalculator, và một API mới là class NewCalculator. Class OldCalculator có một phương thức là operation thực hiện tính toán cho cả phép cộng và phép trừ. Trong khi class NewCalculator thì thực hiện phép cộng và phép trừ ở hai phương thức khác nhau. Một class thứ ba là CalcAdapter sẽ “bọc” class NewCalculator trong khi vẫn giữ nguyên cấu trúc hàm operation của OldCaclculator.

// old interface
class OldCalculator {
  constructor() {
    this.operations = function(term1, term2, operation) {
      switch (operation) {
        case 'add':
          return term1 + term2;
        case 'sub':
          return term1 - term2;
        default:
          return NaN;
      }
    };
  }
}

// new interface
class NewCalculator {
  constructor() {
    this.add = function(term1, term2) {
      return term1 + term2;
    };
    this.sub = function(term1, term2) {
      return term1 - term2;
    };
  }
}

// Adapter Class
class CalcAdapter {
  constructor() {
    const newCalc = new NewCalculator();

    this.operations = function(term1, term2, operation) {
      switch (operation) {
        case 'add':
          // using the new implementation under the hood
          return newCalc.add(term1, term2);
        case 'sub':
          return newCalc.sub(term1, term2);
        default:
          return NaN;
      }
    };
  }
}

// usage
const oldCalc = new OldCalculator();
console.log(oldCalc.operations(10, 5, 'add')); // 15

const newCalc = new NewCalculator();
console.log(newCalc.add(10, 5)); // 15

const adaptedCalc = new CalcAdapter();
console.log(adaptedCalc.operations(10, 5, 'add')); // 15;

VIII. Tạm kết

Ok phần một đến đây là hết. Ở phần một này, các bạn hãy hiểu cho mình tầm quan trọng của design pattern, và thử hình dung lại trong quá trình các bạn làm việc các bạn đã vô tình gặp được pattern nào mà mình giới thiệu ở trên chưa nhé. Mình nghĩ chắc chắn là rồi, nếu bạn không thấy thì có thể là do bạn tạm thời chưa nhận ra thôi. Vì các pattern trên đều là những pattern rất hay được sử dụng trong quá trình làm việc.

Ở phần 2, mình sẽ giới thiệu tiếp đến các bạn một số pattern khác nữa, cũng rất thường xuyên được sử dụng trong dự án.