Singleton pattern – Javascript

Singleton pattern được áp dụng khi: Chỉ cho phép một class chỉ có một đối tượng duy nhất tồn tại Có khả năng truy cập đến đối tượng từ mọi nơi (global access). Giải quyết cho các bài toán: Shared resource, Logger, Configuration, Caching, Database connection …

Singleton pattern – Javascript
Singleton pattern

Định nghĩa

Singleton là lớp mà chỉ được khởi tạo một lần duy nhất, và có thể truy cập ở phạm vi global. Cái phiên bản duy nhất này có thể chia sẻ khắp ứng dụng của chúng ta, điều này làm nó trở nên tuyệt vời để quản lý global state trong ứng dụng.

Trường hợp cần sử dụng

·       Khi chỉ cho phép một class chỉ có một đối tượng duy nhất tồn tại.

·       Có khả năng truy cập đến đối tượng từ mọi nơi (global access).

·       Giải quyết cho các bài toán: Shared resource, Logger, Configuration, Caching, Database connection …

Cách thực hiện

Đầu tiên, hãy xem một singleton có thể được khai báo như thế nào sử dụng ES2015 class. Trong ví dụ này, chúng ta sẽ xây dựng một class tên là Counter bao gồm:

·       Một phương thức getInstant trả về giá trị của đối tượng đó.

·       getCount trả về giá trị hiện tại của biến counter.

·       increment tăng thêm 1 đơn vị cho biến counter.

·       decrement giảm 1 đơn vị cho biến counter.

let counter = 0;

class Counter {
  getInstance() {
    return this;
  }

  getCount() {
    return counter;
  }

  increment() {
    return ++counter;
  }

  decrement() {
    return --counter;
  }
}

Tuy nhiên, class này vẫn chưa đáp ứng được tiêu chí của Singleton. Một Singleton, “một singleton chỉ có thể khởi tạo đối tượng 1 lần”. Hiện tại, chúng ta có thể khởi tạo đối tượng Counter nhiều lần.

const counter1 = new Counter();
const counter2 = new Counter();

console.log(counter1.getInstance() === counter2.getInstance()); // false

Bằng cách gọi phương thức new chúng ta đã gán cho counter1 counter2 thành 2 đối tượng khác nhau. Giá trị trả về của phương thức getInstant của counter1 counter2 là giá trị tham chiếu của hai đối tượng khác nhau, chúng hoàn toàn không giống nhau.

Hãy chắc chắn rằng chỉ có thể khởi tạo một đối tượng của Counter.

Có một cách để đảm bảo rằng chỉ có một đối tượng duy nhất được khởi tạo, có là tạo thêm 1 biến đặt tên là instance. Trong hàm khởi tạo của Counter, chúng ta có thể gán instance bằng với tham chiếu đến đối tượng khi mà một đối tượng được tạo. Chúng ta có thể ngăn chặn việc khởi tạo mới bằng cách kiểm tra nếu biến instance đã có giá chị hay chưa. Nếu đã có giá trị rồi thì sẽ ném ra cho người dùng một thông báo để người dùng biết.

let instance;
let counter = 0;

class Counter {
  constructor() {
    if (instance) {
      throw new Error("You can only create one instance!");
    }
    instance = this;
  }
  // ... rest of the code
}
const counter1 = new Counter();
const counter2 = new Counter();
// Error: You can only create one instance!

Oke, bây giờ thì người dùng không thể tạo được nhiều đối tượng Counter nữa. Hãy export đối tượng Counter từ file counter.js. Tuy nhiên, trước khi làm thế chúng ta nên “đóng băng” đối tượng này lại. Phương thức Object.freeze đảm bảo rằng đối tượng Counter này không thể bị chỉnh sửa. Các thuộc tính của đối tượng bị freeze không thể bị chỉnh sửa hoặc thêm mới, giúp giảm nguy cơ ghi đè các giá trị của Singleton.

Ví dụ

Hãy xem xét ví dụ sử dụng Counter ở bên trên. Chúng ta có các file sau:

·       counter.js: chứa class Counter và export mặc định đối tượng Counter.

·       index.js: tải các module redButton.js và blueButton.js

import "./redButton";
import "./blueButton";

console.log("Click on either of the buttons 🚀!");

·       redButton.js: import Countervà sử dụng phương thức increment khi người dùng bấm vào nút đỏ. Sau đó log ra giá trị của counter bằng cách gọi phương thức getCount.

import Counter from "./counter";

