Hướng dẫn Java Design Pattern – Decorator

Một trong những khía cạnh quan trọng nhất trong quá trình phát triển một ứng dụng mà các lập trình viên phải đối đầu là sự thay đổi. Khi muốn thêm hoặc loại bỏ một tính năng của một đối tượng, điều đầu tiên chúng ta nghĩ đến là thừa kế (extends). Tuy nhiên, thừa kế không khả thi vì nó là static, chúng ta không thể thêm các lớp con mới vào một chương trình khi nó đã được biên dịch và thực thi. Để giải quyết vấn đề này, chúng ta có thể sử dụng Decorator Pattern được giới thiệu trong phần tiếp theo của bài viết này.

1. Decorator Pattern là gì?

Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.

Decorator pattern là một trong những Pattern thuộc nhóm cấu trúc (Structural Pattern). Nó cho phép người dùng thêm chức năng mới vào đối tượng hiện tại mà không muốn ảnh hưởng đến các đối tượng khác. Kiểu thiết kế này có cấu trúc hoạt động như một lớp bao bọc (wrap) cho lớp hiện có. Mỗi khi cần thêm tính năng mới, đối tượng hiện có được wrap trong một đối tượng mới (decorator class).

Decorator pattern sử dụng composition thay vì inheritance (thừa kế) để mở rộng đối tượng. Decorator pattern còn được gọi là Wrapper hay Smart Proxy.

2. Cài đặt Decorator Pattern như thế nào?

Decorator pattern hoạt động dựa trên một đối tượng đặc biệt, được gọi là decorator (hay wrapper). Nó có cùng một interface như một đối tượng mà nó cần bao bọc (wrap), vì vậy phía client sẽ không nhận thấy khi bạn đưa cho nó một wrapper thay vì đối tượng gốc.

Tất cả các wrapper có một trường để lưu trữ một giá trị của một đối tượng gốc. Hầu hết các wrapper khởi tạo trường đó với một đối tượng được truyền vào constructor của chúng.

Vậy làm thế nào để có thể thay đổi hành vi của đối tượng? Như đã đề cập, wrapper có cùng interface với các đối tượng đích. Khi bạn gọi một phương thức decorator, nó thực hiện cùng một phương thức trong một đối tượng được wrap và sau đó thêm một cái gì đó (tính năng mới) vào kết quả, công việc này tùy thuộc vào logic nghiệp vụ.

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 hiện thực (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ụ:

Để đơn giản hơn, chúng ta xem ví dụ về một hệ thống quản lý dự án, nơi nhân viên đang làm việc với các vai trò khác nhau, chẳng hạn như thành viên nhóm (team member), trưởng nhóm (team lead) và người quản lý (manager). Một thành viên trong nhóm chịu trách nhiệm thực hiện các nhiệm vụ được giao và phối hợp với các thành viên khác để hoàn thành nhiệm vụ nhóm. Mặt khác, một trưởng nhóm phải quản lý và cộng tác với các thành viên trong nhóm của mình và lập kế hoạch nhiệm vụ của họ. Tương tự như vậy, một người quản lý có thêm một số trách nhiệm đối với một trưởng nhóm như quản lý yêu cầu dự án, tiến độ, phân công công việc.

Sau đây là các thành phần tham gia vào hệ thống và hành vi của chúng:

  • Employee: thực hiện công việc (doTask), tham gia vào dự án (join), rời khỏi dự án (terminate).
  • Team member: báo cáo task được giao (report task), cộng tác với các thành viên khác (coordinate with others).
  • Team lead: lên kế hoạch (planning), hỗ trợ các thành viên phát triển (motivate), theo dõi chất lượng công việc và thời gian (monitor).
  • Manager: tạo các 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).

Với cách làm thông thường, chúng ta có sơ đồ như sau:

Bất cứ khi nào một thành viên trong nhóm trở thành một Team Lead, chúng ta phải tạo một đối tượng mới của Team Lead và đối tượng trước đó tham chiếu vào nhân viên đó (Team Member trong nhóm) có thể bị hủy hoặc lưu trữ. Đó không phải là cách tiếp cận được khuyến nghị khi nhân viên vẫn là một phần của tổ chức của bạn. Tương tự như trường hợp với Manager, khi một nhân viên trở thành người quản lý từ một Team Lead / Team Member.

