Behavioral patterns in software design focus on effective communication and the assignment of responsibilities among objects.
| Pattern | Description | Covered |
|---|---|---|
| Observer | Defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. | ✅ |
| Strategy | Defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy lets the algorithm vary independently from clients that use it. | ✅ |
| Command | Encapsulates a request as an object, thereby allowing for parameterization of clients with queues, requests, and operations. | ✅ |
| State | Allows an object to alter its behavior when its internal state changes. The object will appear to change its class. | ✅ |
| Chain of Responsibility | Passes the request along the chain of handlers. Upon receiving a request, each handler decides either to process the request or to pass it to the next handler in the chain. | ❌ |
| Interpreter | Provides a way to evaluate language grammar or expressions. The Interpreter pattern defines a grammar for the language, as well as an interpreter that uses the grammar to interpret sentences in the language. | ❌ |
| Memento | Captures and externalizes an object’s internal state so the object can be restored to this state later. | ❌ |
| Visitor | Represents an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates. | ❌ |
| Template Method | Defines the skeleton of an algorithm in the method, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing its structure. | ✅ |
The strategy pattern is a fundamental design pattern that is essential for understanding composition over inheritance. It is defined as:
quack are shared.fly method to Duck class leads to issues.fly and quack behaviors into different strategies.Different types of ducks inherit from Duck class.
Each subclass implements its own display method.
Promotes flexible code structure.
Allows behaviors to change dynamically.
Reduces dependency on inheritance.
Behaviors are not hard-coded in the Duck class.
They can vary independently from the duck type.
The Strategy Pattern is a powerful tool for creating flexible, maintainable code.
Encourages composition over inheritance.
Enables dynamic behavior assignment.
Push Model: Subject sends detailed data to observers.
Pull Model: Observers request data from the subject.
Origin: Discussed in “Head First Design Patterns” and “Design Patterns: Elements of Reusable Object-Oriented Software” by the Gang of Four.
Purpose: Encapsulates a request as an object.
Benefits:
Parameterization of objects with different requests.
Enabling queuing or logging of requests.
Supporting undoable operations.
Command Pattern Structure:
Command: An object encapsulating a request.
Invoker: Sends the command.
Receiver: The object receiving and executing the request.
Scenario: Controlling smart devices like lights, thermostats.
Application: Creating a smartphone app for device control.
Objective: Encapsulate each action (e.g., turning on a light) as a command.
Advantage: Commands can be passed and manipulated independently of the receiver.
Concept: Objects can be configured with commands to perform various actions.
Example: A remote control with buttons assigned to different light commands.
Queuing: Store and execute commands in sequence.
Logging: Keep a record of executed commands for auditing or replaying.
Implementation: Each command has an execute and undo method.
Use Case: Reversing a command, like turning off a light that was turned on.
public class LightOnCommand implements Command {
private Light light;
public LightOnCommand(Light light) {
this.light = light;
}
public void execute() {
light.turnOn();
}
public void
undo() {
light.turnOff();
}
}LightOnCommand encapsulates the action of turning on a light.public class RemoteControl {
private Command[] commands;
public RemoteControl() {
commands = new Command[4]; // Assuming 4 buttons
}
public void setCommand(int slot, Command command) {
commands[slot] = command;
}
public void buttonPressed(int slot) {
if (commands[slot] != null) {
commands[slot].execute();
}
}
}RemoteControl: Acts as an invoker that triggers commands.Concept: A command that contains multiple commands.
Use Case: Executing a batch of commands with a single action.
Implementation: Commands can be added to a queue and executed in order.
Application: Useful for scheduling and executing tasks sequentially.
Concept: Providing an undo method in each command to reverse its action.
Implementation: Storing the history of executed commands for undo operations.
public class RemoteControlWithUndo {
private Command[] commands;
private Stack<Command> history = new Stack<>();
public void setCommand(int slot, Command command) {
commands[slot] = command;
}
public void pressedButton(int slot) {
if (commands[slot] != null) {
commands[slot].execute();
history.push(commands[slot]);
}
}
public void pressedUndo() {
if (!history.isEmpty()) {
history.pop().undo();
}
}
}RemoteControlWithUndo keeps track of command history for undo operations.Flexibility: Commands can be added, removed, or modified independently.
Reusability: Commands can be used across different contexts and applications.
Extensibility: Easy to add new commands without changing existing code.
Application: Assigning commands to UI elements like buttons, menus.
Example: A toolbar with buttons executing different commands in an application.
Concept: Combining multiple commands into a single composite command.
Use Case: Complex operations that require executing several commands in a sequence.
Object-Oriented Programming: Encapsulating actions as objects.
Functional Programming: Treating functions as first-class citizens, similar to commands in OOP.
Command Pattern: Encapsulates requests as objects, offering flexibility and extensibility.
Key Components: Command, Invoker, Receiver.
Benefits:
Decoupling of command execution from its invocation.
Support for undo operations.
Enhanced control over operations, including queuing and logging.
Applications: Widely used in GUIs, transactional systems, and more.
Objective: Understand the State design pattern in object-oriented programming.
Context: Managing states and behaviors in software systems.
Application: Example of a turnstile system in a subway.
A state machine is a well-studied concept in computer science.
Deals with states and transitions.
Memoryless: Decisions based on current state, not history.
Simplifies State Management: Clear structure for managing states.
Reduces Complexity: Avoids tangled conditional logic.
Adaptability: Easy to modify and add new states.
Object Behaviors: Change based on its state.
No Direct Dependency: On the history of how the state was reached.
Scenario: Subway turnstile system.
Focus: Managing turnstile states using State Pattern.
Closed: Default state. Cannot pass through.
Open: Allows passage. Transitions to closed after entry.
pay_ok: From Closed to Open.
enter: From Open to Closed.
public interface State {
void handleRequest();
}
public class ClosedState implements State {
public void handleRequest() {
// Logic for Closed State
}
}
public class OpenState implements State {
public void handleRequest() {
// Logic for Open State
}
}
public class Turnstile {
private State state;
public Turnstile() {
state = new ClosedState();
}
public void setState(State state) {
this.state = state;
}
public void handleRequest() {
state.handleRequest();
}
}public class ClosedState implements State {
public void handleRequest() {
// Transition to Open State
System.out.println("Payment OK. Gate opening.");
}
}
public class OpenState implements State {
public void handleRequest() {
// Transition to Closed State
System.out.println("Gate closing after entry.");
}
}Scenario: Payment failure at a closed turnstile.
Transition: Remains in Closed state.
New State: Processing.
Purpose: Handle payment processing before opening.
| State | Action | Next State |
|---|---|---|
| Closed | pay | Processing |
| Processing | pay_ok | Open |
| Processing | pay_fail | Closed |
| Open | enter | Closed |
Key Concept: Transition based on current state and event.
No Memory: State does not depend on the history of events.
Clear State Management: Each state and transition is distinct.
Reduced Complexity: Simplifies complex conditional logic.
Easier Maintenance: Adding new states or transitions is straightforward.
Events: Actions triggering state transitions (e.g., pay, enter).
Handling: Each state defines responses to events.
Fundamental concepts in software engineering.
Solutions to common problems in software design.
They provide a template for how to solve a problem.
One of the behavioral design patterns.
Defines the skeleton of an algorithm in a method, deferring some steps to subclasses.
It lets one redefine certain steps of an algorithm without changing the algorithm’s structure.
Promotes code reuse.
Provides a clear structure for algorithms.
Facilitates flexibility and customization.
public class Football extends Game {
@Override
protected void initialize() {
System.out.println("Football Game Initialized.");
}
@Override
protected void startPlay() {
System.out.println("Football Game Started.");
}
@Override
protected void endPlay() {
System.out.println("Football Game Finished.");
}
}The abstract class defines a template method setting up the structure.
Concrete classes implement these steps without changing the structure.
Allows for customization within a fixed framework.
Simplifies code maintenance.
Promotes code reusability and scalability.
Enhances standardization of an algorithm.
“Don’t call us, we’ll call you.”
High-level components make decisions about when to call low-level components.
This principle is integral to the Template Method pattern.
Classes should be open for extension, but closed for modification.
The Template Method pattern adheres to this principle by allowing extension through subclassing.
Incorporates hooks and operations.
Hooks are optional steps in the algorithm, defined in the abstract class.
Concrete classes can override these hooks to add custom behavior.
@startuml
abstract class AbstractClass {
templateMethod(): void {
primitiveOperation1();
hook();
primitiveOperation2();
}
abstract primitiveOperation1()
abstract primitiveOperation2()
hook() { }
}
class ConcreteClass extends AbstractClass {
primitiveOperation1()
primitiveOperation2()
hook()
}
AbstractClass -> ConcreteClass : uses
@endumlpublic abstract class GameWithHooks {
// Template method with a hook
public final void play() {
initialize();
startPlay();
if (addNewGameFeature()) {
addFeature();
}
endPlay();
}
// Hook
protected boolean addNewGameFeature() {
return false;
}
// New feature
protected void addFeature() {}
// Other methods same as previous Game example
}Template Method often uses inheritance.
However, composition can be a more flexible alternative.
This involves defining the algorithm in a separate class and composing it in concrete classes.
Both design patterns are about defining algorithms.
Strategy Pattern allows changing the behavior dynamically.
Template Method defines a fixed algorithm structure, with specific steps being variable.
Subtypes must be substitutable for their base types.
Essential for the Template Method to ensure derived classes can replace base classes without affecting the algorithm.
Template Method is widely used in frameworks and libraries.
It provides a defined structure while allowing users to extend specific functionality.
Examples: Data parsing libraries, game development frameworks, UI rendering engines.
The Iterator Pattern is a fundamental design pattern in object-oriented programming. It provides a way to access the elements of a collection sequentially without revealing the underlying representation of the collection.
This pattern is essential for:
Providing a uniform way to traverse different data structures.
Decoupling the collection objects and the traversal logic.
Next, we’ll explore what collections are and the challenges faced without the Iterator Pattern.
A collection in programming is an object that groups multiple elements into a single unit. Collections are used to store, retrieve, manipulate, and communicate aggregate data.
Examples of collections:
A list of numbers.
A set of unique values.
A map of key-value pairs.
Understanding collections is key to realizing the importance of the Iterator Pattern.
Without the Iterator Pattern, traversing different types of collections can be challenging due to:
Diverse collection structures (arrays, trees, graphs).
The necessity to expose internal representation for iteration.
Increased complexity in client code due to direct traversal logic.
This lack of a unified traversal mechanism leads to less maintainable and more error-prone code.
The Iterator Pattern addresses these challenges by:
Providing a standard way to traverse through a collection.
Allowing the collection to manage the iteration logic internally.
Hiding the internal structure of the collection.
It simplifies client code and enhances maintainability.
This UML diagram illustrates the core components of the Iterator Pattern:
Iterator interface defines the iteration methods.
Aggregate interface provides a method to create an Iterator.
ConcreteIterator implements the Iterator interface for a specific collection.
ConcreteAggregate implements the Aggregate interface and holds the collection.
Here’s a simple implementation of an Iterator in Java:
public interface Iterator<T> {
boolean hasNext();
T next();
}
public class ConcreteIterator<T> implements Iterator<T> {
private T[] items;
private int index = 0;
public ConcreteIterator(T[] items) {
this.items = items;
}
@Override
public boolean hasNext() {
return index < items.length;
}
@Override
public T next() {
return items[index++];
}
}This code demonstrates a basic Iterator for an array of generic type T.
In game development, the Iterator Pattern can be used to manage collections like:
A list of enemies in a game world.
Inventory items in a player’s backpack.
It allows for efficient traversal and operation on these collections without exposing their internal structure.
This diagram represents how a GameWorld can create an EnemyIterator to iterate over its collection of enemies.
Implementing an Iterator for a game world’s enemy list:
public class EnemyIterator implements Iterator<Enemy> {
private GameWorld world;
private int index;
public EnemyIterator(GameWorld world) {
this.world = world;
this.index = 0;
}
@Override
public boolean hasNext() {
return index < world.getEnemies().size();
}
@Override
public Enemy next() {
return world.getEnemies().get(index++);
}
}This code snippet shows an Iterator tailored for iterating over enemies in a game world.
The Iterator Pattern adheres to the Single Responsibility Principle (SRP) by:
Separating the logic of iterating over a collection from the collection itself.
Allowing each class (iterator and collection) to manage its own responsibilities.
Advanced usage of iterators allows for more complex operations such as:
Filtering items while iterating.
Applying functions to each item in the collection.
Combining or chaining iterators for complex data structures.
These advanced techniques provide greater flexibility and power in handling collections.
@startuml
interface Iterator<T> {
+ hasNext(): Boolean
+ next(): T
}
class FilterIterator<T> implements Iterator<T> {
- wrappedIterator: Iterator<T>
- filterCondition: Predicate<T>
+ hasNext(): Boolean
+ next(): T
}
Iterator <|.. FilterIterator
@endumlThis diagram shows a FilterIterator that wraps around another iterator, providing additional filtering capabilities based on a given condition.
Implementing a FilterIterator in Java:
public class FilterIterator<T> implements Iterator<T> {
private Iterator<T> wrappedIterator;
private Predicate<T> filterCondition;
private T nextItem;
private boolean hasNextItem;
public FilterIterator(Iterator<T> iterator, Predicate<T> filter) {
this.wrappedIterator = iterator;
this.filterCondition = filter;
advance();
}
private void advance() {
hasNextItem = false;
while (wrappedIterator.hasNext()) {
T item = wrappedIterator.next();
if (filterCondition.test(item)) {
nextItem = item;
hasNextItem = true;
break;
}
}
}
@Override
public boolean hasNext() {
return hasNextItem;
}
@Override
public T next() {
if (!hasNextItem) {
throw new NoSuchElementException();
}
T item = nextItem;
advance();
return item;
}
}This code filters items in a collection based on a specified condition while iterating.
The Iterator Pattern complements functional programming concepts:
Iterators can be used in conjunction with streams and lambda expressions.
Enables operations like map, filter, and reduce on collections.
This integration brings a declarative approach to collection processing.
Comparing foreach loop and the Iterator Pattern:
foreach is syntactic sugar over iterators in many languages.
Iterators provide more control, e.g., removing elements during iteration.
Understanding this relationship helps in choosing the right iteration mechanism.
This diagram illustrates the integration of the Iterator Pattern in game logic. The GameLogic class uses an iterator to process enemies in the GameWorld.
Implementing game logic using the Iterator Pattern:
public class GameLogic {
private GameWorld world;
public GameLogic(GameWorld world) {
this.world = world;
}
public void processEnemies() {
Iterator<Enemy> iterator = world.getEnemyIterator();
while (iterator.hasNext()) {
Enemy enemy = iterator.next();
// Process each enemy
// e.g., enemy.takeDamage(10);
}
}
}This example shows how to iterate over and process game elements using an iterator.
The Iterator Pattern can also be adapted for infinite collections:
Useful for generating infinite sequences or streams.
Allows lazy evaluation: elements are generated only when needed.
This adaptation is powerful for certain types of data processing tasks.
This diagram represents an InfiniteIterator for an InfiniteCollection, illustrating how iteration can continue indefinitely.
Benefits of the Iterator Pattern:
Separates collection data and iteration logic.
Facilitates various types of traversals and operations.
Enhances code maintainability and readability.
Considerations:
Overhead of creating iterators.
Complexity in understanding and implementing advanced iterators.
The Iterator Pattern is a versatile tool in a developer’s toolkit, applicable in many scenarios with collections.