In object-oriented programming, the decorator pattern is a design pattern that allows behavior to be added to an individual object, dynamically, without affecting the behavior of other objects from the same class.

Problem

Khi cần thêm một thuộc tính hoặc phương thức vào đối tượng, ta thường sử dụng tính kế thừa (Inheritance). Tuy nhiên, đôi khi việc này lại làm cho chương trình tồn tại nhiều derived class không cần thiết.

Ví dụ, một chiếc bánh pizza có thể có hai loại là cà chua và gà. Khách hàng được quyền chọn thêm các topping khác chẳng hạn như tiêu hoặc phô mai. Dẫn đến, chúng ta sẽ có 6 loại pizza như sau:

  • Cà chua + tiêu
  • Cà chua + phô mai
  • Cà chua + tiêu + phô mai
  • Gà + tiêu
  • Gà + phô mai
  • Gà + tiêu + phô mai

Nếu sử dụng kế thừa để mô hình hóa các loại pizza này, ta sẽ phải tạo ra rất nhiều lớp chẳng hạn như TomatoPepperPizza hoặc TomatoPepperCheesePizza.

Solution

Để giải quyết vấn đề trên, chúng ta có thể sử dụng decorator pattern để mở rộng các phương thức của một object theo một cách linh động hơn. Từ “linh động” ở đây có nghĩa là mở rộng các phương thức của một đối tượng cụ thể nào đó nhưng không làm ảnh hưởng đến các đối tượng khác mà có cùng lớp đối tượng.

Sơ đồ lớp của decorator pattern:

Các thành phần của decorator pattern:

  • Component: một interface chung để các đối tượng cần thêm chức năng có thể implement.
  • Concrete Component: một implementation của component mà có bản chất là một đối tượng cần thêm chức năng.
  • Decorator: là một abstract class duy trì tham chiếu đến component và đồng thời implement các phương thức của component.
  • Concrete Decorator: là một implementation của decorator giúp thêm các chức năng.

Sau khi triển khai thì sơ đồ lớp của bài toán Pizza ở trên sẽ có dạng như sau:

Implementation

Component

Trước tiên, tạo ra interface cho một chiếc bánh pizza như sau:

class IPizza {
public:
	virtual std::string doPizza() = 0;
};

Interface này chính là component của decorator pattern.

Concrete Component

Kế đến, tạo ra các concrete component:

class TomatoPizza : public IPizza {
public:
	std::string doPizza() {
		return "I am a tomato pizza";
	}
};
 
class ChickenPizza : public IPizza {
public:
	std::string doPizza() {
		return "I am a chicken pizza";
	}
};

Lớp TomatoPizzaChickenPizza sẽ cần phải implement phương thức doPizza của IPizza.

Decorator

Tạo ra decorator có bản chất là một implementation của IPizza:

class PizzaDecorator : public IPizza {
protected:
	IPizza* _pizza;
 
public:
	PizzaDecorator(IPizza* pizza) {
		_pizza = pizza;
	}
 
public:
	std::string doPizza() {
		return _pizza->doPizza();
	}
 
	IPizza* getPizza() {
		return _pizza;
	}
 
	void setPizza(IPizza* newPizza) {
		_pizza = newPizza;
	}
};

Decorator này sẽ lưu một con trỏ có kiểu là IPizza.

Concrete Decorator

Triển khai các decorator dùng để thêm các topping vào pizza:

Decorator giúp thêm tiêu:

class PepperDecorator : public PizzaDecorator {
public:
	PepperDecorator(IPizza* pizza) : PizzaDecorator(pizza) {}
 
public:
	std::string doPizza() {
		std::string type = _pizza->doPizza();
		return type + addPepper();
	}
 
private:
	std::string addPepper() {
		return " + pepper";
	}
};

Decorator giúp thêm phô mai:

class CheeseDecorator : public PizzaDecorator {
public:
	CheeseDecorator(IPizza* pizza) : PizzaDecorator(pizza) {}
 
public:
	std::string doPizza() {
		std::string type = _pizza->doPizza();
		return type + addCheese();
	}
 
private:
	std::string addCheese() {
		return " + cheese";
	}
};

Usage

Sử dụng decorator pattern vừa được triển khai ở trên như sau:

void TestDecorator() {
	IPizza* tomatoPizza = new TomatoPizza();
	IPizza* chickenPizza = new ChickenPizza();
 
	std::cout << tomatoPizza->doPizza() << std::endl;
	std::cout << chickenPizza->doPizza() << std::endl;
 
	PepperDecorator* pepperDecorator = new PepperDecorator(tomatoPizza);
	std::cout << pepperDecorator->doPizza() << std::endl;
 
	CheeseDecorator* cheeseDecorator = new CheeseDecorator(chickenPizza);
	std::cout << cheeseDecorator->doPizza() << std::endl;
 
	CheeseDecorator* secondCheeseDecorator = new CheeseDecorator(pepperDecorator);
	std::cout << secondCheeseDecorator->doPizza() << std::endl;
}

Output sẽ là:

I am a tomato pizza
I am a chicken pizza
I am a tomato pizza + pepper
I am a chicken pizza + cheese
I am a tomato pizza + pepper + cheese

Applicability

Sử dụng decorator pattern khi ta cần:

  • Thêm vào những hành vi mới cho đối tượng trong runtime mà không làm thay đổi những đoạn code mà sử dụng đối tượng đó.
  • Khi không thể thêm vào những hành vi mới thông qua kế thừa.

Pros and Cons

ProsCons
Mở rộng hành vi của đối tượng mà không cần tạo ra các subclassKhó để xóa bỏ một decorator cụ thể nào đó khỏi decorator stack
Thêm hoặc xóa đi các chức năng của đối tượng trong runtimeKhó để cài đặt một decorator sao cho nó không bị phụ thuộc vào decorator stack
Có thể kết hợp nhiều hành vi bằng cách bọc bên ngoài đối tượng nhiều decoratorCode cấu hình ban đầu có thể nhìn khá xấu
Single responsibility: chia các class có nhiều hành vi thành các class nhỏ hơn

Resources