As a developer, it’s common to handle collection elements using a for loop. But what if you need to delete an element during the iteration? This can cause big problems.
Let’s look at how a simple for loop can break your code and how the Iterator pattern helps you write safer, higher-quality code.
The Iterator Pattern provides several key benefits that improve code quality:
Now, let's see how our real-world refactoring example from a Rust project demonstrates these five benefits.
// BEFORE: Traversal logic is mixed with business logic.
let parent_child_pairs: Vec<(Uuid, Uuid)> = self.state
.get_container_nodes()
.values()
.filter_map(|container| container.parent_id.map(|p_id| (p_id, container.id)))
.chain( // Complex chaining...
self.state
.get_entity_nodes()
.values()
.filter_map(|entity| entity.parent_id.map(|p_id| (p_id, entity.id)))
)
.collect();
ParentChildPairsIter
. Now, the business logic is clean and simple. The complexity is encapsulated inside the iterator. The function now has a single responsibility: to update the child IDs, while the iterator has a single responsibility: to traverse and find the pairs.// AFTER: Complexity is hidden (Encapsulation) and logic is separated (SRP).
let parent_child_pairs: Vec<(Uuid, Uuid)> = self.state
.parent_child_pairs_iter() // Just call the iterator!
.collect();
all_nodes_iter
, that works for different node types using a NodeRef
enum.Before: We had separate functions for each node type (list_entity_nodes
, list_container_nodes
). This was not flexible because if we added a NewNodeType
, we would have to add another function, and the client code would need to be updated to call it.
After: We now have one reusable iterator, all_nodes_iter
. This single iterator provides us with various traversal methods because we can chain functions like filter_map
to get exactly what we need. The client code is now more flexible because it doesn't depend on specific methods, only on the standard iterator interface.
// The single, flexible `all_nodes_iter` can be used for various traversals.
// Traversal Method 1: Get only Container nodes.
let containers: Vec<&ContainerNode> = self.state
.all_nodes_iter()
.filter_map(|node| node.as_container())
.collect();
// Traversal Method 2: Get only Entity nodes.
let entities: Vec<&EntityNode> = self.state
.all_nodes_iter()
.filter_map(|node| node.as_entity())
.collect();
// Traversal Method 3: Get all node IDs.
let all_ids: Vec<Uuid> = self.state
.all_nodes_iter()
.map(|node| node.get_id())
.collect();
This shows how one iterator can provide many different ways to view the same data, which is a core strength of the pattern.
A Note on Safe Data Modification
While our refactoring examples focus on reading data, the Iterator pattern also provides a standard, safe way to handle deletion. An iterator's remove()
method allows the collection to manage its internal state correctly during removal, which prevents the bugs that happen when you try to remove an item from a list using a simple for loop with an index.
The Iterator Pattern is more than just a for loop. As we saw in our code, it's a powerful tool for writing clean, flexible, and maintainable software by separating traversal logic from data structures. By applying its principles, you can significantly improve your code's design. Happy coding!