Menu
CoddyTech

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.

challenge icon

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 Pizza với các phương thức ảo thuần túy getDescription() (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 ToppingDecorator kế thừa từ Pizza và chứa một std::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.5 vào chi phí
    • PepperoniTopping: thêm ", Pepperoni" vào phần mô tả và 2.0 vào chi phí
    • MushroomTopping: thêm ", Mushrooms" vào phần mô tả và 1.0 và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à yes hoặc no:

    1. Thêm phô mai?
    2. Thêm pepperoni?
    3. 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.5

Với đầu vào yes, no, yes:

Order: Pizza, Cheese, Mushrooms
Total: $10.5

Với đầu vào no, no, no:

Order: Pizza
Total: $8

Với đầu vào yes, yes, yes:

Order: Pizza, Cheese, Pepperoni, Mushrooms
Total: $12.5

Lư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;
}
quiz iconTự kiểm tra

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

Tự luyện tập: Trình biên dịch C++ online