Tổng quan về Bridge Design Pattern


Ý tưởng của nó là tách tính trừu tượng (abstraction) ra khỏi tính hiện thực (implementation) của nó. Từ đó có thể dễ dàng chỉnh sửa hoặc thay thế mà không làm ảnh hưởng đến những nơi có sử dụng lớp ban đầu.  


I. Giới thiệu

Bridge Pattern là một trong những Pattern thuộc nhóm Structural Pattern.

Ý tưởng của nó là tách tính trừu tượng (abstraction) ra khỏi tính hiện thực (implementation) của nó. Từ đó có thể dễ dàng chỉnh sửa hoặc thay thế mà không làm ảnh hưởng đến những nơi có sử dụng lớp ban đầu.

Ban đầu chúng ta thiết kế một class với rất nhiều xử lý, bây giờ chúng ta không muốn để những xử lý đó trong class đó nữa. Vì thế, chúng ta sẽ tạo ra một class khác và move các xử lý đó qua class mới. Khi đó, trong lớp cũ sẽ giữ một đối tượng thuộc về lớp mới, và đối tượng này sẽ chịu trách nhiệm xử lý thay cho lớp ban đầu.

II. Mục đích ra đời

Giả sử bạn làm một con game, có class cha Nhân vật và và 2 class kế thừa lại là Kỵ sĩ và Chiến binh. Sau đó, bạn muốn kết hợp thêm 2 vũ khí là dao và kiếm vào cho nhân vật, và bạn phải tạo 4 class kế thừa là là Kỵ sĩ dùng kiếmKỵ sĩ dùng khiênChiến binh dùng kiếm, Chiến binh dùng khiên,...

=> Vấn đề: Việc thêm các loại Nhân vật và Vũ khí mới vào hệ thống khiến ta phải tạo thêm nhiều lớp kế thừa.

Vấn đề này xảy ra khi chúng ta cố gắng mở rộng Nhân Vật và Vũ khí theo hai chiều độc lập, một vấn đề rất phổ biến đối với Kế thừa trong lập trình hướng đối tượng.

=> Giải pháp: mẫu Bridge ra đời, nó giúp chuyển đổi từ kế thừa sang thành phần.

Trong lớp Nhân vật có một thuộc tính là Vũ khí, Vũ khí thì có thể thêm các vũ khí kế thừa như Súng, Gươm, Kiếm tùy ý. 

Khi đó muốn Chiến binh dùng kiếm ta chỉ cần class Chiến binh có thuộc tính Kiếm thôi, tương tự với các nhân vật khác mà không cần phải kế thừa nhiều.

Với cấu trúc mới như vậy, khi có thêm một số vũ khi mới, chúng ta chỉ việc thêm vào một implement mới cho Vũ khí, các thành phần khác của Nhân vật không bị ảnh hưởng. Hoặc cần thêm một nhân vật mới như Sát thủ, ta chỉ cần thêm implement mới cho Nhân Vật, các thành phần khác cũng không bị ảnh hưởng và số lượng class chỉ tăng lên 1.

III. Kiến trúc

Các thành phần trong mô hình:

  • Abstraction: định nghĩa giao diện của lớp trừu tượng, quản lý việc tham chiếu đến đối tượng hiện thực cụ thể (Implementation).
  • Refined Abstraction: kế thừa Abstraction.
  • Implementation: định nghĩa giao diện cho các lớp hiện thực. Thông thường nó là interface định ra các tác vụ nào đó của Abstraction.
  • ConcreteImplementation: kế thừa Implementation và định nghĩa chi tiết hàm thực thi.

