Tổng quan về Mẫu Prototype
Mẫu thiết kế Prototype (Nguyên mẫu) là một mẫu thuộc nhóm Creational (Tạo đối tượng), cho phép tạo ra các đối tượng mới bằng cách sao chép (clone) một đối tượng hiện có thay vì khởi tạo từ đầu. Đối tượng được sao chép này được gọi là "prototype".
Khi nào nên sử dụng?
Mẫu này đặc biệt hữu ích khi chi phí để khởi tạo một đối tượng mới quá lớn. Ví dụ, quá trình khởi tạo yêu cầu các truy vấn cơ sở dữ liệu phức tạp, gọi các API bên ngoài, hoặc xử lý các tệp tin nặng. Trong những tình huống này, việc nhân bản một đối tượng đã được khởi tạo sẵn sẽ tiết kiệm tài nguyên và thời gian hơn rất nhiều. Trong Java, việc này được hỗ trợ sẵn thông qua interface java.lang.Cloneable và phương thức clone().
Các thành phần cốt lõi
- Prototype (Nguyên mẫu trừu tượng): Khai báo interface chứa phương thức sao chép. Trong Java, đó là
java.lang.Cloneable. - Concrete Prototype (Nguyên mẫu cụ thể): Triển khai interface và định nghĩa logic sao chép cho chính nó.
- Client (Máy khách): Yêu cầu tạo đối tượng mới bằng cách gọi phương thức sao chép từ một đối tượng prototype.
Triển khai trong Java: Sao chép nông và Sao chép sâu
Khi sử dụng phương thức clone() mặc định của Java, hệ thống sẽ thực hiện sao chép nông (shallow copy). Điều này có nghĩa là các thuộc tính là kiểu tham chiếu (như List, Array, hoặc các object khác) sẽ không được sao chép giá trị, mà chỉ sao chép con trỏ tham chiếu. Dẫn đến việc bản sao và bản gốc dùng chung một vùng nhớ cho các thuộc tính này.
Để khắc phục, chúng ta cần thực hiện sao chép sâu (deep copy) bằng cách tự động clone các thuộc tính kiểu tham chiếu bên trong phương thức clone().
Định nghĩa lớp Cấu hình Server
import java.util.ArrayList;
import java.util.List;
public class ServerConfig implements Cloneable {
private String environment;
private List<String> activeNodes;
public ServerConfig(String env) {
this.environment = env;
this.activeNodes = new ArrayList<>();
}
public void addNode(String node) {
this.activeNodes.add(node);
}
public List<String> getNodes() {
return this.activeNodes;
}
@Override
public ServerConfig clone() {
try {
ServerConfig cloned = (ServerConfig) super.clone();
// Thực hiện sao chép sâu cho danh sách để tránh dùng chung tham chiếu
cloned.activeNodes = new ArrayList<>(this.activeNodes);
return cloned;
} catch (CloneNotSupportedException e) {
throw new RuntimeException("Quá trình sao chép thất bại", e);
}
}
}
Lớp Client kiểm thử
public class ConfigClient {
public static void main(String[] args) {
ServerConfig primaryConfig = new ServerConfig("Production");
primaryConfig.addNode("Node-A");
ServerConfig backupConfig = primaryConfig.clone();
backupConfig.addNode("Node-B");
System.out.println("Primary nodes: " + primaryConfig.getNodes());
System.out.println("Backup nodes: " + backupConfig.getNodes());
}
}
Kết quả thực thi
Primary nodes: [Node-A]
Backup nodes: [Node-A, Node-B]
Nhờ áp dụng sao chép sâu, việc thêm "Node-B" vào bản sao (backupConfig) không hề ảnh hưởng đến danh sách của bản gốc (primaryConfig).
Biến thể: Prototype Registry (Trình quản lý nguyên mẫu)
Khi hệ thống có nhiều loại prototype khác nhau, việc client phải tự giữ và quản lý các đối tượng gốc sẽ làm mã nguồn trở nên rối rắm. Ta có thể áp dụng biến thể Registry (kết hợp tư tưởng của Factory Pattern) để lưu trữ tập trung và cung cấp các bản sao theo yêu cầu.
Interface và các lớp Component cụ thể
public interface UIComponent extends Cloneable {
UIComponent clone();
void render();
}
public class ButtonComponent implements UIComponent {
@Override
public UIComponent clone() {
try {
return (ButtonComponent) super.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
@Override
public void render() {
System.out.println("Đang hiển thị nút bấm (Button).");
}
}
public class InputComponent implements UIComponent {
@Override
public UIComponent clone() {
try {
return (InputComponent) super.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
@Override
public void render() {
System.out.println("Đang hiển thị trường nhập liệu (Input).");
}
}
Lớp Prototype Registry
import java.util.HashMap;
import java.util.Map;
public class ComponentRegistry {
private Map<String, UIComponent> components = new HashMap<>();
public ComponentRegistry() {
// Tải trước các nguyên mẫu mặc định
components.put("Button", new ButtonComponent());
components.put("Input", new InputComponent());
}
public void register(String key, UIComponent component) {
components.put(key, component);
}
public UIComponent create(String key) {
UIComponent prototype = components.get(key);
if (prototype == null) {
throw new IllegalArgumentException("Không tìm thấy component: " + key);
}
// Trả về một bản sao mới mỗi khi được yêu cầu
return prototype.clone();
}
}
Sử dụng Registry trong Client
public class RegistryClient {
public static void main(String[] args) {
ComponentRegistry registry = new ComponentRegistry();
UIComponent btn1 = registry.create("Button");
btn1.render();
UIComponent input1 = registry.create("Input");
input1.render();
}
}
Kết quả thực thi
Đang hiển thị nút bấm (Button).
Đang hiển thị trường nhập liệu (Input).