Tản mạn về Singleton

singleton-pattern-uml

Mở đầu

Quá dễ dàng để nhận thấy rằng, khi ta chỉ cho phép tạo một đối tượng của một lớp cụ thể mặc cho người khác có cố gắng tạo bao nhiêu đối tượng đi nữa, thì ta phải dùng mẫu pattern Singleton này. Trong cuốn sách GoF nói rằng, mẫu Singleton “Đảm bảo rằng một lớp chỉ có duy nhất một thể hiện và cung cấp một biến toàn cục để truy cập nó”.

Bạn sử dụng mẫu Singleton khi bạn muốn hạn chế việc sử dụng tài nguyên (thay vì việc tạo không hạn chế số lượng đối tượng) hoặc khi bạn cần phải xử lý một đối tượng nhạy cảm, mà dữ liệu của nó không thể chia sẻ cho mọi thể hiện.

Ví dụ:

Screen Shot 2018-09-27 at 9.55.47 AM

 

 

 

 

 

Ngoài ra bạn có thể sử dụng mẫu Singleton khi bạn muốn hạn chế số lượng các thể hiện được tạo bởi vì bạn muốn chia sẻ dữ liệu của các đối tượng này. Ví dụ như khi bạn có một đối tượng cửa sổ window hay hộp thoại dialog, cần phải hiện thị và thay đổi dữ liệu, bạn sẽ không muốn tạo nhiều thể hiện của đối tượng này, vì bạn sẽ bị bối rối trong việc phải truy cập dữ liệu của thể hiện nào.

Screen Shot 2018-09-27 at 10.02.56 AM

Bài toán

Ồ, vậy bài toán đặt ra là: Ta có một class Boo vậy làm cách nào để ta thực hiện được mẫu pattern Singleton lên class Boo đó. Nhưng class đó phải đảm bảo được việc hạn chế cho các class khác có thể truy cập, đồng thời cũng đảm bảo được việc nó chỉ tạo một đối tượng duy nhất tại mọi thời điểm. 

Bắt đầu

Đầu tiên ta phải tạo class Boo đã:

Screen Shot 2018-09-27 at 10.25.05 AM

 

Ồ, vấn đề là ở đây, cái hàm khởi tạo quá riêng tư, mỗi khi ta cần khởi tạo đối tượng mới bằng toán tử new, lại có một đối tượng được sinh ra. Có lẽ ta nên thay đổi một chút.

Screen Shot 2018-09-27 at 10.30.38 AM.png

 

Mọi thứ có vẻ khá hơn, nhưng con vấn đề đa luồng thì sao, nếu hai luồng chạy cùng một thời điểm, có thể ta sẽ tạo ra 2 đối tượng cũng 1 lúc. Ta cần giải quyết điều đó ngay. Không khó, ta chỉ cần thay đổi 1 chút hàm getInstancethôi.

Screen Shot 2018-09-27 at 10.34.30 AM.png

 

Mọi thứ thật hoàn hảo, nhưng dường như code quá dài, ta có thể tối ưu hơn một chút. Giả sử ta thấy thằng việc “lazy init” là không cần thiết, vì đàng nào cũng phải khởi tạo ngay từ đầu. Các tham số của hàm khởi tạo có thể thay đổi sau, hoặc để default, ta có thể thay đổi source code như sau. Với việc khởi tạo đối tượng ngay từ đầu ta còn không cần từ khoá synchronized cho việc đồng bộ. Quá hay

Screen Shot 2018-09-27 at 10.52.29 AM.png

Lưu ý

Việc tạo class theo Singleton thường sẽ rất khó test ở phía client, vì việc không thể test mock cho các singleton.

Mặc dù việc thực hiện tạp pattern singleton không quá phức tạp nhưng chúng ta nên tiếp cận theo nhiều phương pháp khác nhau.

  • Singleton với các member là các trường final

Screen Shot 2018-09-27 at 11.04.49 AM

 

Việc gọi đối tượng khá đơn giản với Boo.singletonBoo. Việc ta thiết lập privatehay protected cho hàm khởi tạo để đẩm bảo đối tượng là duy nhất, không nhiều hơn, không ít hơn. Client không thể thay đổi được điều đó, trừ phi họ dùng refeflection, bằng cách gọi private constructor reflectively cùng với AccessibleObject.setAccessible. Để hạn chế việc này, ta nên thêm vào hàm khởi tạo các throw exception, nếu có yêu cầu khởi tạo lần thứ hai.

  • Singleton với member là các hàm static factory 

Screen Shot 2018-09-27 at 12.54.13 PM

 

Việc gọi Boo.getInstance() sẽ trả về tham chiếu của đối tượng, và tất nhiên không có đối tượng nào được khởi tạo thêm ở đây. Hướng tiếp cận này là hướng tiếp cận kiểu API, làm cho class Boo khá rõ ràng, do đối tượng được khai báo là final nên ta đang tham chiếu đến duy nhất một đối tượng. vá tất nhiên hướng tiếp cận này cũng khá đơn giản để cài đặt. Hướng tiếp cận này rất linh hoạt khi ta muốn thay đổi bản thân bên trong phương thức mà không cần phải thay đổi API, có thể nói rằng ta đang tách riêng đối tượng với mỗi luồng gọi đến phương thức. Và tất nhiên ta hoàn toàn có thể viết một generic singleton factory. Ngoài ra ta có thể sử dụng cả method reference ( tính năng mới trên java 8).

Tất nhiên để class singleton, ta nên nhìn nhận vấn đề khi class đó implement InterfaceSerializable, nếu như việc implement là bắt buộc, nhưng ta vẫn muốn đảm bảo việc class là singleton, ta cần làm hai việc: 1) Khai báo tất cả các trường với từ khoá là transient (Để khi writeObject, jvm sẽ bỏ qua các trường này). 2) cung cấp phương thức readResolve, vì mỗi lần đối tượng được readObject khi deserialized, phương thức trên sẽ được gọi trước, nếu như ta bỏ qua phương thức readResolve, đối tượng ta nhận về sẽ không phải là đối tượng ta khởi tạo ban đầu khi serialized.

Screen Shot 2018-09-27 at 1.51.16 PM.png

 

Đừng quá quan tâm đến cái serialVersionUID, vì đó chỉ là cái định danh cho mỗi đối tượng, và trong quá trình jvm thực hiện serialized, nó sẽ phân biệt các đối tượng với nhau thôi.

  • Singleton với enum

Screen Shot 2018-09-27 at 1.55.46 PM.png

 

Với hướng tiếp cận này, ta không phải lo lắng vấn đề serialization, cũng không lo về việc khởi tạo lại nhiều lần, vì đó là cơ chế của enum không cho phép điều đó. Có thể nói đây là hướng tiếp cận tốt nhấtcho việc thiết kế pattern Singleton. Duy chỉ có việc là nó chỉ extend được mỗi enum thôi, nhưng vẫn implement được interface nhé.

Tài liệu tham khảo

Effective Java 3rd Edition _ Joshua Bloch

Software Architecture Design Patterns in Java _ Partha Kuchana

Design Patterns For Dummies_ Steve Holzner, PhD

( Bài viết nếu có gì sai sót, mong nhận thêm gạch đá để hoàn thiên hơn ạ )

 

 

Add a Comment

Scroll Up