Các lưu ý khi cài đặt:

  • Cần xác định rõ các chiều trực giao trong liên kết giữa các class. Các yếu tố dùng để xác định khi tách class có thể là abstraction/platform, domain/infrastructure, font-end/back-end, interface/implementation.
  • Với mỗi platform, tạo một concrete implementation nhưng đảm bảo các class này đều tuân theo Implementation interface.
  • Trong abstraction class, thêm một trường có kiểu Implementation. Abstraction sẽ ủy nhiệm lại công việc cho implementation object được tham chiếu đến.
  • Nếu hệ thống có nhiều logic cấp cao khác nhau, tạo ra các lớp refined abstraction từ lớp abstraction cha.
  • Client code cần đưa implementation object vào constructor của abstraction để tạo mối liên kết. Sau khi khởi tạo abstraction hoàn tất thì client có thể làm việc với abstraction mà không cần bận tâm đến implementation.

IV. Source code minh họa với C#

Sau đây mình sẽ trình bày sơ đồ và code minh họa cho hệ thống Nhân vậtVũ khí ở trên như sau:

  • Tạo Interace VuKhi
    interface VuKhi
    {
        string TanCong();
    }

 

  • Tạo các vũ khí (subclass) implement từ Interface VuKhi
    class Gay : VuKhi
    {
        public string TanCong()
        {
            return "Tan cong bang gay";
        }
    }

    class Kiem : VuKhi
    {
        public string TanCong()
        {
            return "Tan cong bang kiem";
        }
    }

    class Khien : VuKhi
    {
        public string TanCong()
        {
            return "Tan cong bang khien";
        }
    }

 

  • Tạo Abstract class NhanVat
    abstract class NhanVat
    {
        public VuKhi vukhi { get; set; }

        public abstract string TanCong();
    }

 

  • Tạo các nhân vật (subclass) kế thừa từ class NhanVat
    class SatThu : NhanVat
    {
        public int dame = 1;
        public override string TanCong()
        {
            return vukhi.TanCong() + " dame =" + (dame * 5).ToString();
        }
    }

    class KySi : NhanVat
    {
        public int dame = 2;
        public override string TanCong()
        {
            return vukhi.TanCong() + " dame =" + (dame * 3).ToString();
        }
    }

    class ChienBinh : NhanVat
    {
        public int dame = 3;
        public override string TanCong()
        {
            return vukhi.TanCong() + " dame =" + (dame * 10).ToString();
        }
    }

 

  • Sử dụng
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Tao doi tuong");
            var Gay = new Gay();
            var Kiem = new Kiem();
            var Khien = new Khien();
            SatThu SatThu_Gay = new SatThu { vukhi = Gay };
            SatThu SatThu_Kiem = new SatThu { vukhi = Kiem };
            KySi KySi_Kiem = new KySi { vukhi = Kiem };
            ChienBinh ChienBinh_Khien = new ChienBinh { vukhi = Khien };

            Console.WriteLine($"Sat thu dung gay: {SatThu_Gay.TanCong()}");
            Console.WriteLine($"Sat thu dung kiem: {SatThu_Kiem.TanCong()}");
            Console.WriteLine($"Ky si dung kiem: {KySi_Kiem.TanCong()}");
            Console.WriteLine($"Chien binh dung khien: {ChienBinh_Khien.TanCong()}");
        }
    }

 

