Khi bạn muốn đẹp hơn

Bạn đã bao giờ thấy một ngôi nhà được xây dựng thế nào chưa? Ban đầu chỉ là một ô đất trống, rồi người ta làm cái móng, dựng cái khung, chát xi măng, quét sơn và cuối cùng là lắp đặt nội thất. Trong lập trình cũng vậy, để hiển thị được thông tin đến người dùng chúng ta cũng phải thực hiện các bước như vậy.

Giới thiệu

Lâu lắm rồi mình mới quay lại code view (trả về trang HTML cho client) nên chợt thấy Decorator Design Pattern cần thiết biết bao. Làm sao từ một cái template loằng ngoằng lại có thể tạo ra được các mã HTML để trả về cho client nhỉ? Câu trả lời sẽ đến từ Decorator Pattern.

Mục tiêu

Decorator Pattern ra đời với 2 mục tiêu chính:

  1. Cho phép chúng ta set giá trị của một đối tượng tuỳ vào mục đích mà chúng ta sử dụng
  2. Cho phép chúng ta tách logic của việc khởi tạo một đối tượng ra thành nhiều phần, từ đó giảm được số lượng dòng code trong một hàm và làm tăng khả năng mở rộng source code

Ví dụ

Quay trở lại với ví dụ tạo trang HTML từ template nhé, giả sử chúng ta có một trang html với 3 phần

<Header>
<Body>
<Footer>

vói template như sau:

<!-- header -->
Chào mừng ${user} đến với ${myURL}
<!-- Body -->
Hôm nay chúng tôi có ${numberOfEvents} sự kiện dành cho bạn
<!-- Footer -->
Nội dung được cung cấp bởi ${myURL}

Những dữ liệu có dạng ${tên biến} chúng ta gọi nó là placeholder.

Dữ liệu chúng ta có để thay thế cho các placeholder bao gồm:

  1. user: là người dùng hiện tại đã đăng nhập vào website
  2. myURL: là đường link của tổ chức
  3. numberOfEvents: là số lượng sự kiện dành cho khách hàng đang đăng nhập

Nào bây giờ hãy bắt tay vào việc thôi. Đầu tiên chúng ta cần thiết kế lớp thế này:

  1. View: là đối tượng sẽ được "trang trí" và trả về cho client
  2. ViewDecorator: Là interface dùng chung, chứa hàm decorate
  3. ViewTemplateDecorator: Là lớp sẽ lấy template và set vào cho view
  4. ViewDataDecorator: Là lớp sẽ set dữ liệu để thay thế các placeholder

Tiếp theo là source code thôi nhỉ:

class View {
    private String html;
    // hàm setter, getter bỏ qua cho đỡ dài
}

interface ViewDecorator {
    void decorate(View view); 
}

class ViewTemplateDecorator implements ViewDecorator {
    private String template = ...;
    @Override
    public void decorate(View view) {
        view.setHtml(template);
    }
}

class ViewDataDecorator implements ViewDecorator {
    @Override
    public void decorate(View view) {
        String html = view.getHtml();
        Map<String, Object> data = getViewData();
        for(String key : data.keySet()) {
            html = html.replace("${" + key + "}", data.get(key).toString());
        }
        view.setHtml(html);
    }
}

Khi sử dụng sẽ thế này:

View view = new View();
ViewDecorator templateDecorator = new ViewTemplateDecorator();
ViewDecorator dataDecorator = new ViewDataDecorator();
templateDecorator.decorate(view);
dataDecorator.decorate(view);
System.out.println(view.getHtml());

Và chúng ta sẽ có kết quả:

Chào mừng Mr.Young đến với youngmonkeys.org
Hôm nay chúng tôi có 3000 sự kiện dành cho bạn
Nội dung được cung cấp bởi  youngmonkeys.org

Kết hợp để hoàn hảo

Bạn có thấy nhược điểm gỉ ở thiết kế ở trên không? Rõ ràng là có 2 nhược điểm:

  1. Khi muốn sử dụng các decorator chúng ta cứ phải khởi tạo và gọi, như bạn thấy, chúng ta phải mất 4 dòng code thì mới có thể decorate được 1 view
  2. Lớp view có chứa hàm setHtml(html), câu hỏi đăt ra là sau khi chúng ta decorate xong mà lại ko để ý, gọi lại hàm setHtml(html) một lần nữa thì bao nhiêu công sức của chúng ta sẽ là công cốc.

Để khắc phục được 2 nhược điểm này chúng ta cần kết hợp với Builder và Chain of Responsibility design kiểu thế này:

Và khi sử dụng sẽ chỉ đơn giản thế này mà thôi:

View view = new ViewBuilder()
    .addDecorator(new ViewTemplateDecorator())
    .addDecorator(new ViewDataDecorator())
    .build();

Với Unity

Không chỉ với Unity mà tất cả các game engine nói chung đều có thể sử dụng decorator rất mềm dẻo. Ví dụ một nhân vật trong game mỗi khi lên một level sẽ có thêm một món đồ mới (áo giáp, vũ khí) trên người và màu sắc của nhân vật cũng có thể thay đổi, điều này cũng có thể tương đương với việc có thêm một đối tượng decorator mà thôi, kiểu thế này:

// level 10
character.addDecorator(new ArmorDecorator());

// level 20
character.addDecorator(new WeaponDecorator());

Với ReactJS

Cũng tương tự như ví dụ tạo trang html ở trên, các framework front-end như ReactJS hay angular hay VueJS đều có những hàm render để tạo ra các mã HTML trước khi hiển thị cho người dùng.

Tham khảo

  1. Wiki
  2. Ví dụ với Java
  3. Ví dụ với unity