Singleton design pattern

120

Singleton là một design pattern thuộc nhóm khởi tạo, với design pattern này thì mỗi class sẽ chỉ khởi tạo được một đối tượng duy nhất.

I. Bài toán

Mình đang xây dựng một website tin tức sử dụng PHP và MySQL. Để quản lý việc kết nối giữa PHP và MySQL, mình có tạo ra một class Database như sau:

<?php

// mywebsite/Database.php

class Database
{
    public $connection;

    public function __construct()
    {
        $host = 'localhost';
        $username = 'root';
        $password = '';
        $database = 'my_database';
        $this->connection = new \mysqli($host, $username, $password, $database);
    }
}

Cách sử dụng

<?php

// mywebsite/test.php

require('Database.php');

// Cách sử dụng
$database = new Database();
$connection = $database->connection;
$connection->query("...");

Ok, cùng review đoạn code trên xem thế nào nhé:

  • Class Database có một thuộc tính public là $connection, và thuộc tính này là “thể hiển” của class mysqli (dòng 11 – file Database.php).
  • Thuộc tính $connection chính là cầu nối giữa PHP và MySQL, nếu bạn muốn thực hiện bất kỳ query nào từ website PHP tới MySQL, thì đều phải sử dụng tới thuộc tính $connection này.
  • Cách sử dụng thì bạn thấy rồi đó, mình khởi tạo class Database như bình thường, sau đó sử dụng thuộc tính $connection để thực hiện một query đơn giản

Tới lúc này, mọi thứ vẫn ổn, ngoại trừ việc mình đã cố tình loại bỏ các thao tác bắt lỗi để đoạn code trở nên đơn giản, dễ hiểu hơn.

Nhưng giờ bài toán của mình trở nên phức tạp hơn, trong quá trình tải trang web, có rất nhiều chỗ mình cần sử dụng đến thông tin trong database. Cụ thể như 2 trường hợp sau:

Sử dụng lần 1 ở mywebsite/init.php, mục đích để lấy các thông tin cơ bản của website như tiêu đề trang web, mô tả của trang web, nằm trong bảng settings.

<?php

// mywebsite/init.php

require('Database.php');

$database = new Database();
$connection = $database->connection;
$connection->query("SELECT * FROM `settings`");

Sử dụng lần 2 ở mywebsite/home.php, mục đích để lấy ra các bài viết mới nằm trong bảng articles.

<?php

// mywebsite/home.php

require('Database.php');

$database = new Database();
$connection = $database->connection;
$connection->query("SELECT * FROM `articles`");

Giờ chúng ta nhìn lại code của 2 file trên, chúng bắt đầu có một số vấn đề:

  • Code bị lặp đi lặp lại
  • Nếu 2 file trên chạy tuần tự, thì bạn sẽ có 2 connection được khởi tạo.

Vấn đề code bị lặp đi lặp lại tạm thời chúng ta chưa bàn tới, hãy bàn tới việc bạn sẽ có 2 connection được tạo ra đã:

  • Lần 1 tạo ở mywebsite/init.php dòng 9.
  • Lần 2 tạo ở mywebsite/home.php dòng 9.

Việc tạo ra 2 connection này rõ là không nên, bởi:

  • Càng nhiều connection, máy bạn sẽ càng tốn tài nguyên, đồng nghĩa với hiệu năng (tốc độ tải trang web) kém.
  • Hai connection được tạo ra giống hệt nhau, mục đích tạo ra cũng giống nhau, vậy tại sao phải tạo ra 2 connection, kể như chỉ cần tạo ra 1 cái rồi dùng chung cho tất cả có phải tốt hơn không?

Trên là mình mới lấy ví dụ cần sử dụng dữ liệu trong MySQL ở 2 chỗ, còn trong thực tế nó xảy ra vô cùng thường xuyên. Nếu cứ áp dụng cách như trên, trong một lần tải trang web khả năng bạn sẽ phải tạo ra hằng trăm connection.

Có cách nào để khắc phục điều này, làm sao để xuyên suốt quá trình hoạt động của website chỉ tạo ra một connection duy nhất. Singleton design pattern chính là thứ bạn cần lúc này.

II. Giải pháp với Singleton design pattern

Singleton design pattern là một dạng design pattern thuộc nhóm khởi tạo, nó giúp bạn với mỗi class sẽ chỉ có thể tạo ra một đối tượng.

Singleton cũng được biết đến như một trong những design pattern được sử dụng nhiều nhất.

Quay trở lại ví dụ với class Database ở phần I, giờ mình sẽ viết lại theo singleton desing pattern.

