Decorator Pattern ra đời hỗ trợ xử lý bài toán mà Lập Trình Viên thường xuyên gặp phải trong quá trình phát triển. Đó chính là sự thay đổi (nâng cấp, sửa chữa) hệ thống.
Với các yêu cầu như vậy, chúng ta thường nghĩ tới việc kế thừa (extends). Nhưng kế thừa cũng có 1 số hạn chế nhất định như việc không thể linh động thay đổi lúc runtime. Để khắc phục điều này, chúng ta có 1 khái niệm mới đó là Decorator Pattern.
Trong các phần trước của bài viết chúng ta đã tìm hiểu về Adapter Pattern, Bridge Pattern, Composite Pattern… Trong bài này cùng CodeTuTam tìm hiểu Decorator Pattern trong PHP nhé.
Decorator Pattern là gì?
Decorator Pattern là 1 mẫu thiết kế cho phép chúng ta thêm các hành vi mới vào 1 đối tượng đã có. Việc này thực hiện bằng cách wrapper đối tượng đã có bằng 1 đối tượng mới. Thông qua đối tượng wrapper này chúng ta sẽ cung cấp thêm các hành vi mong muốn vào
Hay nói cách khác, Decorator Pattern cho phép chúng ta thêm chức năng mới vào đối tượng đã có mà không ảnh hưởng đến các đối tượng khác (đã liên kết với đối tượng đã có).
Mỗi khi cần thêm tính năng mới,chúng ta có thể đưa đối tượng hiện có wrap trong 1 đối tượng mới – gọi là Decorator Class.
Tham khảo thêm thông tin trên wiki về Decorator Pattern
Decorator Pattern giải quyết vấn đề gì?
Để hiểu rõ hơn Decorator Pattern giải quyết vấn đề gì, chúng ta xem thử ví dụ sau đây.
Hãy tưởng tượng, chúng ta đang xây dựng 1 thư viện nhỏ cho phép gửi thông báo cho người dùng mỗi khi có 1 thông tin mới.
Ứng dụng cho thư viên này như là phần mềm nhắc lịch, ứng dụng đọc báo, hay xem kết quả thể thao…
Phiên bản đầu tiên của ứng dụng này sẽ rất đơn giản. Nó chỉ có 1 class Notifier với hàm khởi tạo, 1 vài thuộc tính như $message và hàm send để gửi thông báo. Tính năng thông báo cũng chỉ hỗ trợ duy nhất qua phương thức Email.
Không quá khó khăn để thực hiện, bất kì ứng dụng (tích hợp thư viện Notifier) nào muốn gửi thông báo qua email, chỉ cần gọi tới hàm send của Notifier.
Sau 1 thời gian vận hành, các khách hàng của chúng ta mong muốn nhận thông báo qua nhiều kênh khác nhau, không đơn thuần chỉ là Email nữa. Ví dụ với các thông báo khẩn cấp mong muốn nhận thông báo qua SMS. Hay thậm chí là thông báo qua Facebook, Slack, Firebase Cloud Message…
Giải pháp trước mắt là xây dựng class con kế thừa Notifier, trong class này sẽ có thêm hàm mở rộng để thực hiện chức năng mới.
Xem chừng mọi thứ đã khá ổn thỏa với bài toán kế thừa, nhưng không…
Một khách hàng hỏi chúng ta rằng:”Tại sao lại không cho phép gửi thông báo qua nhiều kênh khác nhau cùng 1 lúc, điều này thực sự cần thiết”.
Điển hình cho việc này, nếu có 1 sự kiện vô cùng hệ trọng, chúng ta mong muốn nhận được thông báo qua mọi kênh có thể”.
Và đây là kết quả:
Số lượng class đã tăng lên gấp bội, và điều này sẽ lặp lại nếu hệ thống tiếp tục mở rộng hơn nữa. Đây chính là lúc chúng ta cần nghĩ tới mẫu thiết kế Decorator Pattern.
Decorator pattern giải quyết vấn đề như thế nào?
Kế thừa là điều đầu tiên mà nhiều người nghĩ tới khi cần thay đổi cách hoạt động của 1 vài phương thức (hành vi). Tuy vậy kế thừa có những nhược điểm của nó
- Kế thừa là tĩnh. Nghĩa là chúng ta không thể thay đổi hành vi của 1 đối tượng lúc runtime. Chúng ta chỉ có thể thay thế đối tượng đó bằng các sử dụng subclass mới.
- Trong PHP và hầu hết tất cả các ngôn ngữ lập trình. Class con chỉ được phép có 1 class cha mà thôi (Không tồn tại đa kế thừa).
Giải pháp cho việc này là chúng ta sử dụng Composition (cấu trúc thành phần) thay vì dùng kế thừa. Composition là một đối tượng sẽ chứa tham chiếu đến đối tượng khác, ủy thác cho nó 1 số công việc.
Decorator pattern cũng có thể gọi là Wrapper. Đối tượng wrapper sẽ chứa 1 đối tượng của Class ban đầu. Trong wrapper cũng chưa các hàm, lệnh giống như class gốc, và nó sẽ chuyển tiếp yêu cầu thực hiện cho đối tượng gốc đó. Tuy nhiên wrapper có thể thực hiện 1 vài hành động trước hoặc sau khi đối tượng gốc xử lý.
Wrapper sẽ implement 1 interface giống như đối tượng được bao bọc trong nó. Do đó với các lớp Client ( lớp thực thi) thì các đối tượng này hoàn toàn giống nhau.
Như mô hình trên, ta có thể coi mặc định trong Notifier có sẵn phương thức gửi thông báo qua Email.
BaseDecorator sẽ chứa đôi tượng Notifier, đồng thời cũng có phương thức send tương ứng ( do cùng implement 1 interface)
Các class, SmsDecorator, FacebookDecorator, SlackDecorator sẽ kế thừa BaseDecorator. Trong các hàm send của những class này có thể có các bước tiền xử lý hoặc hậu xử lý dữ liệu
Chương trình của chúng ta sẽ như sau
Các bạn có thể xem lại nội dung của bài viết này ở trên refacoring.guru
Ví dụ Decorator Pattern trong thực tế
Việc mặc quần áo ấm có thể coi là 1 ví dụ về decorator. Khi lạnh, ta mặc thêm áo ấm. Trời trở nên lạnh hơn nữa, ta mặc lên mình chiếc áo khoác. Sau đó trời mưa, ta lại khóac lên mình chiếc áo mưa.
Các chiếc áo này đều mở rộng hành vi của ta, nhưng nó không phải 1 phần của chúng ta. Chúng ta có thể cởi ra bất kì chiếc áo nào khi thấy không cần thiết nữa
Ví dụ về quản lý nhân viên
Trong hệ thống quản lý dự án sẽ có nhiều vai trò khác nhau. Ví dụ như : Thành viên nhóm (Team Member), Trưởng nhóm (Team Leader), Người quản lý (Manager)
Mỗi một người sẽ có các công việc cụ thể khác nhau.
Team Member: báo cáo tiến độ task được giao( report Task), Cộng tác cùng thành viên khác (coordinate)
Team Leader: Lên kế hoạch (planning), hỗ trợ thành viên (motivate), giám sát chất lượng (monitor)
Manager: Tạo yêu cầu dự án( Create Requirement), Giao nhiệm vụ cho thành viên (Assign Task), quản lý tiến độ dự án (Progress Management)
Employee: (nhân viên thông thường) : Thực hiện công việc (doTask), Tham gia dự án (join), rời khỏi dự án (terminate)
Khi 1 thành viên trong nhóm trở thành 1 team leader, chúng ta phải tạo đối tượng mới của Team Leader. Đối tượng trước đó của Team member đó có thể bị hủy bỏ, không tái sử dụng được.
Cách làm này có phần không ổn, bởi lẽ đây chỉ là việc tăng cấp cho nhân viên. Vì Team Member đó vẫn là 1 phần của hệ thống, không bị mất đi.
Hoặc trong 1 trường hợp khác, 1 Team Member cũng có thể có trách nhiệm như 1 Team Lead, hoặc 1 manager (Kiêm quản lý). Khi đó chúng ta sẽ tạo 2 đối tượng cho 1 nhân viên. Việc này hoàn toàn không chính xác.
Cài đặt Decorator Pattern trong PHP
Các thành phần trong mẫu thiết kế Decorator:
Component: là một interface quy định các method chung cần phải có cho tất cả các thành phần tham gia vào mẫu này.
ConcreteComponent: là lớp thể hiện (implements) các phương thức của Component.
Decorator: là một abstract class dùng để duy trì một tham chiếu của đối tượng Component và đồng thời cài đặt các phương thức của Component interface.
ConcreteDecorator: là lớp hiện thực (implements) các phương thức của Decorator, nó cài đặt thêm các tính năng mới cho Component.
Client: đối tượng sử dụng Component.
Ví dụ Decorator Pattern trong PHP
<?php interface EmployeeComponent{ public function getName():string; public function doTask():void; public function join(\DateTime $date):void; public function terminate(\DateTime $date):void; } class EmployeeConcreteComponent implements EmployeeComponent{ protected $name; public function __construct($name){ $this->name = $name; } public function getName():string{ return $this->name; } public function doTask():void{ echo $this->name." làm công việc hàng ngày....".PHP_EOL; } public function join(\DateTime $date):void{ echo $this->name." tham gia dự án lúc ".$date->format('d/m/Y').PHP_EOL; } public function terminate(\DateTime $date):void{ echo $this->name." rời dự án lúc ".$date->format('d/m/Y').PHP_EOL; } } abstract class EmployeeDecorator implements EmployeeComponent{ protected $employeeComponent; public function __construct(EmployeeComponent $ec){ $this->employeeComponent = $ec; } public function getName():string{ return $this->employeeComponent->getName(); } public function join(\DateTime $date):void{ echo $this->employeeComponent->getName()." join ".$date->format('d/m/Y').PHP_EOL; } public function terminate(\DateTime $date):void{ echo $this->employeeComponent->getName()." terminate ".$date->format('d/m/Y').PHP_EOL; } } class TeamMember extends EmployeeDecorator{ public function reportTask():void{ echo $this->getName()." báo cáo công việc".PHP_EOL; } public function coordinateWithOthers():void{ echo $this->getName()." phối hợp cùng thành viên khác".PHP_EOL; } public function doTask():void{ $this->employeeComponent->doTask(); $this->reportTask(); $this->coordinateWithOthers(); } } class TeamLeader extends EmployeeDecorator{ public function planning():void{ echo $this->getName()." Lên kế hoạch".PHP_EOL; } public function motivate():void{ echo $this->getName()." hỗ trợ thành viên".PHP_EOL; } public function monitor():void{ echo $this->getName()." quản lý".PHP_EOL; } public function doTask():void{ $this->employeeComponent->doTask(); $this->planning(); $this->motivate(); $this->monitor(); } } class Manager extends EmployeeDecorator{ public function createRequirement():void{ echo $this->getName()." tạo yêu cầu dự án".PHP_EOL; } public function assignTask():void{ echo $this->getName()." giao nhiệm vụ".PHP_EOL; } public function managerProgress():void{ echo $this->getName()." quản lý tiến độ".PHP_EOL; } public function doTask():void{ $this->employeeComponent->doTask(); $this->createRequirement(); $this->assignTask(); $this->managerProgress(); } }
Chúng ta có 1 số hàm phụ trợ để tạo các đối tượng tương ứng như sau
// Tạo 1 nhân viên mới, 1 nhân viên bất kì chưa có trong hệ thống function getEmployee($name='Employee'):EmployeeComponent{ $employeeConcreteComponent = new EmployeeConcreteComponent($name); return $employeeConcreteComponent; } //Thêm nhân viên vào dự án //nhân viên sẽ trở thành Team Member function addEmployeeToProject(EmployeeComponent $employee):EmployeeComponent{ $employee->join(new DateTime); return new TeamMember($employee); } // Team member trở thành teamLeader function upgradeToLeader(EmployeeComponent $employee):EmployeeComponent{ return new TeamLeader($employee); } // Kiêm quyền quản lý Manager function holdManager(EmployeeComponent $employee):EmployeeComponent{ return new Manager($employee); }
// Tạo 1 nhân viên mới Tên : Nam $employeeNam = getEmployee('Nam'); $employeeNam->doTask(); echo '-----------------------------'.PHP_EOL; // Tạo 1 nhân viên mới Tên : Tuấn $employeeTuan = getEmployee('Tuấn'); $employeeTuan->doTask(); echo '-----------------------------'.PHP_EOL; // Khí có nhu cầu thêm thành viên vào nhóm dự án // Thêm Mr Nam vào dự án, Nam chính thwucs trở thành team Member $teamMember = addEmployeeToProject($employeeNam); $teamMember->doTask(); echo '-----------------------------'.PHP_EOL; // Sau 1 thời gian làm tốt, Nam được xét lên làm TeamLeader $teamLeader = upgradeToLeader($employeeNam); $teamLeader->doTask(); echo '-----------------------------'.PHP_EOL; // TIếp tục làm tốt, //Nam được sếp tin tưởng cho kiêm nhiệm thêm chức vụ QUản lý - Manager $teamLeaderManager = holdManager($teamLeader); $teamLeaderManager ->doTask();
Và đây là kết quả
Nam làm công việc hàng ngày.... ----------------------------- Tuấn làm công việc hàng ngày.... ----------------------------- Nam tham gia dự án lúc 12/05/2020 Nam làm công việc hàng ngày.... Nam báo cáo công việc Nam phối hợp cùng thành viên khác ----------------------------- Nam làm công việc hàng ngày.... Nam Lên kế hoạch Nam hỗ trợ thành viên Nam quản lý ----------------------------- Nam làm công việc hàng ngày.... Nam Lên kế hoạch Nam hỗ trợ thành viên Nam quản lý Nam tạo yêu cầu dự án Nam giao nhiệm vụ Nam quản lý tiến độ
Như chúng ta thấy, mặc dù vẫn là Mr Nam nhưng chức vụ, hoặc công việc thay đổi vào mỗi lúc theo cấp bậc thăng tiến. Dù mọi thứ thay đổi nhưng cái cốt lõi là người tên Nam vẫn được giữ nguyên.
Sử dụng Decorator Pattern khi nào?
- Sử dụng khi muốn thay đổi hành vi của 1 đối tượng trong lúc runtime mà không muốn chỉnh sửa đối tượng đó.
- Sử dụng trong trường hợp không thể sử dụng kế thừa vì 1 lý do nào đó, hoặc do mã nguồn sử dụng từ khóa final
- Trong 1 số trường hợp nếu viết theo hướng kế thừa thì mất quá nhiều công sức.
Ưu và nhược điểm khi sử dụng Decorator Pattern
Ưu điểm | Nhược điểm |
Có thể mở rộng đối tượng mà không cần phải tạo lớp con mới (subclass) | Decorator bố trí theo dạng từng cấp bậc, do vậy khó tạo ra các hành vi mà không phụ thuộc thứ tự cấp bậc của Decorator (Decorator Stack). |
Có thể dùng nhiều Wrapper vào với nhau để tạo ra tổ hợp hành vi (Xem lại ví dụ bên trên) | |
Nguyên tắc đơn chức năng. Decoratior Pattern giúp chúng ta có thể chia class thành các class nhỏ với nhiệm vụ cụ thể dễ dàng | |
Có thể thêm, loại bỏ chức năng của đối tượng trong lúc runtime |
So sánh Decorator và Adapter Pattern
Nếu để ý, bạn sẽ thấy Decorator và Adapter Pattern đều chia sẻ cách gọi là Wrapper Pattern. Việc này xuất phát từ cấu trúc triển khai của nó
Về cơ bản 2 mẫu thiết kế này có sự giống nhau
+ Cùng là mẫu thiết kế cấu trúc (Structural Pattern)
+ Cùng sử dụng cơ chế Composition
Khác nhau
+ Adapter ngoài cơ chế Composition thì có thể sử dụng kế thừa, còn Decorator thì không.
+ Decorator có xu hướng hoạt động trên nhiều đối tượng
+ Adapeter mục đích sử dụng để chuyển đổi, thay đổi hành vi đối tượng cho phù hợp nhu cầu mới. Còn Decorator thì mở rộng đối tượng, có thể thay đổi đối tượng lúc runtime.