[Design Pattern] Phần 3: Decorator Pattern


[Design Pattern] Phần 3: Decorator Pattern
Trong phần này, chúng ta sẽ tiếp cận với một mẫu thiết kế được gọi là Decorator Pattern. Mẫu này giúp chúng ta mở rộng tính năng của một lớp mà không cần phải chỉnh sửa tại chính lớp đó
Ta sẽ không dài dòng như phần trước nữa mà bắt đầu đi vào ví dụ luôn
Bạn bước vào một cửa hàng bán đồ ăn nhanh. Tại cửa hàng có 3 thực đơn: Hamburger, Phở và Bánh mì với giá lần lượt là 1$ 3$ 2$. Bạn chọn Phở vì đã lâu rồi bạn ở nước ngoài nhưng chưa được ăn món ăn Việt Nam, nên bạn rất thèm. Ông chủ sau khi hoàn thành tô phở, nhập vào máy “Phở 3$” rồi nhìn bạn chờ thanh toán. Bạn cảm thấy tô Phở này chưa có nhiều thịt, bạn yêu cầu nhiều thịt hơn. Ông chủ xóa hóa đơn và nhập lại “Phở + thịt thêm 3.5$”. Nhưng sau đó bạn cảm thấy muốn thêm một chút ớt, lại yêu cầu ông chủ thêm tí ớt nữa. Ông chủ cau mày, nhưng cũng phải phục vụ cho bạn. Thế là ổng xóa đi và nhập lại “Phở + thịt thêm, ớt 3.7$”. Ông chủ lúc này khá bực vì bạn làm cho ổng phải xóa đi và nhập lại hóa đơn liên tục. Bạn lặng lẽ thanh toán, cầm tô phở rồi quay đi, lòng thầm nghĩ “Giá như có thêm rau nữa nhỉ!”.
Trước khi nói ra vấn đề ông chủ gặp phải, ta hãy thiết kế trước để hiểu hơn nhé!
Đầu tiên chúng ta cần tạo một abstract class có tên là Food nhé


Đây là class đại diện cho từng món ăn cụ thể. Nó bao gồm getPrice() để biết giá món ăn, và getName() để biết tên món ăn.
Giờ ta sẽ làm 3 class Hamburger, Pho va Banh_mi nhé



Bạn thấy đấy, mỗi món ăn đều có giá cố định của nó. Nhưng thông thường người mua ngoài việc chọn món ăn thì sẽ chọn thêm một số option của họ, tùy vào số lượng và loại option thì món ăn đó giá sẽ thay đổi. Ta thử làm Test tình huống lúc nãy khi mua Phở nhé

Ok. Giờ bạn cần gọi thêm thịt nhỉ! Phải làm gì đây.
Có khá nhiều cách cho việc này.
-        Như là bạn có thể tạo các biến boolean cho từng món ăn như là từng option đặc biệt, biến nào true thì nghĩa là bạn chọn option đó. Ví dụ class Pho sẽ thêm các biến boolean như beef (thịt), chili (ớt), leek (hành). Khi bạn chọn thịt thêm thì beef sẽ là true và giá tiền sẽ chỉnh lại dựa trên các biến đó. Cách này được nhưng khó khăn cho việc mở rộng vì nếu có thêm một số yêu cầu phụ như thịt heo hay thịt gà thì sẽ phải chỉnh sửa code
-        Hoặc bạn có thể tạo danh sách các yêu cầu phụ tại mỗi class món ăn, khi muốn thêm đồ ăn thì bạn chỉ cần add đối tượng yêu cầu phụ vào trong danh sách là được. (Mình không thiết kế đâu vì lười L )
-        ...
Các cách này đều có điểm chung là sẽ phải thay đổi code của những phương thức. Bạn có biết rằng trong lập trình hướng đối tượng có một nguyên tắc mà khi bạn code bạn nên cố gắng tuân theo, đó là Open-Close: open for extension, but closed for modification. Đại khái là khi bạn mở rộng một tính năng gì đó, thay vì việc chỉnh sửa code cũ, bạn nên cố gắng tìm cách tạo code mới mở rộng từ code cũ. Điều này là dễ hiểu vì code cũ của bạn chạy đúng, nếu bạn chỉnh sửa code cũ để cập nhật tính năng thì có nguy cơ code cũ sẽ bị sai. Mẫu Decorator Pattern mình sắp giới thiệu sau đây sẽ giúp bạn không cần chỉnh sửa code cũ mà vẫn có thể mở rộng dễ dàng.
Mình sẽ giải thích sơ bộ về mẫu này rồi đi vào code sau.

