Back to Blog
design-patternscsharptypescriptbehavioral-patterns

Iterator Pattern — FixIt Pro Series #15

Traversing a handyman's job card queue without exposing the internal list structure. Learn the Iterator pattern and why you've been using it every time you write a for...of loop, in C# and TypeScript.


Series: Design Patterns with FixIt Pro  ·  Episode 15 / 22  ·  Behavioral Pattern
Previous: #14 — Command Pattern


The Scenario

Every handyman in FixIt Pro has a job queue, an ordered list of job cards assigned to them for the day. The scheduler adds jobs. The handyman works through them in priority order. The reporting module counts them. The billing system totals them up.

Each of these components needs to traverse the queue, but none of them should care how the queue is internally structured. Is it a List<JobCard>? A linked list? A priority queue sorted by urgency? A circular buffer for recurring jobs?

Right now, callers access the internal list directly:

for (int i = 0; i < handyman.Jobs.Count; i++)
    Process(handyman.Jobs[i]);

This couples every caller to the internal structure. Change List<T> to a priority queue and every caller breaks.

The Iterator pattern decouples traversal from the collection.


What Is the Iterator Pattern?

Provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation.

The iterator is a separate object that knows how to traverse a specific collection. The collection provides an iterator; the client uses the iterator. The collection's internal structure stays private.

The four participants

Role FixIt Pro equivalent
Iterator interface IJobIterator — declares HasNext() and Next()
Concrete Iterator PriorityJobIterator — traverses by urgency order
Aggregate interface IJobCollection — declares CreateIterator()
Concrete Aggregate HandymanJobQueue — the collection, returns its iterator

C# Implementation

In C#, the Iterator pattern is baked into the language via IEnumerable<T> and IEnumerator<T>. We'll implement the pattern manually first to understand the mechanics, then show how C# formalises it with yield return.

// ── Job Card (simplified) ──────────────────────────────────

public class JobCard
{
    public string JobId    { get; init; } = Guid.NewGuid().ToString()[..8];
    public string Title    { get; init; } = string.Empty;
    public string Category { get; init; } = string.Empty;
    public int    Priority { get; init; } = 0; // higher = more urgent
    public string Status   { get; set;  } = "Queued";

    public override string ToString() =>
        $"[P{Priority}] #{JobId} '{Title}' [{Category}] — {Status}";
}

// ── Iterator Interface ─────────────────────────────────────

public interface IJobIterator
{
    bool    HasNext();
    JobCard Next();
    void    Reset();
}

// ── Aggregate Interface ────────────────────────────────────

public interface IJobCollection
{
    IJobIterator CreateIterator();
    void         AddJob(JobCard job);
    int          Count { get; }
}

// ── Concrete Iterator — Priority Order ─────────────────────

public class PriorityJobIterator : IJobIterator
{
    private readonly List<JobCard> _sorted;
    private int                    _index = 0;

    public PriorityJobIterator(IEnumerable<JobCard> jobs)
    {
        _sorted = jobs
            .OrderByDescending(j => j.Priority)
            .ToList();
    }

    public bool    HasNext() => _index < _sorted.Count;
    public JobCard Next()    => _sorted[_index++];
    public void    Reset()   => _index = 0;
}

// ── Concrete Aggregate ─────────────────────────────────────

public class HandymanJobQueue : IJobCollection
{
    private readonly List<JobCard> _jobs = new();
    public string HandymanName { get; }

    public HandymanJobQueue(string handymanName)
    {
        HandymanName = handymanName;
    }

    public void AddJob(JobCard job) => _jobs.Add(job);
    public int  Count               => _jobs.Count;

    public IJobIterator CreateIterator() =>
        new PriorityJobIterator(_jobs);
}

// ── IEnumerable<T> approach — idiomatic C# ────────────────

public class HandymanJobQueueEnumerable : IEnumerable<JobCard>
{
    private readonly List<JobCard> _jobs = new();
    public string HandymanName { get; }

    public HandymanJobQueueEnumerable(string handymanName)
    {
        HandymanName = handymanName;
    }

    public void AddJob(JobCard job) => _jobs.Add(job);

    public IEnumerator<JobCard> GetEnumerator()
    {
        foreach (var job in _jobs.OrderByDescending(j => j.Priority))
            yield return job;
    }

