Singleton

 

 

Dẫn nhập thực tế:

Hầu hết các đối tượng trong một ứng dụng đều chịu trách nhiệm cho công việc của chúng và truy xuất dữ liệu tự lưu trữ (self-contained data) và các tham chiếu trong phạm vi được đưa ra của chúng. Tuy nhiên, có nhiều đối tượng có thêm những nhiệm vụ và có ảnh hưởng rộng hơn, chẳng hạn như quản lý các nguồn tài nguyên bị giới hạn hoặc theo dõi toàn bộ trạng thái của hệ thống. Ví dụ có thể có rất nhiều máy in trong hệ thống nhưng chỉ có thể tồn tại duy nhất một Sprinter Spooler (Phần quản lý máy in)

Hay

Giả sử trong ứng dụng có chức năng bật tắt nhạc nền chẳng hạn, khi người dùng mở app thì ứng dụng sẽ tự động mở nhạc nền và nếu người dùng muốn tắt thì phải vào setting trong app để tắt nó, trong setting của app cho phép người dùng quản lí việc mở hay tắt nhạc, và trong trường hợp này bạn sẽ cần sử dụng singleton để quản lí việc này. Chắc chắn bạn phải cần duy nhất 1 instance để có thể ra lệnh bật hay tắt, tại sao ? vì đơn giản bạn không thể tạo 1 instance để mở nhạc rồi sau đó lại tạo 1 instance khác để tắt nhạc, lúc này sẽ có 2 instance được tạo ra, 2 instance này không liên quan đến nhau nên không thể thực hiện thực hiện việc cho nhau được, bạn phải hiểu rằng instance nào bật thì chỉ có instance đó mới được phép tắt nên dẫn đến phải cần 1 instance.

Định nghĩa:

Đảm bảo 1 class chỉ có 1 instance và cung cấp 1 điểm truy xuất toàn cục đến nó.
Tần suất sử dụng: cao trung bình 

UML class diagram

 

 

Thành phần tham gia:

 

  • Singleton
    • Định nghĩa một thể hiện – Instance duy nhất cho phép client được quyền truy cập đến.
    • Singleton chịu trách nhiệm cho việc khởi tạo và duy trì thể hiện của nó.

Một số ví dụ về Singleton Pattern: file system, file manager, window manager, printer spooler, ngày giờ…

Lợi ích

Việc sử dụng Singleton Pattern đem lại các lợi ích sau:

  • Quản lý việc truy cập tốt hơn vì chỉ có một thể hiện đơn nhất.
  • Cho phép cải tiến lại các tác vụ (operations) và các thể hiện (representation) do pattern có thể được kế thừa và tùy biến lại thông qua một thể hiện của lớp con
  • Quản lý số lượng thể hiện của một lớp, không nhất thiết chỉ có một thể hiện mà có số thể hiện xác định.
  • Khả chuyển hơn so với việc dùng một lớp có thuộc tính là static, vì việc dùng lớp static chỉ có thể sử dụng một thể hiện duy nhất, còn Singleton Pattern cho phép quản lý các thể hiện tốt hơn và tùy biến theo điều kiện cụ thể.

Trường hợp sử dụng

Người lập trình có thể dùng Singleton Pattern trong những trường hợp sau:

  • Trong trường hợp chỉ cần một thể hiện duy nhất của một lớp.
  • Khi thể hiện duy nhất khả mở thông qua việc kế thừa, người dùng có thể sử dụng thể hiện kế thừa đó mà không cần thay đổi các đoạn mã của chương trình.

Cách thực hiện

