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
forloop 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.