Iterator Pattern

고승우·2025년 8월 1일
0

Rust

목록 보기
17/17

Iterator Pattern

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 value of the Iterator Pattern

The Iterator Pattern provides several key benefits that improve code quality:

  1. Encapsulation: It hides the complex internal structure of a collection. The client code doesn't need to know if the data is stored in a list, map, or tree.
  2. Single Responsibility Principle (SRP): It separates the logic for traversing a collection from the collection's main responsibility of storing and managing data.
  3. Safe Data Modification: It provides a controlled way to modify a collection during iteration (e.g., with an iterator's remove() method), preventing common bugs.
  4. Various Traversal Methods: A single collection can offer different iterators for different ways of traversal (e.g., forward, backward, or filtered).
  5. Flexibility and Reusability: Client code works with a standard iterator interface, not a specific collection class. This makes it easy to swap out collections and reuse traversal logic.

My Refactoring: The 5 Benefits in Action

Now, let's see how our real-world refactoring example from a Rust project demonstrates these five benefits.

  1. Encapsulation and Single Responsibility Principle (SRP)
    These two principles are about hiding complexity and separating concerns. Look at how we refactored the logic for finding parent-child relationships.
  • Before: The business logic was directly mixed with complex traversal logic. The function had to know exactly how to filter and chain together different node lists. This violates the SRP because the function is doing more than one job.
// 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();
  • After: We moved all the complex traversal logic into a new 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();
  1. Flexibility, Reusability, and Various Traversal Methods
    These benefits are about writing adaptable code. We achieved this by creating a single, powerful iterator, 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.

Conclusion

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!

profile
٩( ᐛ )و 

0개의 댓글