<?php

class Database
{
    private $instance = null;
    public $connection;

    private function __construct()
    {
        $host = '';
        $username = '';
        $password = '';
        $database = '';
        $this->connection = new \mysqli($host, $username, $password, $database);
    }

    public static function getInstance()
    {
        if (static::$instance == null) {
            static::$instance = new Database();
        }
        
        return static::$instance;
    }
}

Cách sử dụng

<?php

$database = Database::getInstance();
$connection = $database->connection;
$connection->query("...");

Các thay đổi so với phiên bản trước đó như sau:

  • Có một thuộc tính private static$instance
  • Hàm __construct() chuyển từ public sang private
  • Bổ sung thêm một hàm mới là public static function getInstance
  • Khi sử dụng, thay vì gọi toán tử new để khởi tạo đối tượng thì sử dụng phương thức Database::getInstance().

Cùng review đoạn code trên xem có gì cải tiến không nhé:

  • Vì hàm __construct() ở dạng private, nên bạn sẽ không thể sử dụng từ khóa new để khởi tạo đối tượng từ bên ngoài class, giúp hạn chế việc khởi tạo đối tượng một cách bừa bãi.
  • getInstance() là một phương thức static, nó kiểm tra thuộc tính $instance đã được khởi tạo hay chưa? Nếu chưa khởi tạo thì khởi tạo, nếu khởi tạo rồi thì sẽ trả về kết quả của lần khởi tạo đó. Điều này giúp việc khởi tạo đối tượng sẽ chỉ xảy ra một lần duy nhất cho dù bạn gọi hàm getInstance() bao nhiêu lần đi nữa.

Với những cải tiến như trên, vấn đề tạo ra nhiều connection đã được giải quyết.

III. Các thành phần có trong Single design pattern

Tùy vào ngôn ngữ lập trình mà sẽ có cách triển khai singleton design pattern khác nhau, tuy nhiên chúng luôn có đủ các thành phần:

  • Client: Chính là nơi gọi hàm Database::getInsance() đó.
  • Một class chứa phương thức để khởi tạo đối tượng, phương thức này đảm bảo Sẽ chỉ khởi tạo đối tượng một lần duy nhất ở lần gọi hàm đầu tiên, những lần sau sẽ chỉ trả về đối tượng đã được khởi tạo trước đó.

Dựa vào những thành phần trên, có thể tổng quát hóa Singleton design pattern trong PHP như sau:

<?php

class Classname
{
    private static $instance = null;

    private function __construct()
    {
        //
    }

    public static function getInstance()
    {
        if (static::$instance == null) {
            static::$instance = new static;
        }
        
        return static::$instance;
    }
}

// Cách sử dụng
$object = Classname::getInstance();

IV. Khi nào thì sử dụng Singleton desgin pattern

Nói đơn giản thì Khi bạn muốn một class sẽ chỉ tạo ra một đối tượng xuyên suốt toàn bộ dự án thì sẽ sử dụng tới Singleton design pattern, giống như trường hợp kết nối tới database mà mình đề ở ở mục I. Tuy nhiên cái khó là làm sao để bạn biết rằng class đó chỉ nên tạo ra một đối tượng duy nhất. Để biết được điều này thì chỉ có cách là bạn phải thật sự hiểu mình đang làm gì và muốn làm gì, bằng cách đọc rõ yêu cầu hoặc phân tích rõ yêu cầu trước khi lao vào code.

Mình sẽ liệt kê cho bạn một số trường hợp nên sử dụng Single design pattern, tuy nó có thể không chính xác với bạn, nhưng cứ tham khảo xem thế nào nhé:

  • Khi muốn tạo một class quản lý việc kết nối tới database, hoặc các kết nối tương tự.
  • Khi tích hợp tính năng thanh toán qua các cổng thanh toán.
  • Khi bạn muốn quản lý các thông tin cấu hình của hệ thống thông qua các class

V. Tổng kết

Tổng kết lại thì có một số ý quan trọng như sau:

  • Singleton là design pattern thuộc nhóm khởi tạo, class được triển khai theo singleton sẽ chỉ có thể tạo ra một đối tượng.
  • Để xác định khi nào sử dụng singleton, buộc bạn phải hiểu rõ mình sắp code cái gì, liệu nó có phải là đối tượng “global” toàn dự án hay không.
  • Singleton là một trong những design pattern được sử dụng phổ biến nhất.

Chào tạm biệt, hẹn gặp bạn bạn trong những bài viết tiếp theo.