Decorator pattern – mở rộng object không dùng kế thừa

    6

    Decorator pattern là mẫu thiết kế thuộc nhóm structural. Nó cung cấp khả năng mở rộng một object bằng cách “đính kèm” biến hoặc phương thức mới cho một object. Object không biết gì về việc mình đang được “trang trí” (decorated) với những phương thức và biến mới. Mẫu thiết kế này thích hợp cho các hệ thống cần mở rộng liên tục. Về ý tưởng Decorator tương đối gần gũi với kế thừa và bạn có thể dễ dàng hiểu được mẫu thiết kế này nếu bạn nắm rõ tư tưởng của kế thừa.

    Trong bài học này chúng ta sẽ xem xét chi tiết ý tưởng, thiết kế và cách thực hiện mẫu thiết kế này.

    Decorator pattern là gì?

    Mẫu thiết kế Decorator giúp bổ sung thêm các trạng thái (nghĩa là các biến thành viên) và hoạt động (các phương thức) mới vào một object sẵn có.

    Bạn nghe có quen không ạ? Bổ sung thêm biến và phương thức mới vào một object sẵn có. Vậy đó không phải là kế thừa sao? Trên thực tế bạn có thể hình dung Decorator là một kiểu “kế thừa” cũng được, vì ý tưởng của chúng khá gần gũi. Tuy nhiên, “kế thừa” kiểu Decorator thực hiện ở mức độ object, thay vì mức độ class. Do thực hiện ở mức độ object, “kế thừa” của Decorator diễn ra khi chương trình chạy (runtime), thay vì lúc định nghĩa kiểu ở compile time.

    Hãy lấy ví dụ khác về một bức ảnh hiển thị trên màn hình. Các chương trình hỗ trợ xem/xử lý ảnh có thể ghép vào đó nhiều yếu tố trang trí như đường viền, tag. Các thành phần trang trí này xuất hiện bên trên bức ảnh nhưng lại không ảnh hưởng đến file ảnh gốc. Tổ hợp của ảnh gốc và các phần trang trí tạo ra một object mới. Bạn nhìn thấy ảnh tổ hợp này.

    Decorator pattern chính là có cùng ý tưởng như vậy. Mỗi thành phần trang trí cho bức ảnh gốc là một object độc lập. Và cũng có vô số cách trang trí cho bức ảnh. Từ một ảnh gốc bạn có thể tạo ra rất nhiều bức ảnh có các kiểu trang trí khác nhau. Bạn thậm chí có thể kết hợp các kiểu trang trí để tạo ra những bức ảnh độc.

    Tóm lại, mẫu Decorator có những đặc điểm:

    • Có thể bổ sung (động) những thành viên mới (trang trí) cho object;
    • Object gốc không thay đổi và không biết gì về những thứ được bổ sung cho nó;
    • Do vậy bạn không cần phải xây dựng một class khổng lồ với mọi thứ bên trong;
    • Các object trang trí độc lập nhau và có thể tổ hợp với nhau.

    Như vậy, về tư tưởng chung, Decorator gần gũi với kế thừa. Vậy tại sao không dùng luôn kế thừa. Nếu một class do người khác phát triển và đánh dấu sealed, bạn không thể kế thừa được từ class đó nữa. Khi này, nếu muốn mở rộng nó, bạn hãy nghĩ đến Decorator. Nếu bạn cần mở rộng object runtime, bạn cần đến Decorator.

    Ngoài ra, Decorator có khả năng kết hợp đa dạng để mở rộng object mà nếu bạn dụng kế thừa, kết cục sẽ là rất nhiều class con gây khó khăn cho việc bảo trì code về sau.

    Thiết kế Decorator pattern

    Dưới đây là sơ đồ thiết kế UML của mẫu thiết kế Decorator. Nếu không nhớ các ký hiệu, hãy đọc lại phần UML của bài học tổng quan về Design Pattern.

    Các thành phần của thiết kế này như sau:

    Component: class gốc. Bạn có thể hình dung nó như bức ảnh gốc, hoặc class cha trong kế thừa.

    Decorator: class trang trí. Bạn có thể hình dung nó là class con (trong kế thừa), hoặc bức ảnh đã được trang trí thêm.

    Cả Component và Decorator đều phải thực thi một giao diện (interface) chung IComponent.

    addedState và addedBehavior là biến và phương thức riêng của Decorator bổ sung cho object gốc (của Component). Dễ hình dung nó giống như các biến phương thức thành viên riêng của class con.

    Operation: decorator không chỉ bổ sung mà còn có thể thay thế phương thức của class gốc. Nếu có những phương thức cần thay thế, nó phải được chỉ rõ trong giao diện chung giữa Component và Decorator. Dễ hình dung nó giống như ghi đè phương thức trong kế thừa.

    Giữa Decorator và IComponent còn có thêm quan hệ aggregate (has-a), thể hiện qua biến private component kiểu IComponent bên trong Decorator. Nó rất gần với ý tưởng tạo ra một cây kế thừa của lập trình hướng đối tượng. Do mối quan hệ này, một object đã được mở rộng có thể lại tiếp tục được mở rộng bởi một decorator khác tạo ra cả một “cây kế thừa” theo kiểu Decorator. Điều này cũng giống như một bức ảnh đã được trang trí lại có thể tiếp tục được trang trí thêm nữa.

    Thực thi Decorator pattern

    Khi bạn đã hiểu ý tưởng của Decorator pattern, giờ là lúc thực hiện một ví dụ minh họa cơ bản.

    using System;
    using static System.Console;
    namespace P01_Concept
    {
        interface IComponent
        {
            string Operation();
        }
        class Component : IComponent
        {
            public string Operation()
            {
                return "Hello world! This is the original object";
            }
        }
        class DecoratorA : IComponent
        {
            private readonly IComponent _component;
            public DecoratorA(IComponent component)
            {
                _component = component;
            }
            // coi như "kế thừa" phương thức này từ object gốc
            // nếu muốn bạn có thể "giả lập ghi đè" bằng cách thay đổi nội dung phương thức này
            public string Operation()
            {
                return _component.Operation();
            }
            // bổ sung phương thức này cho object gốc
            public string AddedBehavior()
            {
                return "This is the A Decorator object";
            }
        }
        class DecoratorB : IComponent
        {
            private readonly IComponent _component;
            public DecoratorB(IComponent component)
            {
                _component = component;
            }
            // giả lập ghi đè Operation
            public string Operation()
            {
                var s = _component.Operation();
                return $"{s}. But I was 'overrode'";
            }
        }
        class Program
        {
            static void Main(string[] args)
            {
                Title = "DECORATOR DESIGN PATTERN";
                IComponent orgComponent = new Component();
                DecoratorA aComponent = new DecoratorA(orgComponent);
                DecoratorB bComponent = new DecoratorB(orgComponent);
                DecoratorA abComponent = new DecoratorA(bComponent);
                ForegroundColor = ConsoleColor.Green;
                WriteLine($"Original object: {orgComponent.Operation()}");
                ForegroundColor = ConsoleColor.Yellow;
                WriteLine($"A Decorator object: {aComponent.Operation()}. {aComponent.AddedBehavior()}");
                ForegroundColor = ConsoleColor.Cyan;
                WriteLine($"B Decorator object: {bComponent.Operation()}");
                ForegroundColor = ConsoleColor.Magenta;
                WriteLine($"AB Decorator object: {abComponent.Operation()}");
                ReadKey();
            }
        }
    }

    Trong ví dụ này sử dụng lại đúng những tên gọi trong sơ đồ thiết kế UML. Nó cũng minh họa sự tương quan giữa Decorator và kế thừa.

    Ví dụ minh họa nguyên lý của mẫu thiết kế Decorator

    Ứng dụng của mẫu Decorator

    Mẫu thiết kế Decorator có một số ứng dụng thực tế.

    Thứ nhất là trong đồ họa, video, âm thanh. Ví dụ, để video streaming có thể được nén theo nhiều tỉ lệ khác nhau; Âm thanh có thể đưa ra nhiều dịch vụ chuyển đổi cùng lúc.

    Thứ hai, sử dụng trong một số API xuất nhập. Ví dụ chuỗi class Stream của .NET (Stream => FileStream, MemoryStream, NetworkStream => BinaryWriter/Reader, StreamWriter/Reader) sử dụng Decorator. Nếu bạn từng làm việc với một trong số các loại luồng trên hẳn sẽ để ý: (1) các adapter mở rộng các backing store stream nhưng không kế thừa từ nó; (2) trong object, ví dụ của NetworkStream hay StreamWriter, đều có object của Stream; (3) các loại stream có thể mở rộng lẫn nhau. Hãy nhớ lại mô hình thiết kế của Decorator xem có sự tương đồng nào không.

    Thứ ba, trình duyệt và ứng dụng mobile sử dụng mẫu này để tạo ra giao diện phù hợp cho từng loại kích thước màn hình.

    Nói tóm lại, trong những tình huống sau bạn nên nghĩ tới Decorator:

    • Muốn mở rộng sealed class;
    • Mở rộng hoặc thay đổi object “động” ở runtime mà không đụng chạm đến object gốc;
    • Không muốn tạo class con (tránh sử dụng kế thừa).

    Kết luận

    Trong bài học này bạn đã làm quen với mẫu thiết kế Decorator. Đây là một mẫu thiết kế khá đơn giản khi thực hiện. Decorator có nhiều ứng dụng trong thực tế và bạn có thể sẽ cần đến Decorator để thay thế cho kế thừa.

    + Nếu bạn thấy site hữu ích, trước khi rời đi hãy giúp đỡ site bằng một hành động nhỏ để site có thể phát triển và phục vụ bạn tốt hơn.
    + Nếu bạn thấy bài viết hữu ích, hãy giúp chia sẻ tới mọi người.
    + Nếu có thắc mắc hoặc cần trao đổi thêm, mời bạn viết trong phần thảo luận cuối trang.
    Cảm ơn bạn!

    Theo dõi
    Thông báo của
    guest

    6 Thảo luận
    Cũ nhất
    Mới nhất
    Phản hồi nội tuyến
    Xem tất cả bình luận
    Trần Xuân Tín

    Các lớp con kế thừ trực tiếp từ Interface thế kia là dở rồi

    Đức Thái

    Bạn có thể nói rõ hơn vấn đề của thiết kế này được không ạ? Làm thế nào để thiết kế tốt hơn ạ? Cảm ơn bạn!

    hoàng tử

    theo mình thấy chả có vấn đề gì hết,

    Lần cuối chỉnh sửa 3 năm trước bởi hoàng tử
    hoàng tử

    mình đọc trên kia thì nó bảo extend từ component :V

    ThànhTM6

    Bài viết hay, chi tiết, dễ hiểu. cảm ơn tác giả nhiều

    DTA

    Chị Mai Chi còn những mẫu nào khác nữa không ạ, VD của chị rất gần gũi dễ hiểu.