Lấy ví dụ về Phở nhé. Một đối tượng Pho sẽ có phương thức getPrice() và getName()
Khi ta muốn gọi thêm Thịt, nó sẽ trông như thế này

Một đối tượng Beef chứa một đối tượng Pho. Và điều này cũng tương tự khi gọi thêm yêu cầu phụ khác
Khi thực hiện phương thức getName() và getPrice(), chúng sẽ thực hiện như sau

Ở phương thức getPrice() của đối tượng này, tại beef nó sẽ yêu cầu đối tượng bên trong là Pho trả về giá là 3$ ra, sau đó nó sẽ lấy giá của nó 0.5$ cộng vô 3$ thành 3.5$ và trả ra ngoài. Nguyên tắc ở đây là bên lớp ngoài yêu cầu lớp bên trong, lớp bên trong yêu cầu lớp bên trong nữa thực hiện, rồi sau đó trả về từ trong ra ngoài.
Chúng ta để ý rằng chúng ta không hề thay đổi gì ở lớp Pho, mà mỗi lần yêu cầu, ta sẽ tạo một đối tượng ngoài bao lấy bên trong, và gọi theo nguyên tắc đệ quy để thực hiện xử lý. Để cụ thể hơn, chúng ta cùng thực hiện code.

Ta sẽ tạo một lớp FoodDecorator extends từ lớp Food. Ý nghĩa của nó là khi một đối tượng như Pho được thể hiện dưới dạng FoodDecorator, điều đó có nghĩa là nó có thêm yêu cầu phụ. Cách thể hiện của nó như hình trên là một đối tượng bao bọc đối tượng khác.
Để bao bọc được thì ta cần phải nhận diện nó thuộc đối tượng nào, nên ta sẽ dùng một biến food để biết được điều đó.
Hai phương thức getPrice() và getName() là bắt buộc đối với từng yêu cầu phụ.
Bây giờ chúng ta sẽ bắt đầu tạo các lớp yêu cầu phụ

Tiếp tục với Ớt nào

Giờ ta sẽ test để hiểu hơn

Ta sẽ thêm yêu cầu phụ là thịt nhé

Ta sẽ tiếp tục thêm ớt nào

Ta có cảm thấy rằng food1 vẫn giữ là Phở nhưng hàm getName() và getPrice() đã có sự thay đổi không. Đúng vậy, nó như việc chúng ta trang trí (bọc) cho đối tượng food1 bằng các yêu cầu phụ như thịt với ớt vậy, đối tượng vẫn giữ nguyên và hàm bị thay đổi.
Nó giống như việc bạn mặc áo khoác ấy, bạn có thể mặc nhiều lớp, và tất nhiên cũng có thể cởi ra. Trong code thì bạn có thể làm như sau

Bạn gọi cái mà nó đang bọc là được.
Về cơ bản, Decorator Pattern là bọc đối tượng chính bằng những thành phần gọi là Trang Trí. Các phương thức của các đối tượng Trang Trí này áp dụng nguyên tắc đệ quy để gọi lần lượt từ trong ra ngoài
Sơ đồ của Decorator Pattern trông sẽ như này này

Cảm ơn các bạn đã đón xem! Thật sự thì mình biết cách nó thực hiện nhưng không nắm rõ được ý nghĩa sâu xa của mẫu này nên chỉ giới thiệu cho các bạn cách sử dụng thôi. Các bạn thông cảm nhé.
Do thứ 7 có việc nên chủ nhật mình mới có thời gian làm. Nói thật thì cũng phải mất kha khá thời gian để hiểu nó :( dù hông hiểu hết. Dù sao cũng cám ơn các bạn đã đọc nhé!
Nội dung bài viết thuộc về Lê Công Diễn. Có sự tham khảo từ sách Design Pattern for Dummies cũng như một số tài liệu khác.

Người viết: Lê Công Diễn
Mang đi nhớ ghi nguồn



Nhận xét

  1. Wonderfull,..
    Ủa , sao mấy bài trước có giới thiệu bài kì sau, ví dụ " kì sau : Obsever,..." .Bài này lại không có , tính bỏ giữa chừng à .... :(( buồn

    Trả lờiXóa

Đăng nhận xét

Bài đăng phổ biến từ blog này

Deploy project Springboot MIỄN PHÍ sử dụng Render

Ứng dụng Mã hóa bất đối xứng (Asymmetric cryptography) vào Chữ ký số (Digital Signature)

API và HTTP - Một số khái niệm cơ bản cần biết về Web (Phần 2)