Writing code isn’t just about creating objects or structures — it’s about making those parts work together smoothly. Behavioral design patterns help you define clear communication and responsibility rules between your objects, so your system behaves reliably and flexibly, even as it grows.
- Observer: Defines a one-to-many dependency so that when one object changes state, its dependents are notified automatically.
- Strategy: Defines a family of algorithms, encapsulates each one, and makes them interchangeable.
- Command: Encapsulates a request as an object, allowing parameterization and queuing.
- Iterator: Provides a way to access elements of a collection sequentially without exposing its underlying representation.
- State: Allows an object to change its behavior when its internal state changes.
- Chain of Responsibility: Passes a request along a chain of handlers until one handles it.
- Mediator: Defines an object that encapsulates how a set of objects interact.
- Visitor: Separates an algorithm from the object structure it operates on.
- Template Method: Defines the skeleton of an algorithm in a base class but lets subclasses override specific steps.
- Memento: Captures and restores an object’s internal state without exposing its details.
- Interpreter: Provides a way to evaluate sentences in a language by defining a grammar representation and interpreter.
#Observer Pattern
The Observer pattern defines a one-to-many relationship between objects, so when one object (the subject) changes state, all its dependents (observers) are notified and updated automatically.
It’s like subscribing to a newsletter — when the publisher releases a new issue, all subscribers get notified without asking repeatedly.
1class Subject {
2 constructor() {
3 this.observers = [];
4 }
5
6 subscribe(observer) {
7 this.observers.push(observer);
8 }
9
10 unsubscribe(observer) {
11 this.observers = this.observers.filter(obs => obs !== observer);
12 }
13
14 notify(data) {
15 this.observers.forEach(observer => observer.update(data));
16 }
17}
18
19class Observer {
20 constructor(name) {
21 this.name = name;
22 }
23
24 update(data) {
25 console.log(`${this.name} received data:`, data);
26 }
27}
28
29// Usage
30const subject = new Subject();
31
32const observer1 = new Observer('Observer 1');
33const observer2 = new Observer('Observer 2');
34
35subject.subscribe(observer1);
36subject.subscribe(observer2);
37
38subject.notify('Hello Observers!');
39
40subject.unsubscribe(observer1);
41
42subject.notify('Second update');
43
Pros:
- Decouples subject and observers — subject doesn’t need to know details of observers.
- Supports dynamic addition and removal of observers.
- Facilitates event-driven and reactive programming.
Cons:
- Can lead to memory leaks if observers aren’t unsubscribed properly.
- Harder to debug when many observers react to the same event.
- Risk of unexpected side effects if observers modify shared state.
Real-World Usage Cases:
- UI event handling (clicks, input changes).
- Data binding frameworks (React state updates, Angular zones).
- Pub/Sub systems and messaging queues.
- Logging and monitoring systems.
When to Use:
- When multiple components need to respond to changes in another component.
- When you want loose coupling between event producers and consumers.
When to Avoid:
- When observer count is very high and performance is critical.
- When tight control over update order is required.
#Observer Pattern
The Observer pattern is like a subscription system where one object (called the subject) keeps a list of others (observers) who want to know when something changes.
When the subject updates or changes something important, it tells all the observers automatically.
Imagine you subscribe to a YouTube channel. When the channel uploads a new video, YouTube notifies you without you having to check repeatedly. That’s basically the Observer pattern in action!
This pattern helps keep your code organized and flexible because the subject doesn’t need to know the details about who is watching — it just sends out the news. Observers can also come and go anytime.
1class Subject {
2 constructor() {
3 this.observers = [];
4 }
5
6 subscribe(observer) {
7 this.observers.push(observer);
8 }
9
10 unsubscribe(observer) {
11 this.observers = this.observers.filter(obs => obs !== observer);
12 }
13
14 notify(data) {
15 this.observers.forEach(observer => observer.update(data));
16 }
17}
18
19class Observer {
20 constructor(name) {
21 this.name = name;
22 }
23
24 update(data) {
25 console.log(`${this.name} received data:`, data);
26 }
27}
28
29// Usage
30const subject = new Subject();
31
32const observer1 = new Observer('Observer 1');
33const observer2 = new Observer('Observer 2');
34
35subject.subscribe(observer1);
36subject.subscribe(observer2);
37
38subject.notify('Hello Observers!');
39
40subject.unsubscribe(observer1);
41
42subject.notify('Second update');
43
Pros:
- Helps keep components loosely connected, making your app easier to change and extend.
- You can add or remove observers anytime without changing the subject.
- Great for event-driven designs like UI events, data updates, or messaging systems.
Cons:
- If you forget to unsubscribe observers, it can cause memory leaks (unused objects hanging around).
- When many observers listen to the same subject, it can be hard to know the order updates happen.
- If observers change shared data unexpectedly, it can lead to bugs.
Real-World Usage Cases:
- User interfaces reacting to button clicks or input changes.
- Frameworks like React or Angular use similar ideas to update the screen when data changes.
- Systems that send notifications or log events whenever something happens.
- Chat apps where many users listen for new messages.
When to Use:
- When multiple parts of your program need to react to the same event or change.
- When you want to keep your components independent and flexible.
When to Avoid:
- When you have a huge number of observers and performance is critical.
- When you need strict control over the order in which observers are notified.
#Strategy Pattern
The Strategy pattern is all about choosing different ways to do something — like picking different strategies or algorithms at runtime.
Instead of hardcoding one way to solve a problem, you define a family of algorithms (strategies) and swap them out depending on the situation.
Imagine you’re paying for something online. You might want to pay by credit card, PayPal, or cryptocurrency. The Strategy pattern lets you switch between these payment methods easily without changing the main payment process.
This pattern helps keep your code clean and flexible, because you separate the what (payment) from the how (which payment method).
1class PaymentProcessor {
2 constructor(strategy) {
3 this.strategy = strategy;
4 }
5
6 setStrategy(strategy) {
7 this.strategy = strategy;
8 }
9
10 pay(amount) {
11 this.strategy.pay(amount);
12 }
13}
14
15class CreditCardPayment {
16 pay(amount) {
17 console.log(`Paying $${amount} using Credit Card.`);
18 }
19}
20
21class PayPalPayment {
22 pay(amount) {
23 console.log(`Paying $${amount} using PayPal.`);
24 }
25}
26
27class CryptoPayment {
28 pay(amount) {
29 console.log(`Paying $${amount} using Cryptocurrency.`);
30 }
31}
32
33// Usage
34const processor = new PaymentProcessor(new CreditCardPayment());
35processor.pay(100);
36
37processor.setStrategy(new PayPalPayment());
38processor.pay(150);
39
40processor.setStrategy(new CryptoPayment());
41processor.pay(200);
42
Pros:
- Makes it easy to add new strategies without changing existing code.
- Keeps your code clean by separating algorithms from the client using them.
- Lets you switch behavior at runtime depending on context.
Cons:
- Can increase the number of classes/objects in your codebase.
- Clients need to be aware of different strategies and when to use them.
Real-World Usage Cases:
- Payment processing with multiple payment methods.
- Sorting algorithms where you might switch between quicksort, mergesort, or heapsort.
- Compression utilities supporting different compression algorithms.
- UI behavior that changes based on user preferences or device capabilities.
When to Use:
- When you have multiple ways to perform a task and want to switch between them easily.
- When you want to avoid long conditional statements for choosing algorithms.
When to Avoid:
- When you only have one way to do something and don’t expect that to change.
- When managing many strategies becomes too complex for your application.
#Command Pattern
The Command pattern turns a request or action into a standalone object that contains all the information needed to perform that action later.
This means you can store, queue, or log commands, and execute them whenever you want.
Think of it like a remote control: you press buttons that send commands, and the device executes those commands — the remote just knows what command to send, not how it’s done.
This pattern helps decouple the object that invokes the action from the object that knows how to perform it.
1class Command {
2 execute() {}
3}
4
5class Light {
6 turnOn() {
7 console.log('Light is ON');
8 }
9 turnOff() {
10 console.log('Light is OFF');
11 }
12}
13
14class TurnOnCommand extends Command {
15 constructor(light) {
16 super();
17 this.light = light;
18 }
19 execute() {
20 this.light.turnOn();
21 }
22}
23
24class TurnOffCommand extends Command {
25 constructor(light) {
26 super();
27 this.light = light;
28 }
29 execute() {
30 this.light.turnOff();
31 }
32}
33
34class RemoteControl {
35 constructor() {
36 this.commands = [];
37 }
38
39 submit(command) {
40 this.commands.push(command);
41 command.execute();
42 }
43
44 undo() {
45 const command = this.commands.pop();
46 if (command) {
47 console.log('Undoing last command (not implemented here)');
48 }
49 }
50}
51
52// Usage
53const light = new Light();
54const turnOn = new TurnOnCommand(light);
55const turnOff = new TurnOffCommand(light);
56
57const remote = new RemoteControl();
58remote.submit(turnOn);
59remote.submit(turnOff);
60
Pros:
- Turns requests into objects, enabling flexible command management (queueing, undo, logging).
- Decouples the sender and receiver of a request.
- Makes adding new commands easy without changing existing code.
Cons:
- Can lead to many small command classes, increasing code size.
- Undo/redo functionality can be complex to implement.
Real-World Usage Cases:
- GUI buttons and menus (each action is a command).
- Job scheduling systems that queue and run tasks asynchronously.
- Transactional behavior with undo/redo in text editors or CAD tools.
- Macro recording and playback.
When to Use:
- When you need to parameterize methods with different requests.
- When you want to queue, log, or support undoable operations.
When to Avoid:
- When commands are simple and unlikely to need queuing or undo functionality.
#Chain of Responsibility Pattern
The Chain of Responsibility pattern lets you pass a request along a chain of objects until one of them handles it.
Each object in the chain decides either to handle the request or pass it to the next one.
Think of it like customer support: your request moves from a junior agent up to a manager or specialist until someone can help you.
This pattern helps reduce tight coupling between sender and receiver, and makes it easy to add or change handlers without changing the client code.
1class Handler {
2 setNext(handler) {
3 this.next = handler;
4 return handler;
5 }
6
7 handle(request) {
8 if (this.next) {
9 return this.next.handle(request);
10 }
11 return null;
12 }
13}
14
15class AuthenticationHandler extends Handler {
16 handle(request) {
17 if (!request.user) {
18 console.log('Authentication failed: No user');
19 return false;
20 }
21 console.log('Authentication passed');
22 return super.handle(request);
23 }
24}
25
26class AuthorizationHandler extends Handler {
27 handle(request) {
28 if (!request.user.isAdmin) {
29 console.log('Authorization failed: User is not admin');
30 return false;
31 }
32 console.log('Authorization passed');
33 return super.handle(request);
34 }
35}
36
37class LoggingHandler extends Handler {
38 handle(request) {
39 console.log('Logging request:', request);
40 return super.handle(request);
41 }
42}
43
44// Usage
45const auth = new AuthenticationHandler();
46const authorize = new AuthorizationHandler();
47const logger = new LoggingHandler();
48
49auth.setNext(authorize).setNext(logger);
50
51const request = { user: { name: 'Alice', isAdmin: true } };
52auth.handle(request);
53
Pros:
- Reduces coupling between sender and receiver.
- Adds flexibility by allowing handlers to be added or changed dynamically.
- Allows multiple handlers a chance to process a request.
Cons:
- Can make the flow of control harder to follow.
- If no handler processes the request, it may be ignored silently.
- Debugging chains can be tricky if chains are long or complex.
Real-World Usage Cases:
- Event handling in UI frameworks.
- Middleware chains in web servers like Express.js.
- Validation pipelines.
- Support ticket escalation systems.
When to Use:
- When multiple objects can handle a request but the handler isn’t known upfront.
- When you want to avoid coupling sender and receiver tightly.
- When you want to add or remove handlers dynamically.
When to Avoid:
- When requests must be handled by exactly one object with clear responsibility.
- When performance is critical and chain traversal adds overhead.
#State Pattern
The State pattern lets an object change its behavior based on its internal state without using lots of conditional statements.
It’s like a traffic light — the light changes from green to yellow to red, and each state defines how it behaves.
Instead of checking the current color everywhere, the traffic light simply delegates its behavior to the current state object.
This pattern helps keep your code cleaner and easier to maintain by encapsulating state-specific behavior into separate classes.
1class TrafficLight {
2 constructor() {
3 this.setState(new GreenState(this));
4 }
5
6 setState(state) {
7 this.state = state;
8 }
9
10 change() {
11 this.state.handle();
12 }
13}
14
15class GreenState {
16 constructor(light) {
17 this.light = light;
18 }
19
20 handle() {
21 console.log('Green: Go!');
22 this.light.setState(new YellowState(this.light));
23 }
24}
25
26class YellowState {
27 constructor(light) {
28 this.light = light;
29 }
30
31 handle() {
32 console.log('Yellow: Prepare to stop.');
33 this.light.setState(new RedState(this.light));
34 }
35}
36
37class RedState {
38 constructor(light) {
39 this.light = light;
40 }
41
42 handle() {
43 console.log('Red: Stop!');
44 this.light.setState(new GreenState(this.light));
45 }
46}
47
48// Usage
49const light = new TrafficLight();
50light.change(); // Green: Go!
51light.change(); // Yellow: Prepare to stop.
52light.change(); // Red: Stop!
53light.change(); // Green: Go!
54
Pros:
- Eliminates complex conditional logic based on state.
- Makes adding new states easier without modifying existing code.
- Keeps state-specific behavior encapsulated and organized.
Cons:
- Increases number of classes.
- Can make the code harder to understand at a glance if overused.
Real-World Usage Cases:
- UI components that behave differently based on user interaction (e.g., buttons that change appearance).
- Workflow engines with distinct stages or statuses.
- Network connection states (connected, disconnected, reconnecting).
- Media players with play, pause, stop states.
When to Use:
- When an object’s behavior depends on its state and it must change behavior at runtime.
- When you want to avoid large conditional statements scattered across your code.
When to Avoid:
- When the number of states is small and simple enough to manage with conditionals.
- When adding many classes complicates your project unnecessarily.
#Mediator Pattern
The Mediator pattern helps you reduce the complexity of communication between many objects by introducing a central mediator that controls how these objects interact.
Instead of objects referring to each other directly, they talk to the mediator, which handles the communication.
Think of it like an air traffic controller: pilots don’t talk to each other directly but coordinate all movements through the controller.
This pattern makes your system easier to maintain and change because the objects are less dependent on each other.
1class ChatRoom {
2 constructor() {
3 this.participants = {};
4 }
5
6 register(participant) {
7 this.participants[participant.name] = participant;
8 participant.chatRoom = this;
9 }
10
11 send(message, from, to) {
12 if (to) {
13 // Private message
14 to.receive(message, from);
15 } else {
16 // Broadcast message
17 Object.values(this.participants).forEach(participant => {
18 if (participant !== from) {
19 participant.receive(message, from);
20 }
21 });
22 }
23 }
24}
25
26class Participant {
27 constructor(name) {
28 this.name = name;
29 this.chatRoom = null;
30 }
31
32 send(message, to = null) {
33 this.chatRoom.send(message, this, to);
34 }
35
36 receive(message, from) {
37 console.log(`${this.name} received message from ${from.name}: "${message}"`);
38 }
39}
40
41// Usage
42const chatRoom = new ChatRoom();
43
44const alice = new Participant('Alice');
45const bob = new Participant('Bob');
46const charlie = new Participant('Charlie');
47
48chatRoom.register(alice);
49chatRoom.register(bob);
50chatRoom.register(charlie);
51
52alice.send('Hello everyone!');
53bob.send('Hi Alice!', alice);
54
Pros:
- Reduces direct dependencies between communicating objects.
- Centralizes complex communication logic in one place.
- Makes it easier to change communication rules without affecting participants.
Cons:
- The mediator can become complex and hard to maintain if overloaded.
- Adds an extra layer, which can sometimes impact performance.
Real-World Usage Cases:
- Chat applications and messaging systems.
- GUI frameworks where UI components interact via a mediator.
- Air traffic control systems.
- Event buses or central event dispatchers.
When to Use:
- When many objects need to communicate but you want to avoid tangled dependencies.
- When you want to centralize communication logic to make it easier to change or extend.
When to Avoid:
- When communication is simple and direct between few objects.
- When the mediator would become a “god object” managing too many responsibilities.
#Interpreter Pattern
The Interpreter pattern provides a way to evaluate sentences in a language — it defines a representation for a language’s grammar and a way to interpret sentences using that grammar.
Think of it like a calculator that can read and evaluate expressions like “5 + 3” or a command parser that understands specific commands.
This pattern is useful when you need to parse and evaluate expressions or instructions defined by a simple language.
1class Expression {
2 interpret() {}
3}
4
5class NumberExpression extends Expression {
6 constructor(number) {
7 super();
8 this.number = number;
9 }
10
11 interpret() {
12 return this.number;
13 }
14}
15
16class AddExpression extends Expression {
17 constructor(left, right) {
18 super();
19 this.left = left;
20 this.right = right;
21 }
22
23 interpret() {
24 return this.left.interpret() + this.right.interpret();
25 }
26}
27
28// Usage: representing "5 + 3"
29const five = new NumberExpression(5);
30const three = new NumberExpression(3);
31const expression = new AddExpression(five, three);
32
33console.log(expression.interpret()); // 8
34
Pros:
- Provides a flexible way to evaluate and extend simple languages or expressions.
- Makes grammar rules explicit and object-oriented.
- Easy to add new types of expressions by extending the base class.
Cons:
- Can become complex for very large or complicated grammars.
- Not ideal for performance-critical parsing — specialized parsers might be better.
Real-World Usage Cases:
- Parsing and evaluating simple arithmetic expressions.
- Interpreting custom query languages or filters.
- Command interpreters in games or apps.
- Rule engines that evaluate conditions dynamically.
When to Use:
- When your problem involves interpreting or parsing a language or expressions.
- When you want a clear and extensible object-oriented way to represent grammar rules.
When to Avoid:
- When dealing with complex languages requiring advanced parsing techniques.
- When performance is a major concern and optimized parsers are needed.
#Visitor Pattern
The Visitor pattern lets you add new operations to objects without changing their classes.
It separates an algorithm from the object structure it operates on, so you can define new behaviors without modifying the existing objects.
Think of it like having a group of different shapes, and you want to perform operations like calculating area or rendering — the Visitor lets you add these operations without changing the shapes themselves.
This pattern is useful when you have a complex object structure and want to keep operations separate and extendable.
1class Circle {
2 accept(visitor) {
3 visitor.visitCircle(this);
4 }
5 radius = 5;
6}
7
8class Rectangle {
9 accept(visitor) {
10 visitor.visitRectangle(this);
11 }
12 width = 10;
13 height = 20;
14}
15
16class AreaCalculator {
17 visitCircle(circle) {
18 const area = Math.PI * circle.radius ** 2;
19 console.log(`Circle area: ${area.toFixed(2)}`);
20 }
21 visitRectangle(rectangle) {
22 const area = rectangle.width * rectangle.height;
23 console.log(`Rectangle area: ${area}`);
24 }
25}
26
27// Usage
28const shapes = [new Circle(), new Rectangle()];
29const calculator = new AreaCalculator();
30
31shapes.forEach(shape => shape.accept(calculator));
32
Pros:
- Allows adding new operations without changing existing object classes.
- Keeps related operations organized in visitor classes.
- Makes it easy to add new behaviors.
Cons:
- Can make the object structure more complex because objects need to accept visitors.
- Adding new element classes (objects being visited) requires updating all visitors.
Real-World Usage Cases:
- Compilers and interpreters (e.g., walking ASTs).
- Document object models (applying formatting or exporting).
- Graphics and UI frameworks (performing multiple operations on elements).
- Serialization and deserialization logic.
When to Use:
- When you need to add new operations frequently but don’t want to change existing object classes.
- When you have a stable set of object structures but evolving behaviors.
When to Avoid:
- When your object structure changes often — visitors must be updated accordingly.
- When adding visitor complexity outweighs benefits.
#Template Method Pattern
The Template Method pattern defines the skeleton of an algorithm in a base class but lets subclasses override specific steps without changing the overall structure.
It’s like following a recipe where the main steps are fixed, but you can customize some ingredients or techniques.
This pattern helps you reuse common code and control the order of operations, while allowing flexibility where needed.
1class DataProcessor {
2 process() {
3 this.readData();
4 this.transformData();
5 this.saveData();
6 }
7
8 readData() {
9 throw new Error('readData() must be implemented');
10 }
11
12 transformData() {
13 throw new Error('transformData() must be implemented');
14 }
15
16 saveData() {
17 console.log('Saving data to database');
18 }
19}
20
21class CSVDataProcessor extends DataProcessor {
22 readData() {
23 console.log('Reading data from CSV file');
24 }
25
26 transformData() {
27 console.log('Transforming CSV data');
28 }
29}
30
31class JSONDataProcessor extends DataProcessor {
32 readData() {
33 console.log('Reading data from JSON file');
34 }
35
36 transformData() {
37 console.log('Transforming JSON data');
38 }
39}
40
41// Usage
42const csvProcessor = new CSVDataProcessor();
43csvProcessor.process();
44
45const jsonProcessor = new JSONDataProcessor();
46jsonProcessor.process();
47
Pros:
- Promotes code reuse by centralizing common algorithm steps.
- Controls algorithm flow while allowing customization of steps.
- Reduces code duplication in subclasses.
Cons:
- Subclasses are tightly coupled to the base class.
- Changes to the template method can affect all subclasses.
Real-World Usage Cases:
- Frameworks that define workflows but let users customize specific steps.
- Data processing pipelines where input, transformation, and output vary.
- UI rendering where the layout is fixed but components differ.
When to Use:
- When you have a fixed sequence of steps but want to allow subclasses to customize parts.
- When you want to avoid duplicating the overall algorithm in subclasses.
When to Avoid:
- When subclasses have little in common and share minimal behavior.
- When you need more flexibility than fixed algorithm steps allow.
#Memento Pattern
The Memento pattern lets you capture and save the internal state of an object without exposing its details, so you can restore it later if needed.
Think of it like the “undo” button in a text editor — it saves snapshots of your work so you can go back if you make a mistake.
This pattern helps you implement undo/redo functionality and keep your objects encapsulated without exposing sensitive data.
1class Editor {
2 constructor() {
3 this.content = '';
4 }
5
6 type(words) {
7 this.content += words;
8 }
9
10 save() {
11 return new Memento(this.content);
12 }
13
14 restore(memento) {
15 this.content = memento.getContent();
16 }
17
18 getContent() {
19 return this.content;
20 }
21}
22
23class Memento {
24 constructor(content) {
25 this.content = content;
26 }
27
28 getContent() {
29 return this.content;
30 }
31}
32
33class Caretaker {
34 constructor() {
35 this.history = [];
36 }
37
38 save(editor) {
39 this.history.push(editor.save());
40 }
41
42 undo(editor) {
43 if (this.history.length > 0) {
44 const memento = this.history.pop();
45 editor.restore(memento);
46 }
47 }
48}
49
50// Usage
51const editor = new Editor();
52const caretaker = new Caretaker();
53
54editor.type('Hello ');
55caretaker.save(editor);
56
57editor.type('World!');
58console.log(editor.getContent()); // Hello World!
59
60caretaker.undo(editor);
61console.log(editor.getContent()); // Hello
62
Pros:
- Allows saving and restoring object state without breaking encapsulation.
- Supports undo/redo functionality in applications.
- Keeps object internals hidden from other parts of the system.
Cons:
- Can increase memory usage if many states are saved.
- Managing state history can add complexity.
Real-World Usage Cases:
- Undo/redo features in text editors, graphic software, or IDEs.
- Saving snapshots in games (save/load states).
- Transaction rollback systems.
When to Use:
- When you need to save and restore object states safely.
- When implementing undo/redo or rollback functionality.
When to Avoid:
- When object state is large or complex and memory is limited.
- When simple state resets are sufficient.
#Iterator Pattern
The Iterator pattern provides a way to access elements of a collection sequentially without exposing its underlying structure.
It lets you loop through items in a container (like an array or tree) without worrying about how the collection is implemented.
Think of it like a bookmark in a book — it keeps track of where you are as you flip through the pages one by one.
This pattern helps you write clean and consistent code for traversing different kinds of collections.
1class Iterator {
2 constructor(items) {
3 this.items = items;
4 this.index = 0;
5 }
6
7 next() {
8 if (this.hasNext()) {
9 return this.items[this.index++];
10 }
11 return null;
12 }
13
14 hasNext() {
15 return this.index < this.items.length;
16 }
17}
18
19// Usage
20const collection = ['apple', 'banana', 'cherry'];
21const iterator = new Iterator(collection);
22
23while (iterator.hasNext()) {
24 console.log(iterator.next());
25}
26// Output:
27// apple
28// banana
29// cherry
30
Pros:
- Separates traversal logic from collection structure.
- Supports different traversal algorithms if needed.
- Provides a uniform way to access elements of various collections.
Cons:
- Adds extra classes or complexity for simple collections.
- Can be overkill when native language iteration is sufficient.
Real-World Usage Cases:
- Iterating over arrays, lists, trees, or graphs.
- Implementing custom data structures with their own traversal logic.
- Lazy loading or streaming large datasets.
When to Use:
- When you want to provide a standard way to traverse different collections.
- When you want to hide the internal structure of collections from clients.
When to Avoid:
- When simple loops suffice and collections are simple.
- When language built-in iteration is already powerful and clear.