Design Pattern | Decorator Pattern

Cà phê là một trong những thứ yêu thích của tôi trên thế giới này. Tôi thích một tách cà phê đen đơn giản. Bạn có thích một tách cà phê đen ? Hay bạn thích các phiên bản khác của nó hơn ? Thêm nhiều thành phần như sữa, kem, hoặc socola nóng là những cách tạo ra phiên bản khác của cốc cà phê.

Chúng ta vẫn có thể nghĩ nó là một loại đồ uống có nguồn gốc từ cà phê nhưng mong đợi một sự khác biệt giữa cà phê đen và các phiên bản lạ mắt như pha latte hay espresso. Bất kể loại cà phê chúng ta đang uống là gì, tác dụng của từng thành phần không thay đổi.

Ví dụ, cà phê sẽ luôn hoạt động như một chất kích thích vì hàm lượng cafeine của nó, điều này không thay đổi nếu chúng ta thêm sữa, thay đổi phong cách pha chế, hoặc nếu nó được làm ở các nhiệt độ khác nhau, cà phê vẫn sẽ là một chất kích thích. Điều này đúng cho bất cứ điều gì bạn thêm vào cà phê, một số người không dung nạp lactose (đường sữa) vẫn không thể tiêu thụ các sản phẩm sữa ngay cả khi nó được trộn với cà phê. Lactose trong sữa không thay đổi khi nó được trộn với thứ khác.

Mặc dù mọi thành phần tiếp tục giữ các đặc điểm của riêng nó, kết hợp chúng tạo ra một loại đồ uống mới có thể thú vị hơn so với khi được tiêu thụ riêng biệt.

Trong phần mềm, giống như khi nghĩ ra một loại đồ uống mới, sẽ có lợi khi kết hợp linh hoạt của các hành vi tổng thể. Nhưng chúng ta gặp phải một vấn đề khi cố gắng thực hiện điều này một cách linh hoạt trong thời gian chạy, đó là hành vi của một đối tượng được xác định bởi lớp của nó, nhưng khái niệm về một lớp và các mối quan hệ như thừa kế là tĩnh (static). Đó là, xảy ra tại thời điểm biên dịch, điều này có nghĩa là chúng ta không thể thay đổi các lớp trong khi chương trình của chúng ta đang chạy. Chúng ta sẽ cần tạo ra một lớp mới để đạt được sự kết hợp hành vi mới. Kết quả là, có nhiều kết hợp mới sẽ dẫn đến nhiều lớp và chúng ta không muốn điều đó. Cho rằng một đối tượng có một hành vi nhất định, chúng ta có khả năng gắn kết các hành vi hoặc trách nhiệm bổ sung với nó không ?

May mắn thay, có một Decorator Pattern  để đạt được điều này và sử dụng tổng hợp để kết hợp các hành vi trong thời gian chạy. Tập hợp được sử dụng để thể hiện mối quan hệ "has a" hoặc "weak containtment" giữa hai đối tượng. Chúng ta có thể sử dụng mối quan hệ "has a" để xây dựng một chồng các đối tượng trong đó mỗi cấp độ của ngăn xếp chứa một đối tượng biết về hành vi của chính nó và gia tăng một đối tượng bên dưới nó trong ngăn xếp. Đây là những gì một ngăn xếp tổng hợp sẽ trông như thế nào:


Đối tượng A là một đối tượng cơ sở vì nó không chứa các đối tượng khác, nó sẽ có tập hợp các hành vi của riêng mình. Đối tượng B tổng hợp đối tượng A, thực tế đối tượng B được cho phép làm tăng hành vi của đối tượng A. Chúng ta có thể tiếp tục thêm các đối tượng vào ngăn xếp theo cách mà các đối tượng C được tổng hợp từ đối tượng B. thực tế làm tăng hành vi cho đối tượng B.

Mối quan hệ kết hợp luôn luôn one-to-one trong Decorator Pattern để xây dựng ngăn xếp sao cho một đối tượng nằm trên một đối tượng khác. Để đạt được sự kết hợp tổng thể hành vi cho các đối tượng xếp chồng lên nhau, bạn sẽ gọi phần từ trên cùng là đối tượng C. Đối tượng C sẽ gọi đối tượng B, và đối tượng B sẽ gọi đối tượng A. Đối tượng A phản hồi với hành vi của nó, sau đó đối tượng B thêm hành vi tăng dần của nó. Cấu trúc thực tế của mẫu thiết kế này sử dụng cả giao diện và kế thừa để các lớp tuân theo một kiểu chung, các thể hiện của chúng có thể được xếp chồng lên nhau theo cách tương thích để xây dựng hành vi tổng thể mạch lạc. Bây giờ, hãy xem Decorator Pattern qua UML:


Giao diện thành phần (Interface Component) được sử dụng để xác định loại phổ biến cho tất cả các lớp, một lớp client sẽ mong đợi cùng một giao diện trên tất cả các lớp thành phần. Một lớp thành phần cụ thể được thực hiện giao diện thành phần và có thể được khởi tạo. Một thể hiện của lớp này có thể được sử dụng làm đối tượng cơ sở trong ngăn xếp, Decorator là một lớp trừu tượng (abstract), giống như lớp thành phần cụ thể (concreteComponent), nó thực hiện giao diện thành phần (implement component interface). Sự khác biệt chính là Component tổng hợp các loại thành phần khác sẽ cho phép chúng ta xếp chồng các thành phần lên nhau và Component đóng vai trò là siêu lớp trừu tượng của các concrêt decorator sẽ cung cấp gia tăng hành vi.

Chúng ta xây dựng stack các thành phần bắt đầu từ một thể hiện của lớp  concrete component class, và tiếp tục với các thể hiện của các lớp con decorator abstract class. Về mặt tương tự như ví dụ cà phê của chúng ta, thành phần concrete component sẽ là cà phê đen, decorator cho cà phê của chúng ta sẽ là sữa, đường, socola, và bất kỳ thành phần nào khác có thể thêm vào cà phê.

Có nhiều tương tự có thể được sử dụng cho Decorator Pattern từ cà phê đến pizza, nhưng, những sự tương tự này không chuyển dịch tốt sang cách bạn thực sự sử dụng mẫu thiết kế này cho phần mềm.  Vì vậy, hãy xem xét một ví dụ thực tế hơn với những gì bạn sẽ thấy trong một hệ thống phần mềm:

Một trang web là một ví dụ về nơi chúng ta sẽ sử dụng mẫu thiết kế này, một trang web cơ bản chỉ đơn giản được tạo thành từ HTML và có thể là một chút JavaScript, tuy nhiên, hành vi của một trang web có thể phức tạp hơn. Điều gì sẽ xảy ra nếu bạn muốn đảm bảo rằng người truy cập trang của bạn được ủy quyền ? Hoặc nếu bạn muốn chia sẻ số lượng lớn kết quả tìm kiếm thành các trang riêng biệt thì sao ? Bạn không muốn viết các loại trang web hoàn toàn khác nhau cho mọi kết hợp có thể có của trang web, phân trang hoặc các hành vi lưu trữ. Thay vào đó, bạn có thể sử dụng một Decorator Pattern để tạo một lớp cho từng loại hành vi và xây dựng sự kết hợp cụ thể của trang web mà bạn muốn trong thời gian chạy, hãy lấy ví dụ này và xem nó trông thế nào trong sơ đồ UML bằng cách là theo thiết kế chung cho Decorator Pattern.


 Giao diện thành phần của bạn là một trang web (component interface), điều này sẽ định nghĩa tất cả các lớp con trong mẫu là một loại trang web có cách triển khai riêng về cách hiển thị. Lớp concrete component class là BasicWebPage của bạn. Nó bao gồm HTML, stylesheet và JavaScript mà chúng ta sẽ biểu diễn là String cho đơn giản. Trang web cơ bản (BasicWebPage) này biết cách hiển thị tất cả các yếu tố trang web của nó, bây giờ, bạn cần trang trí (decorator) để thêm nhiều chức năng hơn vào trang web cơ bản của bạn bằng cách sử dụng tổng hợp thay vì phân lớp chính trang web cơ bản. Bạn sẽ cần sử dụng các subtypes của lớp WebPageDecorator trừu tượng để tăng thêm trang web cơ bản của chúng ta. Như chúng ta khám phá trước đó, WebPageDecorator sẽ là một kiểu con của webPageInterface. Do đó, một kiểu con của WebpageDecorator cũng là một kiểu con của InterfaceWebPage. Bạn có thể xác định bất kỳ số lượng hành vi nào để bạn muốn tăng trường trang web cơ bản của mình. Trong ví dụ này, chúng ta sẽ nâng cao trang web cơ bản bằng cách thêm ủy quyền để đảm bảo người dùng có thể truy cập trang và xác thực để đảm bảo người dùng là người được nhận những gì.

Làm thế nào điều này làm giảm số lượng các lớp mà chúng ta sẽ cần phải tạo ra ? Nếu bạn đang sử dụng tính kế thừa của trang web cơ bản của bạn, bạn sẽ cần tạo mọi lớp cho mọi kết hợp của hành vi này. Điều đó có nghĩa là chúng ta sẽ cần một lớp riêng cho các hàm xác thực và ủy quyền kết hợp. Decorator Pattern giải quyết vấn đề này bằng cách cho phép các concrete decorator tổng hơp bất kỳ loại thành phần nào, bây giờ chúng ta sẽ thực hiện Pattern này.

Bước 1, thiết kế Component Interface. Đầu tiên, bạn xác định giao diện của mình ở các lớp còn lại trong mẫu thiết kế sẽ là kiểu con của nó. Giao diện sẽ xác định các hành vi phổ biến mà trang web decorator sẽ có.


