+2

Design Pattern - Decorator

Tiếp tục chủ đề về design pattern, pattern của ngày hôm nay là Decorator, một pattern thuộc nhánh Structural Pattern.

Định nghĩa

Decorator là gì? Mình xin phép bê nguyên định nghĩa từ wikipedia cho tiện.

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

Decorator thường được dùng khi ta muốn thêm chức năng cho một đối tượng đã tồn tại trước đó, mà không muốn ảnh hưởng đến các đối tượng khác. Ví dụ, giả sử khách hàng muốn có thêm chức năng video trên website, sau khi ta đã hoàn thành các chức năng được yêu cầu. Thay vì sửa lại thành phần ban đầu, ta có thể "decorate" một thành phần nào đó với chức năng video. Bằng cách này thì chúng ta vừa bảo lưu được các thành phần đã xây dựng không bị chỉnh sửa, vừa thêm được chức năng mới.

Thông thường website của chúng ta đã đáp ứng được các nhu cầu của khách hàng và vận hành trơn tru. Tuy nhiên có một số chức năng tùy chọn có thể phù hợp với những nhu cầu khác nhau. Chẳng hạn một đoạn text có thể có scroll hoặc không. Với Decorator pattern, ta có thể tạo ra những chức năng cốt lõi, và trang trí (decorate) cho nó các chức năng khác tùy nhu cầu.

Đặt vấn đề

Lý thuyết tất nhiên là khó hiểu, nên tốt nhất là nên đi kèm ví dụ minh họa. Bài toán đặt ra như sau. Giả sử ta có một cửa hàng trà sữa chẳng hạn, tất nhiên là nó bán trà sữa rồi. Menu đầu tiên rất đơn giản, dựng một class trà sữa (ví dụ minh họa nên mình không đi sâu vào mấy quy tắc bla bla...):

class MilkTea
{
	public function cost()
	{
		return 30;
	}
}

Ô cê. Tạm thời là mỗi ly trà sữa giá 30 cành. Ngặt nỗi, khách hàng đến mua trà sữa, họ lại muốn bỏ thêm trân châu. Giải pháp đầu tiên là mình extend class MilkTea, tạo thêm món mới:

class PearlMilkTea extends MilkTea
{
	public function cost()
	{
		return parent::cost() + 10;
	}
}

Trà sữa trân châu thì thêm 10 cành là 40 cành, ez. Khổ cái là không phải ông nào cũng thích trân châu, có ông thích bỏ pudding vào, giờ sao, ta lại hì hục viết thêm class mới:

class PuddingMilkTea extends MilkTea
{
    public function cost()
    {
        return parent::cost() + 5;
    }
}

Đến đây bạn ngỡ rằng đã được thở phào nhẹ nhõm, nhưng không, bạn chợt nhận ra nếu mai sau mà khách dở chứng đòi thêm một mớ thành phần thập cẩm khác, không lẽ mỗi lần lại phải thêm món mới (mà chưa chắc người khác đã thích), chưa kể có người thích bỏ cả trân châu và pudding, người thì muốn trà sữa trân châu 2/3 đường, bạn thấy quá bị động. Xem ra giải pháp subclass có vẻ không khả thi. Đây chính là lúc mà Decorator pattern phát huy hiệu quả, thay vì extend class MilkTea liên tục và tạo nên các món mới kiểu PearlMilkTea, PuddingMilkTea mà không phải ai cũng thích (tùy chọn), ta sẽ tách trà sữa thành một đối tượng riêng biệt và để khách hàng tự chọn sẽ thêm hoặc không thêm thành phần mới.

Triển khai Decorator cơ bản

Đầu tiên là tạo một interface:

interface IngredientInterface
{
	public function cost();
}

Giờ ta cần viết lại class MilkTea như sau:

class MilkTea implements IngredientInterface
{
    private $description = 'Milk Tea';

    public function getDescription()
    {
        return $description;
    }

	public function cost()
	{
		return 30;
	}
}

Mục đích của function getDescription đơn giản là giúp ta biết rõ mình đang thêm những gì vào trà sữa, nhằm minh họa tốt hơn cho ví dụ này. Giờ ta tạo thêm một class mới, gọi là IngredientDecorator (nhìn vào UML sẽ thấy đây là class cha của các class decorator):

abstract class IngredientDecorator implements IngredientInterface
{
    protected $ingredient;
    
    public function __construct(IngredientInterface $ingredient)
    {
        $this->ingredient = $ingredient;
    }
}

để abstract vì ta chưa biết sẽ thêm gì vào trà sữa. Ok, giờ là lúc xem class Pearl mới sẽ có gì khác:

class Pearl extends IngredientDecorator
{
    public function cost()
    {
        return $this->ingredient->cost() + 10;
    }
    
    public function getDescription()
    {
        return $this->ingredient->getDescription() . ', Pearl';
    }
}

Tương tự cho Pudding:

class Pudding extends IngredientDecorator
{
    public function cost()
    {
        return $this->ingredient->cost() + 5;
    }
    
    public function getDescription()
    {
        return $this->ingredient->getDescription() . ', Pudding';
    }    
}

Bạn có thể thấy thuộc tính $ingredient của class Pearl là một đối tượng kiểu IngredientInterface, hay nói cách khác, nó có thể là MilkTea, có thể Pudding, ... Điều này cho phép chúng ta thêm không giới hạn món phụ vào cốc trà sữa, và bản thân Pearl cũng không cần biết cái $ingredient của nó đã chứa những gì, chỉ cần vác ra xài thôi. Vậy là xong, giờ chúng ta thử test xem sao:

$object1 = new MilkTea();
$object2 = new Pearl($object1);
echo $object1->getDescription() . ': ';
echo $object1->cost();
echo $object2->getDescription() . ': ';
echo $object2->cost();

được kết quả là:

Milk Tea: 30
Milk Tea, Pearl: 40

Ngon rồi, giờ nếu ta muốn thêm pudding và cả trân châu vào trà sữa thì sao, rất đơn giản:

$object3 = new Pudding(new Pearl(new MilkTea()));
echo $object3->getDescription() . ': ';
echo $object3->cost();

kết quả:

Milk Tea, Pearl, Pudding: 45

Giờ nếu khách hàng có muốn bỏ một lô lốc đồ vào trà sữa cũng không phải là vấn đề, đặc biệt là ta được phép tùy chọn, dùng khi có nhu cầu thay vì phải tạo một món mới như giải pháp ban đầu. Ta có thể thấy Decorator pattern cho phép ta "decorate" một đối tượng theo run-time thay vì compile-time. Một lưu ý nữa là việc cài đặt interface IngredientInterface ở cả MilkTea và IngredientDecorator rất quan trọng. Nếu không cài đặt interface này thì rõ ràng ta chỉ kết hợp được trà sữa với một thành phần nào đó như trân châu hoặc pudding, thay vì cả 2.

Vậy là mình đã giới thiệu xong về Decorator pattern, văn vở vẫn còn lủng củng, mọi người có ý kiến hoặc chỉnh sửa gì cho bài viết của mình cứ comment ở dưới nhé. Thanks for reading. Source code: https://github.com/nghiadd90/design-pattern


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí