Behavioral Design Patterns
Behavioral design patterns focus on the interaction between objects, defining how they communicate and collaborate. These patterns promote loose coupling and help manage complex flows of control and data. In this article, we will explore seven key behavioral design patterns: Observer, Strategy, Command, Memento, Mediator, State, and Template Method.
This is a continuation of Introduction to Design Patterns, if you haven’t checked it out you can check it here.
1. Observer Pattern
The Observer pattern establishes a one-to-many dependency between objects. When one object (the subject) changes its state, all dependent objects (observers) are notified and updated automatically. This pattern is particularly useful for implementing distributed event-handling systems.
Implementation
- Subject: Maintains a list of observers and notifies them of state changes.
- Observer: Interface that defines the update method.
- Concrete Subject: Implements the subject interface and holds the state.
- Concrete Observer: Implements the observer interface and updates itself when notified.
Use Case
This pattern is commonly used in scenarios where a state change in one object requires updates in multiple other objects, such as in GUI frameworks, event handling systems, or stock price updates.
Code Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
using System;
using System.Collections.Generic;
// Subject interface
public interface ISubject
{
void Attach(IObserver observer);
void Detach(IObserver observer);
void Notify();
}
// Concrete Subject
public class Stock : ISubject
{
private List<IObserver> observers = new List<IObserver>();
private string _stockSymbol;
private double _price;
public Stock(string stockSymbol, double price)
{
_stockSymbol = stockSymbol;
_price = price;
}
public double Price
{
get { return _price; }
set
{
_price = value;
Notify();
}
}
public void Attach(IObserver observer)
{
observers.Add(observer);
}
public void Detach(IObserver observer)
{
observers.Remove(observer);
}
public void Notify()
{
foreach (var observer in observers)
{
observer.Update(_stockSymbol, _price);
}
}
}
// Observer interface
public interface IObserver
{
void Update(string stockSymbol, double price);
}
// Concrete Observer
public class StockInvestor : IObserver
{
private string _name;
public StockInvestor(string name)
{
_name = name;
}
public void Update(string stockSymbol, double price)
{
Console.WriteLine($"Investor {_name} notified: {stockSymbol} price changed to {price}");
}
}
// Client Code
public class Program
{
public static void Main(string[] args)
{
Stock stock = new Stock("AAPL", 120.0);
StockInvestor investor1 = new StockInvestor("John");
StockInvestor investor2 = new StockInvestor("Sarah");
stock.Attach(investor1);
stock.Attach(investor2);
stock.Price = 125.0;
stock.Price = 130.0;
}
}
Code Explanation
- The
Stock
class implements theISubject
interface and notifies its observers of price changes. - The
StockInvestor
class implements theIObserver
interface and receives updates when the stock price changes.
Summary of the Flow
- The
ISubject
interface allows for attaching and detaching observers. - The
Stock
class notifies all attachedIObserver
instances when its price changes. StockInvestor
instances receive updates whenever the stock price changes.
Output
1
2
3
4
Investor John notified: AAPL price changed to 125
Investor Sarah notified: AAPL price changed to 125
Investor John notified: AAPL price changed to 130
Investor Sarah notified: AAPL price changed to 130
Benefits
- Promotes loose coupling between the subject and observers.
- Supports dynamic and flexible systems that can easily add or remove observers.
- Facilitates event-driven programming.
2. Strategy Pattern
The Strategy pattern defines a family of algorithms, encapsulating each one, and making them interchangeable. This pattern enables clients to choose the algorithm that best fits their needs at runtime.
Implementation
- Strategy Interface: Defines a common interface for all supported algorithms.
- Concrete Strategies: Implement the strategy interface with specific algorithms.
- Context: Uses a Strategy instance to call the algorithm defined by the Strategy interface.
Use Case
This pattern is often used in sorting, validation, or any scenario where multiple algorithms can achieve the same goal, allowing for easy switching and customization.
Code Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
using System;
// Strategy interface
public interface ICompressionStrategy
{
void Compress(string fileName);
}
// Concrete Strategy A
public class ZipCompression : ICompressionStrategy
{
public void Compress(string fileName)
{
Console.WriteLine($"Compressing {fileName} using Zip.");
}
}
// Concrete Strategy B
public class RarCompression : ICompressionStrategy
{
public void Compress(string fileName)
{
Console.WriteLine($"Compressing {fileName} using Rar.");
}
}
// Context
public class FileCompressor
{
private ICompressionStrategy _compressionStrategy;
public FileCompressor(ICompressionStrategy compressionStrategy)
{
_compressionStrategy = compressionStrategy;
}
public void SetStrategy(ICompressionStrategy compressionStrategy)
{
_compressionStrategy = compressionStrategy;
}
public void CompressFile(string fileName)
{
_compressionStrategy.Compress(fileName);
}
}
// Client Code
public class Program
{
public static void Main(string[] args)
{
FileCompressor fileCompressor = new FileCompressor(new ZipCompression());
fileCompressor.CompressFile("example.txt");
fileCompressor.SetStrategy(new RarCompression());
fileCompressor.CompressFile("example.txt");
}
}
Code Explanation
- The
FileCompressor
class uses anICompressionStrategy
to perform file compression. - Strategies can be switched at runtime by calling
SetStrategy
.
Summary of the Flow
- The
ICompressionStrategy
interface allows for different compression algorithms. - The
FileCompressor
class delegates the compression task to the current strategy. - The client can easily switch strategies as needed.
Output
1
2
Compressing example.txt using Zip.
Compressing example.txt using Rar.
Benefits
- Enables selecting algorithms at runtime, promoting flexibility.
- Encapsulates algorithms separately, adhering to the Single Responsibility Principle.
- Simplifies unit testing by allowing the use of mock strategies.
3. Command Pattern
The Command pattern encapsulates a request as an object, thereby allowing for parameterization of clients with queues, requests, and operations. This pattern promotes decoupling between sender and receiver.
Implementation
- Command Interface: Declares an execution method.
- Concrete Command: Implements the command interface and defines the binding between a receiver and an action.
- Invoker: Holds commands and calls their execution methods.
- Receiver: Knows how to perform the operations associated with the command.
Use Case
This pattern is useful in scenarios where you need to parameterize objects with operations, queue operations, or support undo functionality.
Code Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
using System;
// Command interface
public interface ICommand
{
void Execute();
}
// Receiver
public class Light
{
public void TurnOn()
{
Console.WriteLine("Light is ON");
}
public void TurnOff()
{
Console.WriteLine("Light is OFF");
}
}
// Concrete Command for turning on the light
public class TurnOnCommand : ICommand
{
private Light _light;
public TurnOnCommand(Light light)
{
_light = light;
}
public void Execute()
{
_light.TurnOn();
}
}
// Concrete Command for turning off the light
public class TurnOffCommand : ICommand
{
private Light _light;
public TurnOffCommand(Light light)
{
_light = light;
}
public void Execute()
{
_light.TurnOff();
}
}
// Invoker
public class RemoteControl
{
private ICommand _command;
public void SetCommand(ICommand command)
{
_command = command;
}
public void PressButton()
{
_command.Execute();
}
}
// Client Code
public class Program
{
public static void Main(string[] args)
{
Light light = new Light();
ICommand turnOn = new TurnOnCommand(light);
ICommand turnOff = new TurnOffCommand(light);
RemoteControl remote = new RemoteControl();
remote.SetCommand(turnOn);
remote.PressButton();
remote.SetCommand(turnOff);
remote.PressButton();
}
}
Code Explanation
- The
Light
class is the Receiver that performs actions. - The
TurnOnCommand
andTurnOffCommand
encapsulate commands. - The
RemoteControl
class is the Invoker that calls the commands.
Summary of the Flow
- The
ICommand
interface defines anExecute
method for command execution. - The
RemoteControl
sets a command and triggers its execution. - Commands are decoupled from the invoker, promoting flexibility.
Output
1
2
Light is ON
Light is OFF
Benefits
- Decouples the sender from the receiver, allowing for easy command management.
- Enables queueing and logging of commands for undo functionality.
- Supports parameterization of clients with various requests.
4. Memento Pattern
The Memento pattern allows for capturing and externalizing an object’s internal state without violating encapsulation, enabling the object to be restored to this state later.
Implementation
- Memento: Stores the internal state of the originator.
- Originator: Creates a memento containing a snapshot of its current state.
- Caretaker: Maintains the memento, preventing direct access to its content.
Use Case
This pattern is often used in applications requiring undo mechanisms, such as text editors or gaming applications
where game states need to be saved and restored.
Code Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
using System;
public class Memento
{
public string State { get; }
public Memento(string state)
{
State = state;
}
}
// Originator
public class TextEditor
{
private string _text = string.Empty;
public void Write(string text)
{
_text = text;
Console.WriteLine($"Current text: {_text}");
}
public Memento Save()
{
return new Memento(_text);
}
public void Restore(Memento memento)
{
_text = memento.State;
Console.WriteLine($"Restored text: {_text}");
}
}
// Caretaker
public class Caretaker
{
private Memento? _memento;
public void Save(TextEditor editor)
{
_memento = editor.Save();
}
public void Restore(TextEditor editor)
{
if (editor == null) throw new ArgumentNullException(nameof(editor));
if (_memento == null) throw new InvalidOperationException("No saved state to restore.");
editor.Restore(_memento);
}
}
// Client Code
public class Program
{
public static void Main(string[] args)
{
TextEditor editor = new TextEditor();
Caretaker caretaker = new Caretaker();
editor.Write("Hello, World!");
caretaker.Save(editor);
editor.Write("Hello, Design Patterns!");
caretaker.Restore(editor);
}
}
Code Explanation
- The
TextEditor
class acts as the Originator that holds the state. - The
Memento
class captures the state. - The
Caretaker
manages saving and restoring states.
Summary of the Flow
- The
TextEditor
creates aMemento
capturing its current state. - The
Caretaker
saves the memento and can restore it later. - State restoration allows the
TextEditor
to revert to previous content.
Output
1
2
3
Current text: Hello, World!
Current text: Hello, Design Patterns!
Restored text: Hello, World!
Benefits
- Encapsulates the state of an object, promoting encapsulation.
- Supports undo operations without exposing internal data structures.
- Simplifies state management in complex systems.
5. Mediator Pattern
The Mediator pattern defines an object that encapsulates how a set of objects interact. This pattern promotes loose coupling by preventing objects from referring to each other explicitly, allowing for more flexible communication.
Implementation
- Mediator: Interface that defines communication methods.
- Concrete Mediator: Implements the mediator interface and coordinates communication between colleagues.
- Colleagues: Classes that communicate through the mediator.
Use Case
This pattern is useful in systems where numerous objects interact in complex ways, such as chat applications or UI components in a window.
Code Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
using System;
// Mediator interface
public interface IMediator
{
void Notify(object sender, string ev);
}
// Concrete Mediator
public class ChatMediator : IMediator
{
private User? _user1;
private User? _user2;
public User User1
{
set { _user1 = value; }
}
public User User2
{
set { _user2 = value; }
}
public void Notify(object sender, string ev)
{
if (sender == _user1 && _user2 != null)
{
Console.WriteLine($"User1 sends: {ev}");
_user2.Receive(ev);
}
else if (sender == _user2 && _user1 != null)
{
Console.WriteLine($"User2 sends: {ev}");
_user1.Receive(ev);
}
}
}
// Colleague
public class User
{
private IMediator _mediator;
public string Name { get; }
public User(string name, IMediator mediator)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
}
public void Send(string message)
{
_mediator.Notify(this, message);
}
public void Receive(string message)
{
Console.WriteLine($"{Name} received: {message}");
}
}
// Client Code
public class Program
{
public static void Main(string[] args)
{
ChatMediator mediator = new ChatMediator();
User user1 = new User("Alice", mediator);
User user2 = new User("Bob", mediator);
mediator.User1 = user1;
mediator.User2 = user2;
user1.Send("Hello, Bob!");
user2.Send("Hello, Alice!");
}
}
Code Explanation
- The
ChatMediator
class manages communication betweenUser
instances. - The
User
class sends and receives messages through the mediator.
Summary of the Flow
User
instances communicate via theIMediator
.- The
ChatMediator
orchestrates message passing between users. - This promotes loose coupling and easier modifications.
Output
1
2
3
4
User1 sends: Hello, Bob!
Bob received: Hello, Bob!
User2 sends: Hello, Alice!
Alice received: Hello, Alice!
Benefits
- Reduces dependencies between objects, promoting flexibility.
- Simplifies communication logic and enhances maintainability.
- Centralizes control logic in one location.
6. State Pattern
The State pattern allows an object to alter its behavior when its internal state changes. This pattern encapsulates state-specific behavior and transitions, making state management cleaner and easier.
Implementation
- State Interface: Defines the methods for various states.
- Concrete States: Implement the state interface and define specific behavior.
- Context: Maintains an instance of a concrete state and delegates state-specific behavior to that instance.
Use Case
This pattern is commonly used in scenarios like UI controls that change behavior based on user interactions or game characters that exhibit different behaviors based on their current state.
Code Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
using System;
// State interface
public interface IState
{
void Handle(Context context);
}
// Concrete State A
public class HappyState : IState
{
public void Handle(Context context)
{
Console.WriteLine("In a happy state!");
context.SetState(new SadState());
}
}
// Concrete State B
public class SadState : IState
{
public void Handle(Context context)
{
Console.WriteLine("In a sad state!");
context.SetState(new HappyState());
}
}
// Context
public class Context
{
public required IState State { get; set; }
public void SetState(IState state)
{
State = state ?? throw new ArgumentNullException(nameof(state));
}
public void Request()
{
State.Handle(this);
}
}
// Client Code
public class Program
{
public static void Main(string[] args)
{
Context context = new Context { State = new HappyState() };
context.SetState(new HappyState());
context.Request();
context.Request();
}
}
Code Explanation
- The
Context
class holds a reference to the current state and delegates behavior to it. HappyState
andSadState
implement the state interface, providing specific behavior.
Summary of the Flow
- The
Context
changes its behavior based on the current state. - States can transition between each other, allowing for flexible behavior management.
- Each state defines specific actions for the context.
Output
1
2
In a happy state!
In a sad state!
Benefits
- Simplifies state management by encapsulating state-specific behavior.
- Reduces conditional logic in code, promoting cleaner implementations.
- Supports the addition of new states without modifying existing code.
7. Template Method Pattern
The Template Method pattern defines the skeleton of an algorithm in a method, deferring some steps to subclasses. This pattern promotes code reuse and consistency by allowing subclasses to redefine certain steps of an algorithm without changing its structure.
Implementation
- Abstract Class: Defines the template method and the steps of the algorithm.
- Concrete Classes: Implement specific steps of the algorithm.
Use Case
This pattern is useful in frameworks where specific steps of an algorithm can vary, such as data processing or rendering operations.
Code Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
using System;
// Abstract Class
public abstract class DataProcessor
{
public void Process()
{
ReadData();
ProcessData();
WriteData();
}
protected abstract void ReadData();
protected abstract void ProcessData();
protected abstract void WriteData();
}
// Concrete Class for XML
public class XmlDataProcessor : DataProcessor
{
protected override void ReadData()
{
Console.WriteLine("Reading XML data...");
}
protected override void ProcessData()
{
Console.WriteLine("Processing XML data...");
}
protected override void WriteData()
{
Console.WriteLine("Writing XML data...");
}
}
// Concrete Class for JSON
public class JsonDataProcessor : DataProcessor
{
protected override void ReadData()
{
Console.WriteLine("Reading JSON data...");
}
protected override void ProcessData()
{
Console.WriteLine("Processing JSON data...");
}
protected override void WriteData()
{
Console.WriteLine("Writing JSON data...");
}
}
// Client Code
public class Program
{
public static void Main(string[] args)
{
DataProcessor xmlProcessor = new XmlDataProcessor();
xmlProcessor.Process();
DataProcessor jsonProcessor = new JsonDataProcessor();
jsonProcessor.Process();
}
}
Code Explanation
- The
DataProcessor
abstract class defines the template methodProcess
, which outlines the algorithm steps. - Concrete classes implement the specific steps for different data formats.
Summary of the Flow
- The
DataProcessor
class controls the algorithm structure. - Subclasses implement specific behaviors for reading, processing, and writing data.
- The template method ensures consistency across different implementations.
Output
1
2
3
4
5
6
Reading XML data...
Processing XML data...
Writing XML data...
Reading JSON data...
Processing JSON data...
Writing JSON data...
Benefits
- Promotes code reuse by encapsulating common algorithm structures.
- Allows customization of specific steps without altering the overall algorithm.
- Enhances maintainability by providing a clear structure.
Conclusion
Behavioral design patterns play a crucial role in defining how objects communicate and collaborate in a system. By implementing these patterns, developers can create flexible, maintainable, and extensible code. Understanding and applying these patterns can lead to more robust software design, enabling easier updates and modifications as requirements evolve. Whether managing state, handling commands, or coordinating interactions, behavioral patterns provide the tools necessary for effective object-oriented design.
You can get the source code of the examples used on the github here