Lập trình · Mẫu thiết kế
Mẫu Iterator (Design Pattern) giải thích kèm ví dụ
Trả lời nhanh
Iterator là một mẫu thiết kế hành vi (behavioral), cung cấp cách truy cập lần lượt các phần tử của một tập hợp mà không để lộ cách tập hợp được lưu. Logic duyệt nằm trong một đối tượng iterator riêng với phương thức như next(), nên một tập hợp có thể cung cấp nhiều thứ tự duyệt và mã client lặp qua mảng, cây và luồng theo cùng một cách. Trong TypeScript và JavaScript, mẫu này được tích hợp sẵn vào ngôn ngữ qua Symbol.iterator và for...of.
Mục đích và vấn đề mà mẫu này giải quyết
Mục đích: cung cấp cách truy cập tuần tự các phần tử của một đối tượng tổng hợp mà không để lộ biểu diễn bên trong. Iterator là một trong các mẫu hành vi trong sách Gang of Four (GoF).
Hãy tưởng tượng một lớp lưu sơ đồ tổ chức của công ty dưới dạng cây. Các phần khác nhau của chương trình cần liệt kê mọi nhân viên: một báo cáo đi theo từng cấp, báo cáo khác cần thứ tự theo chiều sâu. Nếu mỗi báo cáo tự duyệt cây, mọi báo cáo đều phụ thuộc các trường nội bộ của cây, và đổi cây sang cách lưu khác sẽ làm hỏng tất cả.
Với Iterator, cây cung cấp các đối tượng iterator. Báo cáo yêu cầu một iterator và gọi next() cho đến khi xong. Báo cáo không biết dữ liệu là cây, mảng hay các dòng được lấy từ cơ sở dữ liệu theo từng trang.
Cấu trúc và các thành phần
| Vai trò | Trách nhiệm |
|---|---|
| Iterator | Interface để duyệt, thường là next() trả về phần tử kế tiếp và một cờ cho biết việc duyệt đã kết thúc chưa |
| Concrete iterator | Cài đặt việc duyệt cho một tập hợp và nhớ vị trí hiện tại |
| Aggregate (iterable) | Interface có phương thức tạo iterator |
| Concrete aggregate | Tập hợp; trả về một concrete iterator mới mỗi lần được yêu cầu |
| Client | Dùng iterator để thăm các phần tử mà không biết cấu trúc của tập hợp |
Trong JavaScript các vai trò này có tên chuẩn, được định nghĩa bởi các giao thức lặp trong ES2015. Một iterable có phương thức [Symbol.iterator](). Một iterator có phương thức next() trả về { value, done }. Mọi vòng lặp for...of, spread [...x] và phép gán destructuring đều dùng các giao thức này.
Cài đặt bằng TypeScript: iterator viết tay
Lớp range này chỉ lưu điểm đầu, điểm cuối và bước nhảy, không lưu danh sách các số. Iterator của nó tính từng giá trị theo yêu cầu.
class Range implements Iterable<number> {
constructor(private start: number, private end: number, private step = 1) {}
[Symbol.iterator](): Iterator<number> {
let current = this.start;
const { end, step } = this;
return {
next(): IteratorResult<number> {
if (current >= end) return { value: undefined, done: true };
const value = current;
current += step;
return { value, done: false };
},
};
}
}
const evens = new Range(0, 10, 2);
for (const n of evens) console.log(n); // 0 2 4 6 8 (one per line)
console.log([...evens]); // [ 0, 2, 4, 6, 8 ]
console.log(Math.max(...new Range(1, 4))); // 3Mỗi lần gọi [Symbol.iterator]() trả về một iterator mới với vị trí current riêng. Vì vậy evens có thể được lặp hai lần. Nếu tập hợp trả về cùng một iterator mỗi lần, vòng lặp thứ hai sẽ không thấy phần tử nào.
Generator: iterator với ít mã hơn
Hàm generator, viết bằng function*, tự động tạo một iterator. Mỗi yield tạo ra giá trị kế tiếp, và hàm tạm dừng cho đến khi next() được gọi lại. Ở đây một cây nhị phân cung cấp cách duyệt in-order mà không để lộ các nút của nó:
class TreeNode<T> {
constructor(public value: T, public left?: TreeNode<T>, public right?: TreeNode<T>) {}
}
class BinaryTree<T> implements Iterable<T> {
constructor(private root?: TreeNode<T>) {}
*[Symbol.iterator](): Generator<T> {
yield* inOrder(this.root);
}
}
function* inOrder<T>(node?: TreeNode<T>): Generator<T> {
if (!node) return;
yield* inOrder(node.left);
yield node.value;
yield* inOrder(node.right);
}
const tree = new BinaryTree(
new TreeNode(5, new TreeNode(3, new TreeNode(1)), new TreeNode(8)),
);
console.log([...tree]); // [ 1, 3, 5, 8 ]
for (const value of tree) {
if (value > 3) break; // stops early; the rest is never computed
console.log(value); // 1, then 3
}Khi nào nên dùng và khi nào không
- Dùng khi tập hợp có cấu trúc nội bộ phức tạp (cây, đồ thị, API phân trang) và bạn muốn client lặp qua nó một cách đơn giản.
- Dùng để cung cấp nhiều thứ tự duyệt trên cùng dữ liệu, như theo chiều sâu và theo chiều rộng.
- Dùng cho các dãy lười hoặc vô hạn, nơi giá trị chỉ được tính khi được yêu cầu.
- Dùng để cho các tập hợp khác nhau một interface vòng lặp thống nhất.
- Đừng viết iterator tùy chỉnh cho dữ liệu vốn đã là mảng. Hãy trả về mảng (hoặc bản sao chỉ đọc) và để nơi gọi dùng cách lặp có sẵn.
- Đừng dùng iterator lười khi nơi gọi cần truy cập ngẫu nhiên theo chỉ số hoặc cần biết trước độ dài; mảng phù hợp hơn.
Ứng dụng thực tế
- JavaScript:
Array,Map,SetvàStringlà các iterable, cònmap.entries()hoặcset.values()trả về iterator. - Java: interface
java.util.IteratorvớihasNext()vànext(), được vòng lặpfornâng cao dùng trên mọiIterable. - Python: giao thức iterator với
__iter__và__next__, cùng các hàm generator dùngyield. - Cơ sở dữ liệu và API: con trỏ (cursor) trên kết quả truy vấn và API HTTP phân trang trả kết quả từng trang một, để client lặp qua kết quả rất lớn mà không phải tải tất cả vào bộ nhớ.
Các mẫu liên quan
| Mẫu | Quan hệ với Iterator |
|---|---|
| Composite | Iterator thường được dùng để duyệt cây Composite mà không để lộ các nút của chúng. |
| Visitor | Iterator duyệt các phần tử; Visitor định nghĩa việc cần làm với mỗi phần tử. Chúng thường được dùng cùng nhau. |
| Factory Method | Phương thức tạo iterator, như [Symbol.iterator](), là một factory method: mỗi tập hợp trả về lớp iterator của riêng nó. |
| Memento | Iterator có thể lưu vị trí của nó dưới dạng memento để việc duyệt có thể tiếp tục sau. |
Câu hỏi thường gặp
Iterable và iterator khác nhau thế nào?
Iterable là đối tượng có thể tạo ra một iterator, thông qua [Symbol.iterator]() trong JavaScript. Iterator là đối tượng giữ vị trí hiện tại và trả về từng phần tử một qua next().
Generator có phải là iterator không?
Có. Gọi một hàm generator trả về đối tượng generator, vừa là iterator (có next()) vừa là iterable (trả về chính nó từ [Symbol.iterator]()).
Tại sao không đơn giản là công khai mảng bên trong?
Công khai mảng ràng buộc mọi nơi gọi vào lựa chọn lưu trữ đó và cho phép họ sửa đổi nó. Iterator che giấu cấu trúc, nên bạn có thể đổi nó sau này hoặc tính phần tử một cách lười mà không phải sửa nơi gọi.
Internal iterator là gì?
Internal iterator là phương thức nhận một callback và chạy nó cho từng phần tử, như array.forEach(fn). Tập hợp kiểm soát vòng lặp. External iterator, như next(), cho client tự kiểm soát khi nào tiến tiếp và khi nào dừng.