Các mẫu thiết kế (design patterns) là những giải pháp chung, có thể tái sử dụng cho các vấn đề thường gặp trong thiết kế phần mềm. Chúng cung cấp một bộ từ vựng chung để các nhà phát triển có thể giao tiếp hiệu quả hơn về các giải pháp thiết kế.
1. Mẫu nhà máy (Factory Pattern)
Mẫu nhà máy tập trung vào việc tạo đối tượng. Nó cho phép tạo các đối tượng mà không cần chỉ định chính xác lớp của đối tượng đó. Mẫu này hỗ trợ mở rộng theo chiều ngang (thêm sản phẩm mới) rất tốt. Tuy nhiên, nếu cần mở rộng theo chiều dọc (thêm thuộc tính cho sản phẩm hiện có), thường sẽ phải sửa đổi mã nguồn của sản phẩm hiện có. Ví dụ, một hệ thống cần hỗ trợ nhiều loại "ga tàu" khác nhau (Hà Nội, Sài Gòn). Khi thêm một loại ga mới (ví dụ: Đà Nẵng), ta chỉ cần tạo một lớp mới mà không ảnh hưởng đến các ga hiện có. Nhưng nếu muốn thêm một thuộc tính mới cho tất cả các loại ga (ví dụ: "mã bưu điện"), ta sẽ phải sửa đổi mã của từng lớp ga.
2. Mẫu nhà máy trừu tượng (Abstract Factory Pattern)
Mẫu nhà máy trừu tượng cung cấp một giao diện để tạo các họ đối tượng liên quan hoặc phụ thuộc mà không cần chỉ định lớp cụ thể của chúng. Mẫu này hỗ trợ mở rộng theo chiều dọc (thêm một "họ" sản phẩm mới) rất tốt. Tuy nhiên, việc thêm một "sản phẩm" mới vào một họ đã có sẽ yêu cầu sửa đổi cấu trúc hiện có, tương tự như nhược điểm của mẫu nhà máy khi mở rộng theo chiều dọc.
3. Mẫu đơn lẻ (Singleton Pattern)
Mẫu đơn lẻ đảm bảo rằng một lớp chỉ có một thể hiện (instance) duy nhất và cung cấp một điểm truy cập toàn cục đến thể hiện đó.
a. Mô hình "Người ăn xin" (Eager Initialization)
Mô hình này tạo thể hiện của lớp ngay khi lớp được tải vào bộ nhớ. Ưu điểm là tốc độ truy cập nhanh vì thể hiện đã sẵn sàng. Nhược điểm là có thể tốn bộ nhớ nếu thể hiện đó không bao giờ được sử dụng.
public class EagerSingleton {
private static final EagerSingleton INSTANCE = new EagerSingleton();
private EagerSingleton() {
// Private constructor to prevent instantiation
}
public static EagerSingleton getInstance() {
return INSTANCE;
}
public static void main(String[] args) {
System.out.println("Eager Singleton Instance: " + EagerSingleton.getInstance());
}
}
b. Mô hình "Lười biếng" (Lazy Initialization)
Mô hình này tạo thể hiện chỉ khi nó thực sự được yêu cầu lần đầu tiên. Ưu điểm là tiết kiệm bộ nhớ nếu thể hiện không được sử dụng. Nhược điểm là có chi phí thời gian nhỏ cho mỗi lần truy cập để kiểm tra xem thể hiện đã tồn tại hay chưa. Việc sử dụng `synchronized` ở đây đảm bảo an toàn luồng, nhưng có thể ảnh hưởng đến hiệu suất.
public class LazySingleton {
private static LazySingleton instance = null;
private LazySingleton() {
// Private constructor
}
public static synchronized LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
public static void main(String[] args) {
System.out.println("Lazy Singleton Instance: " + LazySingleton.getInstance());
}
}
c. Mô hình "Kiểm tra kép" (Double-Checked Locking)
Mô hình này cải thiện hiệu suất của mô hình "Lười biếng" bằng cách giảm tần suất sử dụng `synchronized`. Nó kiểm tra thể hiện hai lần: một lần bên ngoài khối `synchronized` và một lần bên trong. Từ khóa `volatile` đảm bảo rằng các thay đổi đối với biến `instance` được nhìn thấy ngay lập tức bởi tất cả các luồng.
public class DoubleCheckSingleton {
private static volatile DoubleCheckSingleton instance = null;
private DoubleCheckSingleton() {
// Private constructor
}
public static DoubleCheckSingleton getInstance() {
if (instance == null) { // First check
synchronized (DoubleCheckSingleton.class) {
if (instance == null) { // Second check
instance = new DoubleCheckSingleton();
}
}
}
return instance;
}
}
d. Mô hình "Lớp giữ chỗ khởi tạo muộn" (Lazy Initialization Holder Class)
Mô hình này sử dụng một lớp nội bộ tĩnh để giữ thể hiện đơn lẻ. Lớp nội bộ này chỉ được tải và khởi tạo khi phương thức `getInstance()` được gọi lần đầu tiên. Cơ chế khởi tạo lớp tĩnh của JVM đảm bảo tính an toàn luồng và hiệu quả. Phương thức `getInstance()` không cần bị đồng bộ hóa, mang lại hiệu suất truy cập tốt.
public class InnerSingleton {
private InnerSingleton() {
// Private constructor
}
private static class SingletonHolder {
private static final InnerSingleton INSTANCE = new InnerSingleton();
}
public static InnerSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
e. Mô hình "Liệt kê" (Enum Singleton)
Sử dụng enum để triển khai mẫu đơn lẻ là cách tiếp cận an toàn và hiệu quả nhất trong Java. JVM đảm bảo tính duy nhất của các phần tử enum, do đó tự động xử lý an toàn luồng và ngăn chặn việc khởi tạo lại.
public enum EnumSingleton {
UNIQUE_INSTANCE;
public void singletonOperation() {
System.out.println("Executing operation on Enum Singleton.");
// Feature implementation
}
public static void main(String[] args) {
EnumSingleton.UNIQUE_INSTANCE.singletonOperation();
}
}
4. Mẫu người xây dựng (Builder Pattern)
Mẫu người xây dựng tách biệt việc tạo một đối tượng phức tạp khỏi biểu diễn của nó, cho phép cùng một quy trình xây dựng tạo ra các biểu diễn khác nhau.
- Vai trò Trừu tượng người xây dựng (Abstract Builder): Định nghĩa một giao diện để tạo các thành phần của đối tượng sản phẩm.
- Vai trò Người xây dựng Cụ thể (Concrete Builder): Thực hiện các bước để xây dựng và lắp ráp một đối tượng sản phẩm.
- Vai trò Đạo diễn (Director): Xây dựng một đối tượng bằng cách sử dụng giao diện của người xây dựng trừu tượng.
- Vai trò Sản phẩm (Product): Đối tượng phức tạp đang được xây dựng.
5. Mẫu nguyên mẫu (Prototype Pattern)
Mẫu nguyên mẫu sử dụng một đối tượng hiện có làm khuôn mẫu để tạo ra các đối tượng mới. Điều này hữu ích khi việc tạo đối tượng mới tốn kém (ví dụ: yêu cầu nhiều tài nguyên hoặc khởi tạo phức tạp) hoặc khi cần tạo nhiều đối tượng tương tự nhau.
- Sao chép nông (Shallow Copy): Chỉ sao chép các giá trị dữ liệu (kiểu nguyên thủy, String). Các tham chiếu đến các đối tượng khác vẫn trỏ đến đối tượng gốc.
- Sao chép sâu (Deep Copy): Sao chép tất cả dữ liệu, bao gồm cả các đối tượng được tham chiếu, tạo ra một bản sao hoàn toàn độc lập.
6. Mẫu bộ chuyển đổi (Adapter Pattern)
Mẫu bộ chuyển đổi cho phép các lớp có giao diện không tương thích làm việc cùng nhau. Nó đóng vai trò là cầu nối giữa hai giao diện không tương thích.
Có ba biến thể chính:
- Bộ chuyển đổi lớp (Class Adapter): Sử dụng kế thừa để chuyển đổi giao diện của một lớp.
- Bộ chuyển đổi đối tượng (Object Adapter): Sử dụng thành phần (composition) để chuyển đổi giao diện của một lớp.
- Bộ chuyển đổi mặc định (Default Adapter): Cung cấp một lớp trừu tượng triển khai một giao diện và cho phép các lớp con chỉ ghi đè các phương thức cần thiết.
Bộ chuyển đổi đối tượng thường được ưa chuộng hơn bộ chuyển đổi lớp vì nó linh hoạt hơn (hỗ trợ kế thừa đa cấp và thành phần), cho phép kết hợp các đối tượng một cách động và dễ dàng hơn trong việc mở rộng chức năng.
Ưu điểm của mẫu bộ chuyển đổi:
- Tái sử dụng tốt hơn: Cho phép sử dụng lại các lớp hiện có mà không cần sửa đổi mã nguồn của chúng.
- Mở rộng tốt hơn: Dễ dàng thêm các chức năng mới bằng cách sử dụng bộ chuyển đổi.
Nhược điểm của mẫu bộ chuyển đổi:
- Phức tạp hóa hệ thống: Việc sử dụng quá nhiều bộ chuyển đổi có thể làm cho hệ thống trở nên khó hiểu và khó quản lý.