Thực hiện Singleton Pattern theo các bước sau:

  • Định nghĩa một thuộc tính private static trong lớp Singleton: instance.
  • Định nghĩa tất cả các constructor thành protected hoặc private:  bằng cách này thì thể hiện của lớp Singleton chỉ được khởi tạo nội bộ bên trong, những nơi khác bên ngoài sẽ không được phép khởi tạo.
  • Định nghĩa một accessor public static trong lớp: getInstance(). Các lớp khác sẽ truy xuất thể hiện của lớp Singleton thông qua phương thức này.
  • Thực hiện “lazy initialization” (khởi tạo chậm, khởi tạo khi yêu cầu) trong getInstance(): trả về một thể hiện mới hay một giá trị rỗng (null) tùy thuộc vào một biến boolean, biến này như một cờ hiệu dùng báo xem lớp đó đã có thể hiện hay chưa.
  • Clients chỉ dùng getInstance() để tạo đối tượng của lớp Singleton.

Giải thích: vì hàm khởi tạo có cài đặt là private nên ta không được phép tạo mới thể hiện  ở bên ngoài. Do đó các nơi khác bên ngoài không thể truy cập vào phương thức trả về thông qua thể hiện của lớp Singleton được, mà phải thông qua chính lớp Singleton đó, vì vậy cả phương thức và biến thể hiện phải được cài đặt là static.

5.PNG

Các pattern liên quan

  • Abstract Factory: thường dùng để trả về các đối tượng duy nhất.
  • Builder: dùng tạo một đối tượng phức tạp, trong đó Singleton được dùng để tạo một đối tượng truy xuất tổng quát.
  • Prototype: dùng để sao chép một đối tượng, hoặc tạo ra một đối tượng khác từ prototype (nguyên mẫu) của nó, trong đó Singleton được dùng để chắc chắn chỉ có một prototype.

 

Khi nào sử dụng

  • Khi bạn thấy rằng cách khởi tạo một thể hiện của một lớp rất tốn nhiều hiệu suất và chi phí; chỉ cần một và chỉ một instance là đủ để sử dụng trong một vòng đời của ứng dụng.
  • Được sử dụng để thiết kế/tạo ra đối tượng Logger, Cache, Connection Pool, Thread Pool, … (được tạo ra một lần, không cần thiết phải tạo ra một hay nhiều thể hiện khác để sử dụng).
  • Được sử dụng trong các mẫu design pattern khác như: Builder, Prototype, Facade, Abstract Factory, State … – những mẫu này các bạn chịu khó tìm hiểu để biết thêm nhé.

 

 

Những cách để triển khai mẫu Singleton đơn giản

A. Khởi tạo theo kiểu Eager

Eager là gì?  Eager thường xuất hiện đi kèm với loading trong khái niệm Eager loading( tải đồng thời). Nó giúp bạn load tất cả trong 1 câu lệnh.

3.PNG

Đây là cách khởi tạo một lớp Singleton dễ và đơn giản nhất, theo cách này thì thể hiện sẽ được khởi tạo xong trước khi chương trình start up.

Điểm mạnh:

  • Cài đặt nhanh, đơn giản.
  • Khởi tạo nhanh: nếu quá trình khởi tạo thể hiện không tốn quá nhiều hiệu suất hay tài nguyên.

Điểm yếu:

  • Thể hiện sẽ tạo ra vô ích nếu nó không được sử dụng.
  • Đa số ở thực tế, lớp singleton sẽ đóng vai trò là một File System, Database Connection – khi khởi tạo thể hiện vào lúc sớm sẽ tốn hiệu suất và tài nguyên nhiều và vô ích nếu thể hiện đó không được sử dụng.
  • Không có cách nào để bắt và xử lý được exception xảy ra trong quá trình khởi tạo.
B. Khởi tạo theo khối static
4.PNG

Tương tự với phương pháp Eager, cách này cũng khởi tạo thể hiện của lớp singleton trước khi sử dụng, chỉ khác là nó cho phép ta có thể bắt và xử lý được exception nếu có trong quá trình khởi tạo.

C. Khởi tạo theo kiểu lazy load

5.PNG