Một trường hợp khác là khi một nhân viên có thể thực hiện trách nhiệm của một Team Member trong nhóm cũng như trách nhiệm của Team Lead hoặc một Manager. Trong trường hợp đó, bạn cần tạo hai đối tượng cho cùng một nhân viên là hoàn toàn sai.

Trong các kịch bản này, một Team Member/ Team Lead có thể có thêm trách nhiệm trong lúc thực hiện (run-time). Và trách nhiệm của họ có thể được chỉ định / thu hồi trong lúc run-time.

Hãy xem sơ đồ bên dưới để thấy được cách chúng ta áp dụng Decorator Pattern như thế nào trong trường hợp này.

Như bạn thấy, với Decorator hệ thống của chúng ta linh hoạt hơn rất nhiều. Chúng ta có thể dễ dàng gán một nhân viên sang vai trò TeamMember, TeamLeader, Manager.

EmployeeComponent.java

package com.maixuanviet.patterns.structural.decorator;
 
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
 
public interface EmployeeComponent {
 
    String getName();
 
    void doTask();
 
    void join(Date joinDate);
 
    void terminate(Date terminateDate);
     
    default String formatDate(Date theDate) {
        DateFormat sdf = new SimpleDateFormat("dd/MM/yyyy");
        return sdf.format(theDate);
    }
     
    default void showBasicInformation() {
        System.out.println("-------");
        System.out.println("The basic information of " + getName());
         
        join(Calendar.getInstance().getTime());
         
        Calendar terminateDate = Calendar.getInstance();
        terminateDate.add(Calendar.MONTH, 6);
        terminate(terminateDate.getTime());
    }
}

EmployeeConcreteComponent.java

package com.maixuanviet.patterns.structural.decorator;
 
import java.util.Date;
 
public class EmployeeConcreteComponent implements EmployeeComponent {
     
    private String name;
     
    public EmployeeConcreteComponent (String name) {
        this.name = name;
    }
 
    @Override
    public String getName() {
        return name;
    }
 
    @Override
    public void join(Date joinDate) {
        System.out.println(this.getName() + " joined on " + formatDate(joinDate));
    }
 
    @Override
    public void terminate(Date terminateDate) {
        System.out.println(this.getName() + " terminated on " + formatDate(terminateDate));     
    }
 
    @Override
    public void doTask() {
        // Unassigned task
    }
}

EmployeeDecorator.java

package com.maixuanviet.patterns.structural.decorator;
 
import java.util.Date;
 
public abstract class EmployeeDecorator implements EmployeeComponent {
 
    protected EmployeeComponent employee;
 
    protected EmployeeDecorator(EmployeeComponent employee) {
        this.employee = employee;
    }
 
    @Override
    public String getName() {
        return employee.getName();
    }
 
    @Override
    public void join(Date joinDate) {
        employee.join(joinDate);
    }
 
    @Override
    public void terminate(Date terminateDate) {
        employee.terminate(terminateDate);
    }
}

Manager.java

package com.maixuanviet.patterns.structural.decorator;
 
public class Manager extends EmployeeDecorator {
 
    protected Manager(EmployeeComponent employee) {
        super(employee);
    }
 
    public void createRequirement() {
        System.out.println(this.employee.getName() + " is create requirements.");
    }
 
    public void assignTask() {
        System.out.println(this.employee.getName() + " is assigning tasks.");
    }
 
    public void manageProgress() {
        System.out.println(this.employee.getName() + " is managing the progress.");
    }
 
    @Override
    public void doTask() {
        employee.doTask();
        createRequirement();
        assignTask();
        manageProgress();
    }
}

TeamLeader.java

package com.maixuanviet.patterns.structural.decorator;
 
public class TeamLeader extends EmployeeDecorator {
 
    protected TeamLeader(EmployeeComponent employee) {
        super(employee);
    }
 
    public void planing() {
        System.out.println(this.employee.getName() + " is planing.");
    }
 
    public void motivate() {
        System.out.println(this.employee.getName() + " is motivating his members.");
    }
 
    public void monitor() {
        System.out.println(this.employee.getName() + " is monitoring his members.");
    }
 
    @Override
    public void doTask() {
        employee.doTask();
        planing();
        motivate();
        monitor();
    }
}

TeamMember.java

package com.maixuanviet.patterns.structural.decorator;
 
