Iterator
Iterator — это поведенческий паттерн проектирования, который позволяет последовательно обходить элементы составного объекта, не раскрывая его внутреннего представления.
Проблема
Представьте, что у вас есть коллекция объектов, и вам нужно пройти по всем элементам этой коллекции. Коллекция может быть реализована как массив, связный список, дерево или что-то еще.
Если вы добавите методы обхода прямо в класс коллекции, это нарушит принцип единственной ответственности. Кроме того, код обхода будет дублироваться для каждого типа коллекции.
Решение
Паттерн Iterator предлагает вынести поведение обхода в отдельный класс-итератор, который будет хранить ссылку на обходимую коллекцию и отслеживать текущую позицию в обходе.
Структура
Iterator
├── next(): any
├── hasNext(): boolean
└── current(): any
ConcreteIterator
├── collection: ConcreteCollection
├── position: number
├── next(): any
├── hasNext(): boolean
└── current(): any
IterableCollection
├── createIterator(): Iterator
└── getItems(): any[]
ConcreteCollection
├── items: any[]
├── createIterator(): Iterator
└── getItems(): any[]
Пример реализации
JavaScript
// Интерфейс итератора
class Iterator {
next() {
throw new Error('Метод next должен быть переопределен');
}
hasNext() {
throw new Error('Метод hasNext должен быть переопределен');
}
current() {
throw new Error('Метод current должен быть переопределен');
}
}
// Интерфейс коллекции
class IterableCollection {
createIterator() {
throw new Error('Метод createIterator должен быть переопределен');
}
}
// Конкретная коллекция - список задач
class TaskList extends IterableCollection {
constructor() {
super();
this.tasks = [];
}
addTask(task) {
this.tasks.push(task);
}
getTasks() {
return this.tasks;
}
createIterator() {
return new TaskListIterator(this);
}
}
// Конкретный итератор для списка задач
class TaskListIterator extends Iterator {
constructor(taskList) {
super();
this.taskList = taskList;
this.position = 0;
}
next() {
if (this.hasNext()) {
const task = this.taskList.getTasks()[this.position];
this.position++;
return task;
}
return null;
}
hasNext() {
return this.position < this.taskList.getTasks().length;
}
current() {
if (this.position > 0 && this.position <= this.taskList.getTasks().length) {
return this.taskList.getTasks()[this.position - 1];
}
return null;
}
reset() {
this.position = 0;
}
}
// Итератор для обхода в обратном порядке
class ReverseTaskListIterator extends Iterator {
constructor(taskList) {
super();
this.taskList = taskList;
this.position = taskList.getTasks().length - 1;
}
next() {
if (this.hasNext()) {
const task = this.taskList.getTasks()[this.position];
this.position--;
return task;
}
return null;
}
hasNext() {
return this.position >= 0;
}
current() {
if (this.position >= 0 && this.position < this.taskList.getTasks().length) {
return this.taskList.getTasks()[this.position + 1];
}
return null;
}
reset() {
this.position = this.taskList.getTasks().length - 1;
}
}
// Итератор для фильтрации задач по статусу
class FilteredTaskIterator extends Iterator {
constructor(taskList, status) {
super();
this.taskList = taskList;
this.status = status;
this.position = 0;
this.filteredTasks = this.taskList.getTasks().filter(task => task.status === status);
}
next() {
if (this.hasNext()) {
const task = this.filteredTasks[this.position];
this.position++;
return task;
}
return null;
}
hasNext() {
return this.position < this.filteredTasks.length;
}
current() {
if (this.position > 0 && this.position <= this.filteredTasks.length) {
return this.filteredTasks[this.position - 1];
}
return null;
}
}
// Использование
const taskList = new TaskList();
// Добавляем задачи
taskList.addTask({ id: 1, title: 'Купить молоко', status: 'pending' });
taskList.addTask({ id: 2, title: 'Прочитать книгу', status: 'completed' });
taskList.addTask({ id: 3, title: 'Написать код', status: 'in_progress' });
taskList.addTask({ id: 4, title: 'Погулять с собакой', status: 'pending' });
taskList.addTask({ id: 5, title: 'Приготовить ужин', status: 'completed' });
console.log('=== Обход задач в прямом порядке ===');
const iterator = taskList.createIterator();
while (iterator.hasNext()) {
const task = iterator.next();
console.log(`${task.id}: ${task.title} (${task.status})`);
}
console.log('\n=== Обход задач в обратном порядке ===');
const reverseIterator = new ReverseTaskListIterator(taskList);
while (reverseIterator.hasNext()) {
const task = reverseIterator.next();
console.log(`${task.id}: ${task.title} (${task.status})`);
}
console.log('\n=== Обход только завершенных задач ===');
const completedIterator = new FilteredTaskIterator(taskList, 'completed');
while (completedIterator.hasNext()) {
const task = completedIterator.next();
console.log(`${task.id}: ${task.title} (${task.status})`);
}
TypeScript с интерфейсами
// Интерфейс итератора
interface Iterator<T> {
next(): T | null;
hasNext(): boolean;
current(): T | null;
}
// Интерфейс коллекции
interface IterableCollection<T> {
createIterator(): Iterator<T>;
}
// Конкретная коллекция - дерево файлов
class FileNode {
constructor(
public name: string,
public isDirectory: boolean = false,
public children: FileNode[] = []
) {}
addChild(child: FileNode): void {
if (this.isDirectory) {
this.children.push(child);
}
}
}
class FileTree implements IterableCollection<FileNode> {
constructor(private root: FileNode) {}
createIterator(): Iterator<FileNode> {
return new FileTreeIterator(this.root);
}
}
// Итератор для обхода дерева файлов (DFS)
class FileTreeIterator implements Iterator<FileNode> {
private stack: FileNode[] = [];
constructor(private root: FileNode) {
this.stack.push(root);
}
next(): FileNode | null {
if (!this.hasNext()) {
return null;
}
const node = this.stack.pop()!;
// Добавляем дочерние узлы в обратном порядке для правильного обхода
if (node.isDirectory) {
for (let i = node.children.length - 1; i >= 0; i--) {
this.stack.push(node.children[i]);
}
}
return node;
}
hasNext(): boolean {
return this.stack.length > 0;
}
current(): FileNode | null {
return this.stack.length > 0 ? this.stack[this.stack.length - 1] : null;
}
}
// Итератор для обхода только файлов (не директорий)
class FileOnlyIterator implements Iterator<FileNode> {
private fileIterator: FileTreeIterator;
constructor(root: FileNode) {
this.fileIterator = new FileTreeIterator(root);
}
next(): FileNode | null {
while (this.fileIterator.hasNext()) {
const node = this.fileIterator.next();
if (node && !node.isDirectory) {
return node;
}
}
return null;
}
hasNext(): boolean {
// Проверяем, есть ли еще файлы
const current = this.fileIterator.current();
return this.fileIterator.hasNext() &&
(current === null || !current.isDirectory || this.hasFilesAhead());
}
private hasFilesAhead(): boolean {
const tempIterator = new FileTreeIterator(this.fileIterator['root']);
while (tempIterator.hasNext()) {
const node = tempIterator.next();
if (node && !node.isDirectory) {
return true;
}
}
return false;
}
current(): FileNode | null {
return this.fileIterator.current();
}
}
// Использование
const root = new FileNode('root', true);
const documents = new FileNode('documents', true);
const images = new FileNode('images', true);
const downloads = new FileNode('downloads', true);
root.addChild(documents);
root.addChild(images);
root.addChild(downloads);
documents.addChild(new FileNode('report.pdf', false));
documents.addChild(new FileNode('presentation.pptx', false));
images.addChild(new FileNode('photo1.jpg', false));
images.addChild(new FileNode('photo2.png', false));
downloads.addChild(new FileNode('software.zip', false));
const fileTree = new FileTree(root);
console.log('=== Обход всех узлов дерева ===');
const treeIterator = fileTree.createIterator();
while (treeIterator.hasNext()) {
const node = treeIterator.next();
console.log(`${node.isDirectory ? '[DIR]' : '[FILE]'} ${node.name}`);
}
console.log('\n=== Обход только файлов ===');
const fileIterator = new FileOnlyIterator(root);
while (fileIterator.hasNext()) {
const file = fileIterator.next();
console.log(`[FILE] ${file!.name}`);
}
Когда использовать
Используйте Iterator, когда:
- Нужно обходить сложные структуры данных
- Хотите скрыть детали реализации коллекции
- Нужны разные способы обхода одной коллекции
- Хотите предоставить единый интерфейс для обхода разных коллекций
Преимущества
- Единый интерфейс для обхода разных коллекций
- Поддержка различных способов обхода
- Принцип единственной ответственности
- Принцип открытости/закрытости: можно добавлять новые итераторы
Недостатки
- Может быть избыточным для простых коллекций
- Увеличивает количество классов
- Может снизить производительность
Отличия от других паттернов
- Composite — группирует объекты в древовидные структуры
- Visitor — выполняет операции над элементами коллекции
- Chain of Responsibility — передает запросы по цепочке
Реальный пример
// Iterator для обхода результатов поиска
class SearchResultIterator {
constructor(results) {
this.results = results;
this.position = 0;
this.pageSize = 10;
}
next() {
if (this.hasNext()) {
const result = this.results[this.position];
this.position++;
return result;
}
return null;
}
hasNext() {
return this.position < this.results.length;
}
current() {
return this.position > 0 ? this.results[this.position - 1] : null;
}
// Дополнительные методы для пагинации
getCurrentPage() {
return Math.floor(this.position / this.pageSize) + 1;
}
getTotalPages() {
return Math.ceil(this.results.length / this.pageSize);
}
goToPage(page) {
this.position = (page - 1) * this.pageSize;
}
}
// Использование
const searchResults = Array.from({ length: 25 }, (_, i) => ({
id: i + 1,
title: `Результат ${i + 1}`,
url: `https://example.com/result-${i + 1}`
}));
const iterator = new SearchResultIterator(searchResults);
console.log('=== Обход результатов поиска ===');
console.log(`Всего результатов: ${searchResults.length}`);
console.log(`Всего страниц: ${iterator.getTotalPages()}`);
// Обходим первую страницу
console.log('\n--- Страница 1 ---');
let count = 0;
while (iterator.hasNext() && count < 10) {
const result = iterator.next();
console.log(`${result.id}: ${result.title}`);
count++;
}
console.log(`\nТекущая страница: ${iterator.getCurrentPage()}`);
Iterator — это универсальный способ обхода коллекций!