Bước 2, thực hiện giao diện với lớp thành phần cụ thể (concrete component) cơ sở của bạn. BasicWebPage sẽ triển khai cách nó hiển thị bằng HTML, stylesheet, JavaScript. Đây chính là khối xây dựng cơ sở cho tất cả các đối tượng trang web.


Bước 3, thực hiện giao diện với lớp abtract decorator class. việc thực hiện lớp decorator rất quan trọng mặc dù có ít code. Điều đầu tiên cần lưu ý là một trang web decorator chỉ chứa một phiên bản của trang web (webPage). Điều này cho phép chúng ta xếp chồng decorators lên trên lên trên cùng của basic web page và chồng lên nhau. Mỗi loại trang web chịu trách nhiệm cho hành vi của mình và sẽ gọi đệ quy trang tiếp theo trên ngăn xếp để thực hiện hành vi của nó. Hàm tạo sẽ cho phép bạn liên kết các kiểu con khác nhau của trang web với nhau trong một ngăn xếp. Tất cả những gì bạn cần làm là cho nó biết trường hợp của kiểu con trang web bạn muốn xếp chồng lên. Vì hàm tạo cho phép bạn liên kết bất kỳ kiểu con nào lên ngăn xếp, nên thứ tự bạn xây dựng ngăn xếp là quan trọng.


Phần quan trọng nhất là trang web cơ bản phải là trang đầu tiên trong ngăn xếp. Phần còn lại sẽ phụ thuộc vào thiết kế hệ thống của bạn và hành vi gia tăng nào bạn muốn thực hiện trước tiên, trình abstract decorator của bạn  chỉ cần ủy thác hành vi được hiển thị cho đối tượng trang web mà nó tổng hợp. Điều này sẽ cho phép bạn kết hợp hành vi tháo khỏi stack trang web.

Bước 4, kế thừa từ abstract decorator và thực hiện giao diện thành phần các lớp concrete decorators (lớp trang trí cụ thể). Contructors sẽ sử dụng abstract superclass contructor vì nó sẽ cho phép chúng ta xếp các lớp trang trí lại với nhau. Hãy nhớ rằng lớp abstract web page decorator là tập hợp các lớp trang trí cụ thể. Mỗi class decorator có trách nhiệm riêng của mình, bạn thực hiện các lớp trách nhiệm này trong lớp thích hợp để chúng có thể được gọi.


Để gọi đệ quy hành vi hiển thị, các concrete decorator gọi gọi phương thức hiển thị super, vì siêu lớp trang trí trừu tượng tạo điều kiện cho việc tổng hợp các loại trang web khác nhau, nên lệnh gọi super.display() sẽ khiến trang web tiếp theo trong ngăn xếp thực hiện phiên bản hiển thị của nó cho đến khi bạn truy cập trang web cơ bản. Gọi đệ quy sẽ dừng tại đây vì trang web cơ bản là thành phần cụ thể không tổng hợp bất kỳ loại trang web nào khác, ý tưởng ở đây là liên kết các call được hiển thị từ dưới lên và thực hiện sao lưu. Điều này có ý nghĩa vì bạn cần xây dựng trang web cơ bản của mình trước khi bạn có thể thêm nhiều hành vi vào nó. Bây giờ hãy xem nó hoạt đông:


Trước tiên, bạn xây dựng web page cơ bản đầu tiên, thêm hành vi ủy quyền (authorization) và sau đó thêm hành vi xác thực (authentication) vào cuối cùng, bằng cách này, khi phương thức display() được gọi, nó sẽ liên kết cuộc gọi xuống the basic web page, trang web cơ bản sẽ tự hiển thị, Đầu ra của chương trình sẽ trông như thế này:

Basic web page
Authorization user
Authenticatin user



Như bạn có thể thấy, bạn có thể thêm bất kỳ trang trí nào nào trang web cơ bản để đạt được một hành vi kết hợp khác nhau. Điều này cho phép bạn tự động xây dựng hành vi cơ bản của trang web mình vì chúng ta có thể khởi tạo và thêm các đối tượng mới vào ngăn xếp tổng hợp, mẫu thiết kế này có khả năng cho phép bạn tô điểm lại đồ vật của mình một cách độc đáo.

Tóm lại, các khái niệm chính cần nhớ của mẫu thiết kế này là: chúng ta có thể thêm bất kỳ số lượng hành vi nào một cách linh hoạt cho một đối tượng trong khi chạy bằng cách sử dụng tổng hợp thay thế thay cho thừa kề thuần túy, đây là một điều rất quan trọng.

Mẫu thiết kế này không chỉ cho phép bạn tự động sửa đổi các đối tượng mà còn làm giảm sự đa dạng của các lớp cần viết, sử dụng mẫu thiết kế này sẽ giúp bạn tạo ra phần mềm phức tạp mà không cần chi phí phức tạp.
Cảm ơn.





































Nhận xét

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

Hiểu về Norm Regularization

Faceswap & state-of-the-art (SOTA)