Mẫu trang trí
Thuộc phần Lập trình hướng đối tượng trong hành trình C++ của Coddy. Bài học 98 trên 104.
Mẫu Decorator cho phép bạn thêm hành vi mới vào các đối tượng một cách linh hoạt 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 thêm hành vi khi biên dịch, decorator cho phép bạn mở rộng chức năng khi chạy mà không cần sửa đổi lớp ban đầu.
Mẫu này hoạt động bằng cách để cả đối tượng gốc và các bộ trang trí cùng triển khai một giao diện. Mỗi bộ trang trí giữ một tham chiếu đến thành phần và bổ sung hành vi riêng trước hoặc sau khi ủy quyền cho đối tượng được bọc:
#include <iostream>
#include <memory>
// Giao diện thành phần
class Coffee {
public:
virtual std::string getDescription() const = 0;
virtual double getCost() const = 0;
virtual ~Coffee() = default;
};
// Thành phần cụ thể
class SimpleCoffee : public Coffee {
public:
std::string getDescription() const override { return "Coffee"; }
double getCost() const override { return 2.0; }
};
// Lớp trang trí cơ sở
class CoffeeDecorator : public Coffee {
protected:
std::unique_ptr<Coffee> coffee;
public:
CoffeeDecorator(std::unique_ptr<Coffee> c) : coffee(std::move(c)) {}
};
// Các lớp trang trí cụ thể
class MilkDecorator : public CoffeeDecorator {
public:
MilkDecorator(std::unique_ptr<Coffee> c) : CoffeeDecorator(std::move(c)) {}
std::string getDescription() const override {
return coffee->getDescription() + ", Milk";
}
double getCost() const override { return coffee->getCost() + 0.5; }
};
class SugarDecorator : public CoffeeDecorator {
public:
SugarDecorator(std::unique_ptr<Coffee> c) : CoffeeDecorator(std::move(c)) {}
std::string getDescription() const override {
return coffee->getDescription() + ", Sugar";
}
double getCost() const override { return coffee->getCost() + 0.2; }
};Có thể xếp chồng các decorator, mỗi decorator bọc decorator trước đó, bổ sung chức năng theo từng lớp:
int main() {
std::unique_ptr<Coffee> order = std::make_unique<SimpleCoffee>();
order = std::make_unique<MilkDecorator>(std::move(order));
order = std::make_unique<SugarDecorator>(std::move(order));
std::cout << order->getDescription() << ": $" << order->getCost();
// Đầu ra: Coffee, Milk, Sugar: $2.7
}Dùng Decorator khi bạn cần thêm trách nhiệm cho các đối tượng một cách linh hoạt mà không ảnh hưởng đến các đối tượng khác, hoặc khi việc mở rộng chức năng bằng cách tạo lớp con sẽ khiến số lượng lớp tăng lên quá nhiều.
Thử thách
DễHãy xây dựng Hệ thống đặt pizza bằng mẫu Decorator. Bạn sẽ tạo một hệ thống linh hoạt, trong đó khách hàng có thể bắt đầu với một chiếc pizza cơ bản rồi thêm nhiều loại topping: mỗi topping bọc đơn hàng trước đó và thêm chi phí cùng phần mô tả riêng. Đây là trường hợp sử dụng hoàn hảo cho các decorator: bạn có thể kết hợp topping theo bất kỳ cách nào mà không cần tạo ra hàng loạt lớp con.
Bạn sẽ sắp xếp code trong ba tệp:
Pizza.h: Định nghĩa giao diện thành phần và lớp pizza cơ bản.Tạo lớp trừu tượng
Pizzavới các phương thức ảo thuần túygetDescription()(trả về một chuỗi) vàgetCost()(trả về một số thực), cùng với hàm hủy ảo.Sau đó, tạo lớp cụ thể
PlainPizzađại diện cho pizza cơ bản. Lớp này cần trả về"Pizza"làm phần mô tả và có chi phí cơ bản là8.0.ToppingDecorator.h: Xây dựng lớp cơ sở decorator và các decorator topping cụ thể.Tạo lớp
ToppingDecoratorkế thừa từPizzavà chứa mộtstd::unique_ptr<Pizza>trỏ đến thành phần được bọc. Lớp này làm lớp cơ sở cho tất cả decorator topping.Triển khai ba decorator topping cụ thể:
CheeseTopping: thêm", Cheese"vào phần mô tả và1.5vào chi phíPepperoniTopping: thêm", Pepperoni"vào phần mô tả và2.0vào chi phíMushroomTopping: thêm", Mushrooms"vào phần mô tả và1.0vào chi phí
Mỗi decorator cần gọi đến pizza được bọc, sau đó thêm phần đóng góp riêng.
main.cpp: Tạo đơn đặt pizza tùy chỉnh dựa trên đầu vào của người dùng.Đọc ba đầu vào, mỗi đầu vào là
yeshoặcno:- Thêm phô mai?
- Thêm pepperoni?
- Thêm nấm?
Bắt đầu với một
PlainPizza, sau đó bọc nó bằng các decorator topping phù hợp dựa trên đầu vào, theo thứ tự: phô mai, pepperoni, nấm. Cuối cùng, in thông tin đơn đặt hàng theo định dạng sau:Order: [description] Total: $[cost]
Ví dụ, với đầu vào yes, yes, no:
Order: Pizza, Cheese, Pepperoni
Total: $11.5Với đầu vào yes, no, yes:
Order: Pizza, Cheese, Mushrooms
Total: $10.5Với đầu vào no, no, no:
Order: Pizza
Total: $8Với đầu vào yes, yes, yes:
Order: Pizza, Cheese, Pepperoni, Mushrooms
Total: $12.5Lưu ý cách mỗi decorator bọc decorator trước đó, lần lượt xây dựng phần mô tả và chi phí. Bạn có thể dễ dàng thêm topping mới bằng cách tạo các lớp decorator mới mà không cần sửa code pizza hoặc topping hiện có: đó chính là ưu điểm của mẫu Decorator.
Tự làm thử
#include <iostream>
#include <string>
#include <memory>
#include "Pizza.h"
#include "ToppingDecorator.h"
using namespace std;
int main() {
// Đọc ba giá trị đầu vào (yes hoặc no)
string addCheese, addPepperoni, addMushrooms;
cin >> addCheese;
cin >> addPepperoni;
cin >> addMushrooms;
// TODO: Bắt đầu với PlainPizza bằng std::unique_ptr
// TODO: Nếu addCheese là "yes", bọc pizza bằng CheeseTopping
// TODO: Nếu addPepperoni là "yes", bọc pizza bằng PepperoniTopping
// TODO: Nếu addMushrooms là "yes", bọc pizza bằng MushroomTopping
// TODO: In đơn hàng theo định dạng:
// Đơn hàng: [description]
// Tổng cộng: $[cost]
return 0;
}
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
1Nền tảng về OOP
Tệp bên ngoàiBiên dịch và tạo bản dựng C++Tệp header và tệp nguồnNamespace và phạm viGiới thiệu về OOP trong C++Lớp và đối tượngCon trỏ thisPhương thức (hàm thành viên)Thuộc tính (dữ liệu thành viên)Cơ bản về hàm khởi tạo và hàm hủyÔn tập: máy tính đơn giản4Thuộc tính của lớp
Thành viên đối tượng và thành viên tĩnhHàm truy xuất và hàm thiết lậpHàm thành viên hằngTừ khóa mutablePhương thức và biến tĩnhHàm và lớp bạnÔn tập: Trình quản lý tài khoản ngân hàng7Kế thừa
Kế thừa cơ bảnMức truy cập trong kế thừaThứ tự gọi hàm khởi tạo và hàm hủyGhi đè phương thứcHàm ảo và VTableĐa kế thừaKế thừa ảoÔn tập: hệ phân cấp nhân viên2Quản lý bộ nhớ
Bộ nhớ ngăn xếp và heapCon trỏ và tham chiếuBộ nhớ động (new/delete)Con trỏ thông minh trong C++RAII trong C++Ôn tập: Trình quản lý mảng động5Tính đóng gói
Phạm vi truy cập trong C++Tìm hiểu sâu về phạm vi truy cậpẨn thông tinStruct và lớpLớp lồng nhau và lớp bên trongÔn tập: hệ thống hồ sơ sinh viên8Đa hình
Đa hình lúc biên dịch và khi chạyNạp chồng hàmÔn lại hàm ảoHàm ảo thuần túyLớp trừu tượngThiết kế interface trong C++Ép kiểu động và RTTIÔn tập: Máy tính hình học11Các khái niệm OOP nâng cao
Kết hợp so với kế thừaMixin qua CRTPMẫu PimplXóa kiểuLớp enum và định kiểu mạnhXử lý ngoại lệ trong OOPPhân cấp ngoại lệ tùy chỉnh14Mẫu thiết kế phần 2
Mẫu lệnhMẫu bộ chuyển đổiMẫu trang tríMẫu phương thức khuôn mẫuMẫu trạng tháiMẫu tổng hợpRAII như một mẫu thiết kế3Hàm khởi tạo và hàm hủy
Hàm khởi tạo mặc địnhHàm khởi tạo có tham sốHàm khởi tạo sao chépHàm khởi tạo di chuyểnDanh sách khởi tạoHàm khởi tạo ủy quyềnTìm hiểu sâu về hàm hủyQuy tắc 3 / 5 / 0Ôn tập: lớp chuỗi6Nạp chồng toán tử
Giới thiệu về nạp chồng toán tửNạp chồng toán tử số họcNạp chồng toán tử so sánhToán tử luồngNạp chồng toán tử gánNạp chồng toán tử [] và ()Toán tử chuyển đổi kiểuÔn tập: lớp ma trận9Mẫu
Mẫu hàmMẫu lớpChuyên biệt hóa mẫuMẫu biến tham sốCơ bản về SFINAE và đặc điểm kiểuÔn tập: vùng chứa tổng quátTự luyện tập: Trình biên dịch C++ online