Tổng quan về Bridge design pattern và việc thực hiện nó trong Dart và Flutter
Trong bài viết này, mình sẽ phân tích và thực hiện một mẫu thiết kế cấu trúc (structural design pattern) khác có xu hướng tương đối khó hiểu so với các design pattern khác, nhưng đồng thời cũng rất thiết thực và hữu ích - đó là Bridge.
Design Pattern Bridge là gì?
Bridge, còn được gọi là Handle/Body, thuộc danh mục các mẫu thiết kế cấu trúc (structural design pattern). Mục đích của design pattern này được mô tả trong wikipedia như sau:
Tách abstraction khỏi implementation của nó để hai phần này có thể thay đổi một cách độc lập.
Cách thông thường để abstraction có một trong số các implementation khả thi là sử dụng inheritance - một abstraction xác định interface trong khi các concrete subclass thực hiện nó theo những cách khác nhau. Tuy nhiên, cách tiếp cận này không linh hoạt lắm vì nó liên kết implementation với abstraction tại thời điểm biên dịch và không thể thay đổi implementation tại thời điểm chạy. Điều gì sẽ xảy ra nếu chúng ta muốn implementation được chọn và trao đổi trong thời gian chạy?
Bridge design pattern tách abstraction khỏi implementation của nó để hai phần này có thể thay đổi một cách độc lập. Trong trường hợp này, abstraction sử dụng một abstraction khác làm implementation của nó thay vì sử dụng implementation trực tiếp. Mối quan hệ giữa abstraction và implementation của nó được gọi là bridge - nó làm cầu nối giữa abstraction và implementation, cho phép chúng thay đổi một cách độc lập.
Nếu các thuật ngữ Abstraction và Implementation nghe có vẻ quá hàn lâm đối với bạn, hãy tưởng tượng điều này: abstraction (hoặc interface) chỉ là một layer cấp cao cho một số entity cụ thể. Layer này chỉ là một interface không được tự thực hiện bất kỳ công việc thực sự nào - nó sẽ ủy thác công việc cho implementation layer. Một ví dụ điển hình về điều này là GUI (giao diện người dùng đồ họa) và OS (hệ điều hành). GUI chỉ là một lớp cấp cao nhất để người dùng giao tiếp với hệ điều hành, nhưng bản thân nó không thực hiện bất kỳ công việc thực sự nào - nó chỉ chuyển các lệnh (sự kiện) của người dùng tới nền tảng. Và điều quan trọng là cả GUI và OS đều có thể được mở rộng riêng biệt với nhau, ví dụ: một ứng dụng dành cho desktop có thể có view/panel/dashboard khác nhau và đồng thời hỗ trợ một số API (có thể chạy trên Windows, Linux và macOS) - hai phần này có thể thay đổi một cách độc lập. Nghe giống như Bridge design pattern phải không?
Phân tích
Cấu trúc chung của Bridge design pattern trông như sau:
- Abstraction - xác định một interface cho abstraction và duy trì một tham chiếu đến một object kiểu Implementation;
- Refined abstraction - thực hiện Abstraction interface và cung cấp các biến thể khác nhau để control logic;
- Implementation - xác định một interface cho các implementation class. Abstraction chỉ có thể giao tiếp với một Implementation object thông qua các method được khai báo ở đó;
- Concrete implementation - thực hiện Implementation interface và chứa code dành riêng cho nền tảng.
Applicability
Bridge design pattern nên được sử dụng khi bạn muốn phân chia một monolithic class có một số biến thể của một số chức năng. Trong trường hợp này, pattern cho phép chia class thành nhiều phân cấp class có thể được thay đổi một cách độc lập - nó đơn giản hóa việc bảo trì code, các class nhỏ hơn giảm thiểu nguy cơ phá vỡ code hiện có. Một ví dụ điển hình về cách tiếp cận này là khi bạn muốn sử dụng một số cách tiếp cận khác nhau trong persistence layer , ví dụ: cả database và persistence của hệ thống tệp tin.
Ngoài ra, bridge design pattern nên được sử dụng khi cả abstraction và implementation của chúng đều có thể mở rộng bằng cách phân lớp (subclassing) - pattern cho phép kết hợp các abstraction và implementation khác nhau và mở rộng chúng một cách độc lập.
Cuối cùng, bridge design pattern là một cứu cánh khi bạn cần switch các implementation trong thời gian chạy (run-time). Pattern cho phép bạn thay thế implementation object bên trong abstraction - bạn có thể chèn nó qua constructor hoặc chỉ cần gán nó dưới dạng new value cho một field/property.
Implementation
Đối với phần implementation, chúng ta sẽ thực hiện persistence layer cho ví dụ của chúng ta bằng cách sử dụng Bridge design pattern.
Giả sử ứng dụng của bạn sử dụng external SQL database (không phải tùy chọn SQLite cục bộ trong thiết bị của bạn mà là tùy chọn đám mây). Mọi thứ đều ổn cho đến khi các vấn đề kết nối lung tung xuất hiện. Trong trường hợp này, có hai tùy chọn: bạn không cho phép người dùng sử dụng ứng dụng và cung cấp màn hình mất kết nối funny hoặc bạn có thể lưu trữ dữ liệu trong một số loại lưu trữ cục bộ và đồng bộ hóa dữ liệu sau khi kết nối lại. Rõ ràng, cách tiếp cận thứ hai thân thiện với người dùng hơn, nhưng làm thế nào để thực hiện nó?
Trong persistence layer, có nhiều repositories cho mỗi loại entity. Các repository chia sẻ một interface chung - đó là abstraction của chúng ta. Nếu bạn muốn thay đổi storage type (để sử dụng cục bộ hoặc đám mây) tại thời điểm chạy, các kho này không thể tham chiếu đến implementation cụ thể của lưu trữ, chúng sẽ sử dụng một số loại abstraction được chia sẻ giữa các loại lưu trữ khác nhau. Chúng ta có thể xây dựng một abstraction (interface) khác sau đó được triển khai bởi các kho lưu trữ cụ thể. Bây giờ chúng ta kết nối abstraction của kho lưu trữ của mình với interface của kho lưu trữ - voilà, đó là cách Bridge design pattern được đưa vào sử dụng trong ứng dụng của chúng ta! Trước tiên, hãy kiểm tra sơ đồ class và sau đó nghiên cứu một số chi tiết thực hiện.
Class diagram
Biểu đồ class dưới đây cho thấy việc thực hiện Bridge design pattern:
EntityBase là một abstract class được sử dụng làm base class cho tất cả các entity class. Class chứa một id property và một phương thức khởi tạo có tên EntityBase.fromJson để ánh xạ đối tượng JSON tới class field.
Customer và Order là các concrete entity mở rộng abstract class EntityBase. Customer class chứa các name property và email property, constructor được đặt tên là Customer.fromJson để map JSON object tới các class field và một toJson() method để map các class field với JSON map object tương ứng. Order class chứa dishes (danh sách các món ăn theo order) và tổng số các field, một constructor có tên Order.fromJson và một toJson() method tương ứng.
IRepository là một abstract class được sử dụng làm interface cho các repository:
- getAll() - trả về tất cả các bản ghi từ repository;
- save() - lưu entity kiểu EntityBase trongrepository.
CustomerRepository và OrderRepository là các concrete repository class mở rộng abstract class IRepository và thực hiện các abstract method của nó. Ngoài ra, các class này chứa storage property kiểu IStorage được đưa vào kho lưu trữ thông qua constructor.
IStorage là một abstract class được sử dụng làm interface cho các kho:
- getTitle() - trả về tiêu đề của kho. Method được sử dụng trong giao diện người dùng;
- fetchAll<T>() - trả về tất cả các bản ghi kiểu T từ bộ lưu trữ;
- store<T>() - lưu trữ một bản ghi kiểu T trong bộ lưu trữ.
FileStorage và SqlStorage là các concrete storage class mở rộng abstract class IStorage và triển khai các abstract method của nó. Ngoài ra, FileStorage class sử dụng JsonHelper class và các static method của nó để tuần tự hóa/giải thích các JSON object.
BridgeExample khởi tạo và chứa cả repositories - customer and orde - được sử dụng để truy xuất dữ liệu tương ứng. Ngoài ra, loại lưu trữ của các kho này có thể được thay đổi giữa FileStorage và SqlStorage một cách riêng biệt tại thời điểm chạy.
EntityBase
Một abstract class lưu trữ id field và được mở rộng bởi tất cả các entity class.
Customer
Một simple class để lưu trữ thông tin về khách hàng: tên và email của khách hàng. Ngoài ra, constructor tạo ra các giá trị ngẫu nhiên khi khởi tạo Customer object.
Order
Một simple class để lưu trữ thông tin về đơn đặt hàng: danh sách các món ăn trong đó và tổng giá của đơn đặt hàng. Ngoài ra, constructor tạo ra các giá trị ngẫu nhiên khi khởi tạo Order object.
JsonHelper
Một helper class được FileStorage sử dụng để serialise các object kiểu EntityBase thành các JSON map object và deserialise chúng khỏi chuỗi JSON.
IRepository
Một interface định nghĩa các method được thực hiện bởi các derived repository class. Ngôn ngữ Dart không hỗ trợ interface như một class type, vì vậy chúng ta xác định interface bằng cách tạo một abstract class và cung cấp method header (tên, kiểu trả về, các tham số) mà không có default implementation.
Concrete repositories
CustomerRepository - một triển khai cụ thể của IRepository interface để lưu trữ dữ liệu của khách hàng (customers’ data).
OrderRepository - một implementation cụ thể của IRepository interface để lưu trữ dữ liệu của các đơn đặt hàng.
IStorage
Một interface xác định các method được thực thi bởi các derived storage class.
Concrete storages
FileStorage — một implementation cụ thể của IStorage interface để lưu trữ một object trong bộ lưu trữ dưới dạng file - behaviour này mock bằng cách lưu trữ một object dưới dạng chuỗi JSON.
SqlStorage - một implementation cụ thể của IStorage interface để lưu trữ một object trong storage dưới dạng entity - behaviour này có thể mock bằng cách sử dụng cấu trúc dữ liệu Map và thêm các entity cùng loại vào danh sách.
Ví dụ
Trước hết, một markdown file được chuẩn bị và cung cấp dưới dạng mô tả của pattern:
BridgeExample chứa một danh sách các storages - các instance của SqlStorage class và FileStorage class. Ngoài ra, nó khởi tạo Customer repository và Order repository. Trong các kho lưu trữ, loại lưu trữ cụ thể có thể được hoán đổi cho nhau bằng cách kích hoạt onSelectedCustomerStorageIndexChanged() cho CustomersRepository và onSelectedOrderStorageIndexChanged() cho OrdersRepository.
concrete repository không quan tâm đến loại lưu trữ cụ thể mà nó sử dụng miễn là nơi lưu trữ implement IStorage interface và tất cả các abstract method của nó. Do đó, abstraction (kho lưu trữ) được tách biệt khỏi implementor (lưu trữ) - concrete implementation của lưu trữ có thể được thay đổi cho kho lưu trữ tại thời điểm chạy, kho lưu trữ không phụ thuộc vào chi tiết triển khai của nó.
Như bạn có thể thấy trong ví dụ, kiểu lưu trữ có thể được thay đổi cho từng kho lưu trữ riêng biệt và tại thời điểm chạy - điều này sẽ không thể thực hiện được bằng cách sử dụng phương pháp kế thừa simple class.
Tất cả các code change cho Bridge design pattern và triển khai mẫu của nó có thể được tìm thấy tại đây.
Bài viết được lược dịch từ Mangirdas Kazlauskas.
Chau Le
Cực thích đồ ngọt như sữa, kem. Thích phiêu lưu mạo hiểm nhưng vì Covid mà vô tình trở thành người chia sẻ nội dung lập trình :))
follow me :
Bài viết liên quan
Tự học Dart: Các Dart Operators (toán tử) bạn cần biết
Sep 02, 2023 • 18 min read
Flutter cơ bản: Điều cần biết khi lập trình ứng dụng đầu tiên
Aug 23, 2023 • 13 min read
Dart là gì? Giới thiệu cơ bản về ngôn ngữ lập trình Dart
Aug 21, 2023 • 11 min read
Flutter là gì? Vì sao nên học công cụ lập trình Flutter?
Aug 19, 2023 • 11 min read
Flutter cơ bản: Widget Tree, Element Tree & Render Tree
Aug 19, 2023 • 10 min read
So sánh StatelessWidget và StatefulWidget
Jul 17, 2022 • 12 min read