Cách này nó sẽ tạo thể hiện ở trong phương thức trả về. Trong lượt gọi đầu tiên, nó sẽ kiểm tra rằng thể hiện đã được khởi tạo hay chưa, nếu chưa thì nó sẽ tạo mới và trả về thể hiện đó, còn không thì nó sẽ trả về thể hiện đã được khởi tạo.

Điểm mạnh:

  • Thể hiện sẽ được khởi tạo khi cần thiết.
  • Có thể bắt và xử lý được exception ngay khi khởi tạo.

Điểm yếu:

  • Cơ chế của mẫu Singleton sẽ sai lệch khi có nhiều luồng cùng thực thi. Ví dụ: có 2 luồng T1 và T2 cùng truy xuất vào phương thức trả về của lớp Singleton vào lúc thể hiện chưa được khởi tạo, lúc này cả T1 và T2 sẽ cùng lúc tạo ra 2 thể hiện.

 

Khai báo biến thể hiện ở trên có sử dụng từ khóa volatile – được hiểu là linh động: khi một biến được khai báo kiểu volatile tức là giá trị của nó ở tương lai sẽ được thay đổi.

Có thể tìm hiểu thêm về volatile tại: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/volatile

D. Kiểu Thread Safe

Ở cách này thì nó đã giải quyết được vấn đề ở lazy load bằng cách dùng từ khóa synchronized vào lúc khai báo phương thức. Tức trong cùng một thời điểm, chỉ có một luồng thực thi vào phương thức, cách này có hiệu suất thấp hơn nhưng bù lại đảm bảo được cơ chế của mẫu Singleton.

6.PNG

Điểm mạnh:

  • Thể hiện sẽ được khởi tạo khi cần thiết.
  • Có thể bắt và xử lý được exception ngay khi khởi tạo.
  • Đảm bảo được cơ chế của mẫu Singleton ở trường hợp đa luồng

Điểm yếu:

  • Hiệu suất thấp.
  • Vì cơ chế của mẫu Singleton chỉ sai trong lúc khởi tạo ở trường hợp đa luồng nên không cần thiết phải dùng từ khóa synchronized bao quát hết phương thức trả về – làm chậm đi rất nhiều trong quá trình truy xuất thể hiện.

Chúng ta tối ưu hóa hiệu suất bằng cách triển khai double checked locking – chỉ dùng synchronized block để bao quát phần khởi tạo thôi.

7.PNG

E. Cách triển khai của Bill Pugh
Đây là một cách do ông Bill Pugh sử dụng.

8.PNG

Ở cách này tác giả đã sử dụng một tính năng rất hay của nested class, ông cài đặt một lớp SingletonHelper với kiểu private static, bên trong nó gồm có một biến thể hiện của lớp Singleton.

Khi lớp Singleton được tải vào bộ nhớ của ứng dụng thì lớp SingletonHelper này sẽ chưa được tải vào, nó chỉ được tải và tạo biến thể hiện của lớp Singleton vào bộ nhớ của ứng dụng khi phương thức trả về được gọi. Cách này giúp mình tránh được lỗi cơ chế khởi tạo thể hiện của mẫu Singleton trong đa luồng.

Một vài điểm nâng cao dành cho các bạn muốn tìm hiểu thêm:

  • Cách phá vỡ cơ chế của một mẫu Singleton đã được cài đặt sử dụng Java Reflection.
  • Cách triển khai mẫu Singleton sử dụng Enum
  • Cách triển khai mẫu Singleton sử dụng trong Serializable.

Để hiểu rõ về nâng cao có thể tìm hiểu tại: https://medium.com/@huynhquangthao/m%E1%BA%ABu-thi%E1%BA%BFt-k%E1%BA%BF-singleton-5997128c71b9

 

Demo:

Giả lập load balancer trong lập trình.

1.PNG

 

2.PNG

Bài viết có sử dụng một số ví dụ và lý thuyết tham khảo ở:

Leave a comment