Mẫu Decorator
Thuộc phần Lập trình hướng đối tượng trong hành trình Java của Coddy. Bài học 72 trên 87.
Mẫu Decorator là một mẫu thiết kế cấu trúc cho phép bạn gắn thêm hành vi mới vào các đối tượng bằng cách bọc chúng trong những đối tượng đặc biệt gọi là decorator. Khác với kế thừa, vốn bổ sung hành vi tại thời điểm biên dịch, decorator bổ sung chức năng một cách linh hoạt trong lúc chạy mà không sửa đổi lớp ban đầu.
Hãy tưởng tượng một quán cà phê, nơi bạn bắt đầu với một ly cà phê cơ bản rồi có thể thêm các món như sữa, đường hoặc kem tươi. Mỗi món thêm vào sẽ bao bọc ly cà phê ban đầu, đồng thời cộng thêm chi phí và phần mô tả. Mẫu thiết kế này dùng một giao diện chung để có thể xử lý các đối tượng đã trang trí giống như đối tượng ban đầu:
interface Coffee {
String getDescription();
double getCost();
}
class SimpleCoffee implements Coffee {
public String getDescription() {
return "Simple Coffee";
}
public double getCost() {
return 2.0;
}
}
abstract class CoffeeDecorator implements Coffee {
protected Coffee coffee;
public CoffeeDecorator(Coffee coffee) {
this.coffee = coffee;
}
}
class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
public String getDescription() {
return coffee.getDescription() + ", Milk";
}
public double getCost() {
return coffee.getCost() + 0.5;
}
}Decorator giữ tham chiếu đến đối tượng được bọc và chuyển tiếp các lệnh gọi đến đối tượng đó, đồng thời bổ sung hành vi riêng. Bạn có thể xếp chồng nhiều decorator để kết hợp các tính năng:
Coffee order = new SimpleCoffee();
order = new MilkDecorator(order);
order = new SugarDecorator(order);
System.out.println(order.getDescription()); // Simple Coffee, Milk, Sugar
System.out.println(order.getCost()); // 2.75Mẫu Decorator phát huy hiệu quả khi bạn cần bổ sung trách nhiệm cho các đối tượng mà không tạo ra quá nhiều lớp con. Mỗi decorator hoạt động độc lập, vì vậy bạn có thể tự do kết hợp chúng để tạo ra chính xác tổ hợp hành vi mình cần.
Thử thách
DễHãy xây dựng một hệ thống thông báo bằng mẫu thiết kế Decorator! Bạn sẽ tạo một thông báo cơ sở có thể được mở rộng bằng nhiều kênh gửi khác nhau, gồm cảnh báo qua email, cảnh báo SMS và thông báo đẩy, bằng cách lồng các decorator vào nhau. Cách này phản ánh những hệ thống thực tế, nơi một thông điệp duy nhất có thể cần được gửi đồng thời qua nhiều kênh.
Bạn sẽ sắp xếp code trong bốn tệp:
Notifier.java: Định nghĩa giao diệnNotifier, đóng vai trò là giao diện thành phần cho mẫu thiết kế decorator. Giao diện này cần khai báo phương thứcsend(String message)để xử lý việc gửi thông báo.BasicNotifier.java: Tạo lớp thành phần cụ thể triển khaiNotifier. Lớp này đại diện cho hình thức thông báo đơn giản nhất: cảnh báo trong ứng dụng. Khi gọisend, lớp sẽ in raIn-App: [message].NotifierDecorators.java: Xây dựng hệ thống phân cấp decorator tại đây. Bắt đầu bằng lớp trừu tượngNotifierDecorator, lớp này triển khaiNotifiervà giữ tham chiếu đến một đối tượngNotifierđược bọc (truyền vào qua hàm khởi tạo).Sau đó, tạo ba decorator cụ thể kế thừa
NotifierDecorator:EmailDecorator- Khi gọisend, trước tiên lớp này chuyển tiếp lời gọi đến notifier được bọc, sau đó in raEmail: [message].SMSDecorator- Khi gọisend, trước tiên lớp này chuyển tiếp lời gọi đến notifier được bọc, sau đó in raSMS: [message].PushDecorator- Khi gọisend, trước tiên lớp này chuyển tiếp lời gọi đến notifier được bọc, sau đó in raPush: [message].Main.java: Kết hợp hệ thống decorator của bạn! Bạn sẽ nhận hai đầu vào: thông điệp thông báo (String) và danh sách các kênh cần thêm, được phân tách bằng dấu phẩy (String), chẳng hạn như"email,sms"hoặc"push,email,sms".Bắt đầu với một
BasicNotifier. Sau đó, dựa trên đầu vào về các kênh, bọc nó bằng các decorator phù hợp theo thứ tự xuất hiện. Tên các kênh hợp lệ làemail,smsvàpush.Cuối cùng, gọi
sendvới thông điệp của bạn trên notifier đã được bọc đầy đủ.
Bạn sẽ nhận hai đầu vào theo thứ tự: thông điệp cần gửi (String) và các kênh cần bật (String, phân tách bằng dấu phẩy).
Ví dụ, với đầu vào Server is down! và email,push, đầu ra sẽ là:
In-App: Server is down!
Email: Server is down!
Push: Server is down!Lưu ý cách mỗi decorator thêm hành vi riêng mà vẫn giữ nguyên thông báo ban đầu. Thứ tự lồng các lớp quyết định thứ tự đầu ra: thành phần trong cùng (BasicNotifier) chạy trước, sau đó mỗi decorator thêm lớp xử lý của mình. Bạn có thể kết hợp tùy ý các kênh mà không cần tạo lớp riêng cho từng tổ hợp!
Tự làm thử
import java.util.Scanner;
class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// Đọc thông điệp cần gửi
String message = scanner.nextLine();
// Đọc danh sách kênh phân tách bằng dấu phẩy (ví dụ: "email,sms" hoặc "push,email,sms")
String channelsInput = scanner.nextLine();
// TODO: Bắt đầu với BasicNotifier
// TODO: Tách đầu vào channels theo dấu phẩy
// TODO: Lặp qua từng kênh và bọc notifier bằng decorator phù hợp
// Các kênh hợp lệ là: "email", "sms", "push"
// TODO: Gọi send trên notifier đã được bọc đầy đủ, truyền vào thông điệp
}
}
Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.
Tất cả bài học trong Lập trình hướng đối tượng
1Kiến thức cơ bản về lập trình hướng đối tượng
Tệp bên ngoàiGiới thiệu về lập trình hướng đối tượngLớp và đối tượngTừ khóa thisPhương thứcTrường (thuộc tính)Phương thức khởi tạoNạp chồng phương thức khởi tạoÔn tập: Máy tính đơn giản4Kế thừa
Kế thừa cơ bản (extends)Từ khóa superGhi đè phương thức (@Override)Chuỗi gọi hàm khởi tạoLớp ObjectKế thừa đơn và đa cấpVì sao không hỗ trợ kế thừa nhiều lớpÔn tập: Phân cấp lớp Employee7Phương thức đặc biệt và lớp Object
Phương thức toString()equals() và hashCode()Phương thức clone()compareTo() và ComparableGiao diện ComparatorÔn tập: sắp xếp tùy chỉnh2Bộ điều chỉnh quyền truy cập và đóng gói
Tổng quan về các mức truy cậpPhương thức getter và setterẨn thông tinTừ khóa finalÔn tập: Trình quản lý tài khoản ngân hàng5Tính đa hình
Kiến thức cơ bản về nạp chồng phương thứcGhi đè phương thức (thời gian chạy)Chuyển kiểu lên và chuyển kiểu xuốngToán tử instanceofLớp và phương thức trừu tượngÔn tập: máy tính hình học8Khái niệm OOP nâng cao
Kết hợp và kế thừaKết tập và hợp thànhLớp bên trong, lớp lồng nhau và lớp ẩn danhKiểu enum và phương thức enumBản ghi (Java 16+)Lớp sealed (Java 17+)3Thuộc tính lớp và thành viên tĩnh
Biến của instance và biến tĩnhPhương thức tĩnhKhối tĩnhHằng số (static final)Ôn tập: bộ đếm và tiện ích6Giao diện và lớp trừu tượng
Giới thiệu về giao diệnTriển khai giao diệnTriển khai nhiều giao diệnPhương thức mặc định và tĩnh trong giao diệnLớp trừu tượng và giao diệnGiao diện hàmÔn tập: hệ thống thanh toán9Kiểu tổng quát
Giới thiệu về kiểu tổng quátLớp tổng quátPhương thức tổng quátTham số kiểu có giới hạnKý tự đại diện (?, extends, super)Ôn tập: vùng chứa tổng quát12Mẫu thiết kế, phần 2
Mẫu CommandMẫu AdapterMẫu DecoratorMẫu Template MethodMẫu StateMẫu CompositeMẫu IteratorTự luyện tập: Trình biên dịch Java online