Design Pattern cùng Flutter. Tập 5: Composite - "Chúng sinh bình đẳng"
Giới thiệu
Cấu trúc theo dạng cây không còn lạ gì với chúng ta nữa, ví dụ: Một folder sẽ có thể chứa nhiều folder khác nhau hoặc là chứa các tệp, nếu chỉ chứa mỗi 1 loại tệp và 1 loại folder thì không đáng kể, nhưng nếu ta có nhiều loại tệp khác nhau hay folder khác nhau thì việc chỉ định cho nó một logic cụ thể, một giao diện cụ thể thì càng làm cho mã code chúng ta thêm phức tạp, đặc biệt là cấu trúc dữ liệu động. Đây cũng chính là lý do mẫu thiết kế Composite ra đời - để trừu tượng hoá các lớp một cách đồng nhất, xử lý lớp riêng lẻ và nhóm đối tượng theo cùng một cách.
Vậy...
Composite là ai, địa chỉ nhà ở đâu?
Composite là một loại mẫu thiết kế thuộc structural được ra đời nhằm tạo ra hệ thống phân cấp bậc cho phép xử lý lớp riêng lẻ và nhóm đối tượng theo cùng một cách thống nhất. Cái gọi là "Chúng sinh bình đẳng" cũng từ đó mà ra, mỗi đối tượng component đều thống nhất cách xử lý bằng cách implement component được định nghĩa từ trước.
Thế...
Mục tiêu là gì, tại sao nó lại tồn tại
Mẫu thiết kế Composite ra đời với mục tiêu sau:
- Đơn giản hoá việc phân cấp các đối tượng: Việc phân cấp các đối tượng thành cấu trúc cây sẽ đơn giản trong việc quản lý và xử lý các đối tượng trong cấu trúc phức tạp.
- Cho phép tạo ra sự đồng nhất trong việc xử lý đối tượng.
- Tách các lớp lớn thành các lớp nhỏ hơn (tính trừu tượng) để giảm thiểu số lượng mã code trong lớp lớn, tăng tính "dễ đọc, dễ hiểu, dễ mở rộng" cho các develop khác =)))
Composite Class Diagram
Cách tiếp cận tổng quát của mẫu thiết kế Composite được biểu diễn bằng sơ đồ lớp bên dưới:
Các thành phần chính
- Component: Interface hoặc abstract class đại diện cho một lớp thành phần, định nghĩa ra hàm dùng chung cho các đối tượng.
- Leaf: Là lớp kế thừa Component , biểu diễn các đối tượng riêng lẻ (không có con).
- Composite: Biểu diễn các đối tượng có thể chứa các đối tượng khác (cả Leaf và Composite).
Ứng dụng
Mẫu thiết kế composite rất hữu ích với trường hợp các đối tượng được tổ chức theo dạng cây. Nếu bạn thấy các trường hợp như tập hợp các groups hoặc collections thì bạn nên nghĩ ngay đến mẫu thiết kế này. Phần khó nhất của mẫu thiết kế này chủ yếu là phát hiện ra nơi và thời điểm áp dụng nó. Và nếu bạn đã phát hiện được khả năng sử dụng mẫu thiết kế, mình sẽ mô tả chi tiết triển khai ở dưới đây.
Thực hành
Ví dụ đơn giản nhất có lẽ là cấu trúc cây Widget trong Flutter. Widget là một Component, composite sẽ là widget chứa các widget con như: Column, Row, Wrap, Stack,..Còn Leaf sẽ là các widget không chứa ai cả như Text, Icon, Image,...
Ví dụ thực tế: Khi làm việc với thương mại điện tử, cụ thể là các danh mục và sản phẩm, thường sẽ có phân theo cấp bậc. Mỗi danh mục có thể bao gồm nhiều danh mục và sản phẩm khác nhau.
Cấu trúc ví dụ:
Ở hình trên ta có thể thấy sự phân cấp rõ ràng nhưng để việc sử dụng nó linh hoạt hơn như mỗi component phải chứa các tiêu đề, tính tổng tiền, xây dựng UI khác nhau, thì việc sử dụng mẫu thiết kế Composite là rất cần thiết trong trường hợp này.
Đầu tiên, chúng ta xem qua một lượt bằng sơ đồ bên dưới:
- Component: sẽ đóng vai trò component trong Composite, là Product hoặc Category.
- Category: sẽ đóng vai trò là composite, chứa danh sách component và sẽ implement interface Component.
- Product: là Leaf, không có con cái nào cả, cũng sẽ implement interface Component.
Về sơ đồ tổng quát và cái nhìn chung đã có, ta cùng bắt tay vào xử lý nó luôn nhé ^^.
Lớp đầu tiên chúng ta xây dựng sẽ là một lớp abstract Component, bao gồm các phương thức chung như: getName(), getPrice() và buildItem():
abstract class Component {
String getName();
double getPrice();
Widget buildItem();
}
Tiếp đến ta xây dựng Composite, implement abstract trên, trong này ta sẽ define ra cụ thể các phương thức của nó, và sẽ chứa danh sách các Component
class Category implements Component {
final String name;
final List<Component> children;
Category({
required this.name,
this.children = const [],
});
void add(Component component) {
children.add(component);
}
@override
double getPrice() {
return children.fold(0, (prev, element) => prev + element.getPrice());
}
@override
String getName() {
return name;
}
@override
Widget buildItem() {
return ExpansionTile(
title: Text(name),
initiallyExpanded: true,
children: children.map((e) => e.buildItem()).toList(),
);
}
}
Tương tự, lớp Leaf cũng sẽ implement abstract Component:
class Product implements Component {
final String name;
final double price;
Product({
required this.name,
required this.price,
});
@override
double getPrice() {
return price;
}
@override
String getName() {
return name;
}
@override
Widget buildItem() {
return ListTile(
title: Text(name),
trailing: Text(price.formatVND()),
);
}
}
Về data mẫu, ta có Thời trang bao gồm: thời trang nam, thời trang nữ và áo mưa siêu nhân. Trong thời trang nam sẽ chứa: quần què và quần tây. Còn thời trang nữ sẽ chứa: váy bầu và váy ống suông.
static List<Component> categories = [
Category(
name: 'Thời trang',
children: [
Category(
name: 'Thời trang nam',
children: [
Product(
name: 'Quần què',
price: 1000000,
),
Product(
name: 'Quần tây',
price: 200000,
),
],
),
Category(
name: 'Thời trang nữ',
children: [
Product(
name: 'Váy bầu',
price: 500000,
),
Product(
name: 'Quần ống suông',
price: 1000000,
),
],
),
Product(
name: "Áo mưa siêu nhân",
price: 100000,
),
],
),
];
Và cuối cùng là hiển thị lên màn hình, à, cũng đừng quên tính tiền tất cả sản phẩm trong giỏ hàng nhé, không thì lại đầu tháng phải ăn mì tôm vì "lỡ tay" ấn mua 👻
class CompositePage extends StatelessWidget {
const CompositePage({super.key});
@override
Widget build(BuildContext context) {
final components = ComponentDataSource.categories;
return Scaffold(
appBar: const PrimaryAppBar(
title: 'Composite',
),
body: SafeArea(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'Giỏ hàng',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 20),
...components.map((category) {
return category.buildItem();
}),
const Divider(),
],
),
),
),
),
bottomSheet: Container(
padding: const EdgeInsets.all(16),
width: double.maxFinite,
child: Text('Tổng tiền: ${components.getTotalPriceFormatted()}'),
));
}
}
Thành quả
Và cuối cùng, tadaaa, đây là thành quả mà ta nhận được:
Code theo một design pattern làm ta thấy tự tin hơn hẳn đúng không nào, các bạn có thể xem lại bằng github này nhé ^^
Tổng kết
Tuy series mới bước đầu tập tễnh, nhưng cũng được sự ủng hộ của các bạn. Mình cảm ơn các bạn đã đón chờ series này, cũng là nguồn động lực để mình viết tiếp. Đón chờ mình tại tập tiếp theo của Design Pattern. Tập 6: Strategy - "Chiến lược toàn năng"
All rights reserved