    System.Collections.IEnumerator
        System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
}

// ── Client Code ────────────────────────────────────────────

class Program
{
    static void Main()
    {
        var queue = new HandymanJobQueue("James Nghipandua");

        queue.AddJob(new JobCard { Title = "Fix leaking tap",       Category = "Plumbing",   Priority = 1 });
        queue.AddJob(new JobCard { Title = "Replace circuit board",  Category = "Electrical", Priority = 3 });
        queue.AddJob(new JobCard { Title = "Repair door frame",      Category = "Carpentry",  Priority = 2 });
        queue.AddJob(new JobCard { Title = "Burst pipe — urgent",    Category = "Plumbing",   Priority = 5 });
        queue.AddJob(new JobCard { Title = "Paint living room",      Category = "General",    Priority = 1 });

        Console.WriteLine($"=== {queue.HandymanName}'s Queue (priority order) ===\n");

        var iterator = queue.CreateIterator();
        while (iterator.HasNext())
            Console.WriteLine($"  Processing: {iterator.Next()}");

        Console.WriteLine("\n=== Same queue via IEnumerable<T> ===\n");

        var enumerable = new HandymanJobQueueEnumerable("James Nghipandua");
        enumerable.AddJob(new JobCard { Title = "Fix leaking tap",      Category = "Plumbing",   Priority = 1 });
        enumerable.AddJob(new JobCard { Title = "Replace circuit board", Category = "Electrical", Priority = 3 });
        enumerable.AddJob(new JobCard { Title = "Repair door frame",     Category = "Carpentry",  Priority = 2 });
        enumerable.AddJob(new JobCard { Title = "Burst pipe — urgent",   Category = "Plumbing",   Priority = 5 });

        foreach (var job in enumerable)
            Console.WriteLine($"  {job}");

        var urgentCount = enumerable.Count(j => j.Priority >= 3);
        Console.WriteLine($"\n  Urgent jobs (P3+): {urgentCount}");
    }
}

Output:

=== James Nghipandua's Queue (priority order) ===

  Processing: [P5] #a3f12b 'Burst pipe — urgent' [Plumbing] — Queued
  Processing: [P3] #d7e02b 'Replace circuit board' [Electrical] — Queued
  Processing: [P2] #c2f89d 'Repair door frame' [Carpentry] — Queued
  Processing: [P1] #e9b14c 'Fix leaking tap' [Plumbing] — Queued
  Processing: [P1] #f3a12c 'Paint living room' [General] — Queued

TypeScript Implementation

// ── Model/JobCard.ts ───────────────────────────────────────

const shortId = () => Math.random().toString(36).slice(2, 10);

export class JobCard {
  readonly jobId: string = shortId();
  status: string = "Queued";

  constructor(
    readonly title:    string,
    readonly category: string,
    readonly priority: number = 0
  ) {}

  toString(): string {
    return `[P${this.priority}] #${this.jobId} '${this.title}' [${this.category}] — ${this.status}`;
  }
}

// ── Iterator/PriorityJobIterator.ts ───────────────────────

import { JobCard } from "../Model/JobCard";

export class PriorityJobIterator {
  private sorted: JobCard[];
  private index = 0;

  constructor(jobs: JobCard[]) {
    this.sorted = [...jobs].sort((a, b) => b.priority - a.priority);
  }

  hasNext(): boolean { return this.index < this.sorted.length; }
  next():    JobCard { return this.sorted[this.index++]; }
  reset():   void    { this.index = 0; }
}

// ── Collection/HandymanJobQueue.ts ─────────────────────────

import { JobCard             } from "../Model/JobCard";
import { PriorityJobIterator } from "../Iterator/PriorityJobIterator";

export class HandymanJobQueue {
  private jobs: JobCard[] = [];

  constructor(readonly handymanName: string) {}

  addJob(job: JobCard): void { this.jobs.push(job); }
  get count(): number        { return this.jobs.length; }

  createIterator(): PriorityJobIterator {
    return new PriorityJobIterator(this.jobs);
  }

  // Native TypeScript iterator protocol
  [Symbol.iterator](): Iterator<JobCard> {
    const sorted = [...this.jobs].sort((a, b) => b.priority - a.priority);
    let index = 0;

    return {
      next(): IteratorResult<JobCard> {
        if (index < sorted.length)
          return { value: sorted[index++], done: false };
        return { value: undefined as any, done: true };
      }
    };
  }
}

