0

Bài 4: Buộc Class không thể được khởi tạo bằng constructor private

Bad practice thường gặp (hướng cũ)

public class UtilityClass {
	// Chỉ có static method, nhưng không khai báo constructor
	public static int add(int a, int b) {
		return a + b;
	}
}

UtilityClass u = new UtilityClass(); // Vẫn tạo instance được

Vấn đề của cách này:

  • Utility class bị khởi tạo dù instance là vô nghĩa.
  • Người dùng API có thể hiểu sai mục đích của class.
  • Dễ phát sinh misuse trong codebase theo thời gian.

Giải thích kỹ hơn về bad practice này:

  • Về mặt thiết kế: khi một class chỉ chứa static method/static field, việc cho phép new class đó tạo ra tín hiệu sai. Người đọc code có thể tưởng class này có state theo từng instance, trong khi thực tế không có.
  • Về mặt sử dụng: một số lập trình viên sẽ bắt đầu truyền instance này đi khắp nơi (ví dụ qua constructor hoặc parameter), làm API nhiễu và tăng coupling không cần thiết.
  • Về mặt bảo trì: sau một thời gian, codebase dễ xuất hiện kiểu dùng như utility.doSomething() thay vì UtilityClass.doSomething(), khiến style không nhất quán và khó review.

Ví dụ misuse thường gặp trong dự án:

public class ReportService {
	private final UtilityClass util;

	public ReportService() {
		this.util = new UtilityClass(); // Instance vô nghĩa nhưng vẫn bị truyền như dependency thật
	}
}

Một bad practice khác là dùng abstract để chặn khởi tạo:

public abstract class UtilityClass {
	public static int add(int a, int b) {
		return a + b;
	}
}

class ChildUtil extends UtilityClass {}
ChildUtil c = new ChildUtil(); // Vẫn tạo instance được qua subclass

Điểm nguy hiểm của cách abstract là nó ngầm gợi ý class được thiết kế cho inheritance, trong khi utility class thì ngược lại: không nên khởi tạo và cũng không nên kế thừa.

Best Practice

Đôi khi bạn sẽ muốn viết một lớp chỉ dùng để gom nhóm các phương thức static và các trường static. Những lớp như vậy thường bị mang tiếng xấu vì một số người lạm dụng chúng để né việc tư duy theo hướng đối tượng, nhưng chúng vẫn có những mục đích sử dụng hợp lệ. Chúng có thể được dùng để gom các phương thức liên quan tới giá trị nguyên thủy hoặc mảng, theo kiểu java.lang.Math hoặc java.util.Arrays. Chúng cũng có thể được dùng để gom các phương thức static, bao gồm cả factory (Item 1), cho các đối tượng cài đặt một interface, theo kiểu java.util.Collections. (Từ Java 8, bạn cũng có thể đặt các phương thức như vậy ngay trong interface, giả sử đó là interface bạn có thể sửa.) Cuối cùng, các lớp kiểu này còn có thể dùng để gom các phương thức thao tác trên một lớp final, vì bạn không thể đặt chúng trong lớp con.

Những utility class như vậy vốn không được thiết kế để bị khởi tạo đối tượng: một instance của chúng là vô nghĩa. Tuy nhiên, nếu không khai báo constructor tường minh, trình biên dịch sẽ tự cung cấp một default constructor public, không tham số. Với người dùng, constructor này không khác gì các constructor khác. Trong các API đã phát hành, không hiếm gặp những lớp vô tình có thể khởi tạo.

Cố gắng ngăn khởi tạo bằng cách biến lớp thành abstract là không hiệu quả. Lớp đó vẫn có thể bị kế thừa và lớp con vẫn có thể được khởi tạo. Hơn nữa, cách này còn khiến người dùng hiểu sai rằng lớp được thiết kế để kế thừa (Item 19). Tuy nhiên, có một idiom đơn giản để đảm bảo không thể khởi tạo. Default constructor chỉ được sinh ra khi lớp không có constructor tường minh nào, vì vậy có thể làm một lớp trở nên không thể khởi tạo bằng cách thêm một constructor private:

// Utility class không thể khởi tạo
public class UtilityClass {
	// Chặn default constructor để ngăn khởi tạo
	private UtilityClass() {
		throw new AssertionError();
	}
	... // Phần còn lại được lược bỏ
}

Vì constructor tường minh là private nên nó không thể được truy cập từ bên ngoài lớp. AssertionError không bắt buộc một cách tuyệt đối, nhưng nó tạo thêm lớp bảo vệ trong trường hợp constructor bị gọi nhầm từ bên trong chính lớp. Nó đảm bảo lớp sẽ không bao giờ bị khởi tạo trong bất kỳ tình huống nào. Idiom này hơi trái trực giác vì constructor được cung cấp chỉ để không thể gọi được. Vì vậy, bạn nên thêm comment như ví dụ ở trên.

Một tác dụng phụ của idiom này là nó cũng ngăn lớp bị kế thừa. Mọi constructor đều phải gọi constructor của lớp cha (tường minh hoặc ngầm định), và lớp con sẽ không có constructor lớp cha nào khả dụng để gọi.


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.