public class TeamMember extends EmployeeDecorator {
 
    protected TeamMember(EmployeeComponent employee) {
        super(employee);
    }
 
    public void reportTask() {
        System.out.println(this.employee.getName() + " is reporting his assigned tasks.");
    }
 
    public void coordinateWithOthers() {
        System.out.println(this.employee.getName() + " is coordinating with other members of his team.");
    }
 
    @Override
    public void doTask() {
        employee.doTask();
        reportTask();
        coordinateWithOthers();
    }
}

Client.java

package com.maixuanviet.patterns.structural.decorator;
 
public class Client {
 
    public static void main(String[] args) {
        System.out.println("NORMAL EMPLOYEE: ");
        EmployeeComponent employee = new EmployeeConcreteComponent("maixuanviet");
        employee.showBasicInformation();
        employee.doTask();
 
        System.out.println("\nTEAM LEADER: ");
        EmployeeComponent teamLeader = new TeamLeader(employee);
        teamLeader.showBasicInformation();
        teamLeader.doTask();
 
        System.out.println("\nMANAGER: ");
        EmployeeComponent manager = new Manager(employee);
        manager.showBasicInformation();
        manager.doTask();
 
        System.out.println("\nTEAM LEADER AND MANAGER: ");
        EmployeeComponent teamLeaderAndManager = new Manager(teamLeader);
        teamLeaderAndManager.showBasicInformation();
        teamLeaderAndManager.doTask();
    }
}

Output của chương trình:

NORMAL EMPLOYEE: 
-------
The basic information of VietMX
VietMX joined on 04/11/2018
VietMX terminated on 04/05/2019
 
TEAM LEADER: 
-------
The basic information of VietMX
VietMX joined on 04/11/2018
VietMX terminated on 04/05/2019
VietMX is planing.
VietMX is motivating his members.
VietMX is monitoring his members.
 
MANAGER: 
-------
The basic information of VietMX
VietMX joined on 04/11/2018
VietMX terminated on 04/05/2019
VietMX is create requirements.
VietMX is assigning tasks.
VietMX is managing the progress.
 
TEAM LEADER AND MANAGER: 
-------
The basic information of VietMX
VietMX joined on 04/11/2018
VietMX terminated on 04/05/2019
VietMX is planing.
VietMX is motivating his members.
VietMX is monitoring his members.
VietMX is create requirements.
VietMX is assigning tasks.
VietMX is managing the progress.

3. Lợi ích của Decorator Pattern là gì?

  • Tăng cường khả năng mở rộng của đối tượng, bởi vì những thay đổi được thực hiện bằng cách implement trên các lớp mới.
  • Client sẽ không nhận thấy sự khác biệt khi bạn đưa cho nó một wrapper thay vì đối tượng gốc.
  • Một đối tượng có thể được bao bọc bởi nhiều wrapper cùng một lúc.
  • Cho phép thêm hoặc xóa tính năng của một đối tượng lúc thực thi (run-time).

4. Sử dụng Decorator Pattern khi nào?

  • Khi muốn thêm tính năng mới cho các đối tượng mà không ảnh hưởng đến các đối tượng này.
  • Khi không thể mở rộng một đối tượng bằng cách thừa kế (inheritance). Chẳng hạn, một class sử dụng từ khóa final, muốn mở rộng class này chỉ còn cách duy nhất là sử dụng decorator.
  • Trong một số nhiều trường hợp mà việc sử dụng kế thừa sẽ mất nhiều công sức trong việc viết code. Ví dụ trên là một trong những trường hợp như vậy.

5. So sánh Decorator và Adapter

Giống nhau:

  • Cả hai đều là structural pattern như định nghĩa của GOF.
  • Cả hai sử dụng cách composition để cài đặt.

Khác nhau:

  • Decorator cho phép thêm một tính năng mới vào một object nhưng không được phép sử dụng thừa kế. Nó cho phép thay đổi lúc thực thi (run-time). Adapter được sử dụng khi bạn có một interface, và bạn muốn ánh xạ interface đó đến một đối tượng khác có vai trò chức năng tương tự, nhưng là một interface khác.
  • Decorator có xu hướng hoạt động trên một đối tượng. Adapter có xu hướng hoạt động trên nhiều đối tượng (có thể wrap nhiều interface).