// ── App.ts ─────────────────────────────────────────────────

import { JobCard          } from "./Model/JobCard";
import { HandymanJobQueue } from "./Collection/HandymanJobQueue";

const queue = new HandymanJobQueue("James Nghipandua");

queue.addJob(new JobCard("Fix leaking tap",       "Plumbing",   1));
queue.addJob(new JobCard("Replace circuit board",  "Electrical", 3));
queue.addJob(new JobCard("Repair door frame",      "Carpentry",  2));
queue.addJob(new JobCard("Burst pipe — urgent",    "Plumbing",   5));
queue.addJob(new JobCard("Paint living room",      "General",    1));

// Manual iterator
console.log(`=== ${queue.handymanName}'s Queue (manual iterator) ===\n`);
const iterator = queue.createIterator();
while (iterator.hasNext())
  console.log(`  Processing: ${iterator.next()}`);

// for...of — native iterator protocol
console.log(`\n=== Same queue via for...of ===\n`);
for (const job of queue)
  console.log(`  ${job}`);

// Spread and destructuring
const [first, second, ...rest] = queue;
console.log(`\n  First job : ${first}`);
console.log(`  Second job: ${second}`);
console.log(`  Remaining : ${rest.length} jobs`);

C# vs TypeScript — Key Differences

Aspect C# TypeScript
Language iterator protocol IEnumerable<T> / IEnumerator<T> Iterable<T> via [Symbol.iterator]()
Generator syntax yield return item function* generator or manual Iterator object
foreach / for...of foreach (var item in collection) for (const item of collection)
LINQ integration .Count(predicate), .Where(), .Select() Native array methods: .filter(), .map()
Spread support collection.ToArray() [...collection] spread works natively
Destructuring Limited const [first, ...rest] = collection full support

The Iterator Is Already Everywhere

You use the Iterator pattern every time you write any of the following:

Syntax Hidden iterator
foreach in C# IEnumerator<T>.MoveNext() + Current
for...of in TypeScript [Symbol.iterator]().next()
LINQ Where, Select IEnumerable<T> iterator composition
Array spread [...arr] [Symbol.iterator]()
yield return in C# Compiler-generated state machine
function* in JS/TS Generator iterator

When to Use the Iterator (Explicitly)

Since both languages provide iterators natively, you only need to implement the pattern explicitly when:

  • Your collection has a non-standard traversal order; priority, reverse, filtered, circular
  • You want to expose multiple traversal strategies for the same collection
  • You're building a custom data structure that needs to integrate with language iteration features
  • You need a stateful traversal; pause, resume, reset; that a simple for loop can't express

For standard forward traversal of standard collections, just use foreach / for...of.


Real-World Takeaway

Every database cursor is an Iterator, SQL SELECT results are streamed one row at a time via a cursor that advances on MoveNext(). In .NET, IAsyncEnumerable<T> is an async iterator, await foreach yields items from a stream as they arrive, which is how Entity Framework Core streams large query results. In Node.js, Readable streams implement the async iterator protocol. In Python, every for loop calls __iter__() and __next__().

In FixIt Pro, the HandymanJobQueue can change its internal structure from a List<T> to a priority queue to a sorted tree and every caller using foreach / for...of never knows. The traversal contract stays stable. The implementation can evolve freely.


Repo Structure for This Episode

github.com/antonlungameni/fixit-pro-design-patterns

fixit-pro-design-patterns/
├── csharp/Behavioral/15-Iterator/
│   ├── Model/JobCard.cs
│   ├── Iterator/IJobIterator.cs
│   ├── Iterator/PriorityJobIterator.cs
│   ├── Collection/HandymanJobQueue.cs
│   ├── Collection/HandymanJobQueueEnumerable.cs
│   └── Program.cs
└── typescript/Behavioral/15-Iterator/
    ├── Model/JobCard.ts
    ├── Iterator/PriorityJobIterator.ts
    ├── Collection/HandymanJobQueue.ts
    └── App.ts

Previous: #14 — Command Pattern
Next up: #16 — Mediator Pattern
A DispatchCenter that coordinates between Homeowners, Handymen, and the Scheduler; so none of them need to know about each other directly.