V. Ưu & nhược điểm

    1. Ưu điểm

  • Giảm sự phục thuộc giữa abstraction và implementation (loose coupling): tính kế thừa trong OOP thường gắn chặt abstraction và implementation lúc build chương trình. Bridge Pattern có thể được dùng để cắt đứt sự phụ thuộc này và cho phép chúng ta chọn implementation phù hợp lúc runtime.
  • Giảm số lượng những lớp con không cần thiết: một số trường hợp sử dụng tính inheritance sẽ tăng số lượng subclass rất nhiều. Ví dụ: trường hợp chương trình view hình ảnh trên các hệ điều hành khác nhau, ta có 6 loại hình (JPG, PNG, GIF, BMP, JPEG, TIFF) và 3 hệ điều hành (Window, MacOS, Ubuntu). Sử dụng inheritance trong trường hợp này sẽ làm ta thiết kế 18 lớp: JpgWindow, PngWindow, GifWindow, …. Trong khi áp dụng Bridge sẽ giảm số lượng lớp xuống 9 lớp: 6 lớp ứng với từng implement của Image và 3 lớp ứng với từng hệ điều hành, mỗi hệ điều hành sẽ gồm một tham chiếu đến đối tượng Image cụ thể.
  • Code sẽ gọn gàng hơn và kích thước ứng dụng sẽ nhỏ hơn: do giảm được số class không cần thiết.
  • Dễ bảo trì hơn: các Abstraction và Implementation của nó sẽ dễ dàng thay đổi lúc runtime cũng như khi cần thay đổi thêm bớt trong tương lai.
  • Dễ dàng mở rộng về sau: thông thường các ứng dụng lớn thường yêu cầu chúng ta thêm module cho ứng dụng có sẵn nhưng không được sửa đổi framework/ứng dụng có sẵn vì các framework/ứng dụng đó có thể được công ty nâng cấp lên version mới. Bridge Pattern sẽ giúp chúng ta trong trường hợp này.
  • Cho phép ẩn các chi tiết implement từ client: do abstraction và implementation hoàn toàn độc lập nên chúng ta có thể thay đổi một thành phần mà không ảnh hưởng đến phía Client. Ví dụ, các lớp của chương trình view ảnh sẽ độc lập với thuật toán vẽ ảnh trong các implementation. Như vậy ta có thể update chương trình xem ảnh khi có một thuật toán vẽ ảnh mới mà không cần phải sửa đổi nhiều. [Client. Ví dụ, các lớp của ảnh xem chương trình sẽ độc lập với ảnh vẽ kỹ thuật trong các chương trình thực hiện. Như vậy ta có thể cập nhật ảnh chương trình khi có một ảnh mới vẽ kỹ thuật mà không cần phải sửa đổi nhiều.] x

    2. Nhược điểm

  • Có thể làm tăng độ phức tạp khi áp dụng cho một lớp có tính gắn kết cao

VI. Khi nào thì sử dụng

Bridge được sử dụng khi:

  • Khi muốn tách ràng buộc giữa Abstraction và Implementation, để có thể dễ dàng mở rộng độc lập nhau.
  • Khi cả Abstraction và Implementation của chúng nên được mở rộng bằng subclass.
  • Thay đổi trong thành phần được bổ sung thêm của một Abstraction mà không ảnh hưởng đối với các Client

VII. Design Pattern liên quan

  • Adapter và Bridge giống nhau là đều sẽ nhờ vào một lớp khác để thực hiện một số xử lý nào đó Khác nhau: Khác về mục đích sử dụng, tuy nhiên:
        - Adapter được dùng để biến đổi một class/ interface sang một dạng khác có thể sử dụng được, giúp các lớp không tương thích hoạt động cùng nhau mà bình thường là không thể.
        - Bridge được sử dụng để tách thành phần trừu tượng (abstraction) và thành phần thực thi (implementation) riêng biệt. Khác nhau về thời điểm ứng dụng
  • Abstract Factory: có thể sử dụng cùng với Bridge. Việc ghép nối này rất hữu ích khi một số trừu tượng được xác định bởi Bridge chỉ có thể hoạt động với các triển khai cụ thể. Trong trường hợp này, Abstract Factory có thể đóng gói các quan hệ này và ẩn sự phức tạp khỏi Client.
  • Builder: có thể kết hợp với Bridge. Director class có thể giữ vai trò là Abstraction, trong khi các Builder class khác giữ vai trò Implementation

Tài liệu tham khảo

[1] Refactoring.Guru

[2] Design Patterns for Dummies, Steve Holzner, PhD

[3] Head First, Eric Freeman

[4] Gang of Four Design Patterns 4.0

[5] Dive into Design Pattern

 

Chia sẽ bài viết :