const button = document.getElementById("red");
button.addEventListener("click", () => {
  Counter.increment();
  console.log("Counter total: ", Counter.getCount());
});

·       blueButton.js: thêm event lắng nghe sự kiện tương tự như redButton khi người dùng click

import Counter from "./counter";

const button = document.getElementById("blue");
button.addEventListener("click", () => {
  Counter.increment();
  console.log("Counter total: ", Counter.getCount());
});

Cả 2 redButton.jsblueButton.js đều import cùng một đối tượng từ counter.js. Khi gọi hàm increment từ 2 nguồn là redButton.js blueButton.js, giá trị của thuộc tính counter trong đối tượng Counter sẽ được cập nhật ở cả 2 file. Nó không quan trọng khi bạn click vào nút đỏ hay xanh, giá trị giống nhau sẽ được chia sẽ giữa toàn bộ các phiên bản blue và red. Điều này lý giải việc counter sẽ tiếp tục tăng thêm một đơn vị, mặc dù chúng ta gọi phương thức ở các file khác nhau.

Using a regular object

Việc chỉ tạo ra một đối tượng có thể tiết kiệm được kha khá bộ nhớ. Thay vì việc phải dành ra nhiều bộ nhớ cho đối tượng mỗi khi khai báo, chúng ta chỉ cần thiết lập bộ nhớ cho một đối tượng và nó sẽ được tham chiếu đến toàn bộ ứng dụng. Tuy nhiên, Singleton có thể được coi là một anti-patternvà nên tránh trong JS.

Trong hầu hết các ngôn ngữ khác như C++ hay Java, chúng ta không thể tạo object bằng cách chúng ta vẫn làm trong JS. Trong các ngôn ngữ lập trình hướng đối tượng, chúng ta cần phải khai báo class, sau đó nó mới cho phép việc tạo ra các object. Object được tạo đó có giá trị thể hiện của class, giốn như biến instance ở ví dụ bên trên.

Tuy nhiên việc sử dụng class để triển khai cho ví dụ bên trên là quá mức cần thiết. Vì chúng ta có thể tạo ra trực tiếp một object trong JS, thay vì việc sử dụng class chúng ta chỉ đơn giản là tạo ra một object thông thường mà vẫn cho ra kết quả tương tự.

Hãy refactor lại ví dụ bên trên. Tuy nhiên, lúc này counter chỉ đơn giản là một object thông thường:

let count = 0;

const counter = {
  increment() {
    return ++count;
  },
  decrement() {
    return --count;
  }
};

Object.freeze(counter);
export { counter };

Vì các đối tượng được truyền bằng tham chiếu, cho nên cả 2 redButton và blueButton sẽ tham chiếu đến cùng đối tượng counter. Thay đổi giá trị count ở bất kỳ đâu sẽ thay đổi giá trị của object counter và được phản ánh ở cả 2 file.

Kết luận

Đối tượng Singleton có thể được tham chiếu bởi toàn bộ thành phần trong ứng dụng. Cho nên các biến toàn cục về cơ bản sẽ có thể truy cập trong toàn bộ ứng dụng.

Việc sử dụng biến toàn cục thường được xem là một quyết định thiết kế tồi, vì nó có thể gây ra ô nhiễm phạm vi toàn cục và dẫn đến hành vi không mong muốn.

Kể từ ES2015 rất ít khi biến toàn cục được sử dụng. Cách khai báo biến sử dụng từ khoá mới là let const ngăn chặn việc lập trình viên vô tình khai báo các biến global, bằng việc giữ các biến này ở phạm vi block-scoped. Hệ thống module mới của JS giúp việc truy cập các biến gobal dễ dàng hơn mà không làm “ô nhiễm” các biến toàn cục, bằng cách có thể export giá trị từ một module và import chúng từ các file khác.

Tuy nhiên, Singleton vẫn sẽ thường được sử dụng để duy trì trạng thái toàn cục trong ứng dụng, mặc dù việc này cũng có thể dẫn đến các vấn đề phức tạp khi phần lớn codebase phụ thuộc vào cùng một đối tượng có thể thay đổi.

Việc hiểu luồng dữ liệu khi sử dụng các global state sẽ khá là khó khăn khi dự án phát triển ngày càng lớn và khi có hàng tá các component phụ thuộc lẫn nhau.

Nguồn tham khảo

Singleton Pattern
Share a single global instance throughout our application
What’s a design pattern?
Refactoring.Guru makes it easy for you to discover everything you need to know about refactoring, design patterns, SOLID principles, and other smart programming topics.