Bài 3: Thực thi tính chất singleton bằng constructor private hoặc enum type
Bài toán
Giá sử chúng ta tự triển khai một hệ thống loging đơn giản như sau
public class Logger {
public void log(String message) {
System.out.println(message);
}
}
Ở khắp nơi trong hệ thống của chúng ta sẽ liên tục làm điều này
Logger logger1 = new Logger();
Logger logger2 = new Logger();
Logger logger3 = new Logger();
Vấn đề là mỗi lần gọi như thế sẽ tạo ra một bản thể riêng biệt khiến tiêu tốn tài nguyên một cách không cần thiết, trong khi chúng là stateless. Hơn nữa, việc có nhiều instance của Logger sẽ khiến chúng tranh chấp chung một tài nguyên là file logs, chúng cạnh tranh nhau ghi file và khiến chúng ta rất khó khăn trong quản lý trạng thái.
Giải pháp
Một singleton đơn giản là một class chỉ được khởi tạo đúng một lần. Singleton thường được dùng để biểu diễn một object không có trạng thái (stateless object), chẳng hạn như một hàm, hoặc một thành phần hệ thống vốn dĩ chỉ nên tồn tại duy nhất một bản thể.
Việc biến một class thành singleton có thể khiến việc kiểm thử các client sử dụng nó trở nên khó khăn, bởi bạn không thể thay thế singleton bằng một mock implementation, trừ khi singleton đó triển khai một interface đóng vai trò là kiểu dữ liệu của nó.
Có hai cách phổ biến để triển khai singleton. Cả hai đều dựa trên việc giữ constructor ở mức private và công khai một thành viên public static để cung cấp quyền truy cập đến instance duy nhất.
Trong cách tiếp cận đầu tiên, thành viên này là một field final:
// Singleton với public final field
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis() { ... }
public void leaveTheBuilding() { ... }
}
Constructor private chỉ được gọi đúng một lần để khởi tạo field public static final là Elvis.INSTANCE.
Việc không có constructor public hoặc protected đảm bảo một “vũ trụ chỉ có một Elvis”: sau khi class Elvis được khởi tạo, sẽ tồn tại đúng một instance của Elvis — không nhiều hơn, cũng không ít hơn. Không có hành động nào từ phía client có thể thay đổi điều này, ngoại trừ một trường hợp đặc biệt: một client có đủ đặc quyền có thể sử dụng reflection là thứ chúng ta sẽ bàn luận trong bài 65 kết hợp với phương thức AccessibleObject.setAccessible để gọi constructor private.
Nếu cần bảo vệ khỏi kiểu tấn công này, hãy sửa constructor để nó ném ra exception khi có yêu cầu tạo instance thứ hai.
Trong cách tiếp cận thứ hai, thành viên công khai là một static factory method:
// Singleton với static factory
public class Elvis {
private static final Elvis INSTANCE = new Elvis();
private Elvis() { ... }
public static Elvis getInstance() {
return INSTANCE;
}
public void leaveTheBuilding() { ... }
}
Mọi lời gọi đến Elvis.getInstance() đều trả về cùng một object reference và sẽ không có bất kỳ instance Elvis nào khác được tạo ra (với cùng ngoại lệ liên quan đến reflection như đã đề cập ở trên).
Ưu điểm chính của cách sử dụng public field là API thể hiện rất rõ rằng class này là một singleton: field public static là final, vì vậy nó sẽ luôn tham chiếu đến cùng một object.
Ưu điểm thứ hai là cách tiếp cận này đơn giản hơn.
Một ưu điểm của cách sử dụng static factory là nó cho phép bạn linh hoạt thay đổi quyết định về việc class có phải là singleton hay không mà không cần thay đổi API.
Hiện tại factory method trả về instance duy nhất, nhưng trong tương lai nó hoàn toàn có thể được sửa đổi để trả về một instance riêng cho mỗi thread gọi đến nó.
Ưu điểm thứ hai là bạn có thể viết một generic singleton factory nếu ứng dụng của bạn yêu cầu điều đó.
Ưu điểm cuối cùng của static factory là nó có thể được sử dụng dưới dạng method reference như một supplier. Ví dụ, Elvis::getInstance là một Supplier<Elvis>.
Trừ khi một trong những ưu điểm này thực sự cần thiết, cách sử dụng public field thường được ưu tiên hơn.
Để một singleton được triển khai theo một trong hai cách trên có thể hỗ trợ serialization (Chương 12), việc chỉ đơn giản thêm implements Serializable vào khai báo class là chưa đủ.
Để duy trì đảm bảo singleton, hãy khai báo tất cả các instance field là transient và cung cấp một phương thức readResolve.
Nếu không làm vậy, mỗi lần một instance đã được serialize được deserialize, một instance mới sẽ được tạo ra. Trong ví dụ của chúng ta, điều này sẽ dẫn đến sự xuất hiện của những “Elvis giả mạo”.
Để ngăn điều đó xảy ra, hãy thêm phương thức readResolve sau vào class Elvis:
// Phương thức readResolve để duy trì tính chất singleton
private Object readResolve() {
// Trả về Elvis duy nhất và để garbage collector
// xử lý Elvis giả mạo.
return INSTANCE;
}
Một cách thứ ba để triển khai singleton là khai báo một enum chỉ có một phần tử:
// Enum singleton - cách tiếp cận được khuyến nghị
public enum Elvis {
INSTANCE;
public void leaveTheBuilding() { ... }
}
Cách tiếp cận này tương tự như cách sử dụng public field, nhưng ngắn gọn hơn, được hỗ trợ serialization một cách tự động và cung cấp sự đảm bảo tuyệt đối chống lại việc tạo nhiều instance, ngay cả khi đối mặt với các cuộc tấn công phức tạp thông qua serialization hoặc reflection.
Ban đầu cách này có thể khiến bạn cảm thấy hơi thiếu tự nhiên, nhưng một enum chỉ có một phần tử thường là cách tốt nhất để triển khai singleton.
Lưu ý rằng bạn không thể sử dụng cách tiếp cận này nếu singleton của bạn bắt buộc phải kế thừa một superclass khác ngoài Enum (mặc dù enum vẫn có thể triển khai interface).
All rights reserved