The blog post introduces behavioral design patterns in C++.

In the last two articles, we introduced some creational design patterns and structural design patterns, which are responsible for making object creation and relationship management more flexible and efficient. In this article, we will introduce the final category of design patterns: behavioral design patterns, which aim to coordinate the behaviors of objects effectively to accomplish tasks.
Observer Pattern
The observer pattern, as the name suggests, is a pattern that allows us to broadcast changes to all observers. For example, we can notify all subscribers on YouTube using the observer pattern as follows.
class Video {
public:
string title;
public:
Video(string title) : title(title) {};
};
class IObserver {
public:
virtual void displayNotification(string notification) = 0;
};
class Channel {
public:
string title;
vector<Video*> videos;
private:
vector<IObserver*> observers;
public:
void registerObserver(IObserver* observer) {
observers.push_back(observer);
};
void notifyObservers(string video_title) {
for (int i = 0; i < observers.size(); i++) {
observers[i] -> displayNotification("Check out the new video, "+video_title+", by "+title+"!");
}
};
void registerVideo(Video* video) {
videos.push_back(video);
notifyObservers(video->title);
};
Channel(string title) : title(title) {};
};
class Subscriber : public IObserver {
public:
string name;
public:
void displayNotification(string notification) {
cout << "Notification for " << name << ": " << notification << endl;
};
Subscriber(string name) : name(name) {};
};
int main() {
Channel tk("TKBlog");
Subscriber subscriber1("Subscriber 1");
Subscriber subscriber2("Subscriber 2");
tk.registerObserver(&subscriber1);
tk.registerObserver(&subscriber2);
Video newVideo("Hello World!");
tk.registerVideo(&newVideo);
// => Notification for Subscriber 1: Check out the new video, Hello World!, by TKBlog!
// Notification for Subscriber 2: Check out the new video, Hello World!, by TKBlog!
return 0;
}
The pattern makes use of a vector storing pointers to observers or subscribers so that
the Channel
object can automatically use the subscribers' methods to display notifications
when a new video is added to the channel.
Strategy Pattern
The strategy pattern is a pattern that utilizes different strategies with a shared interface to perform various operations using the same method. Below is an example of the strategy pattern, where we filter vectors using different strategies by passing them to a method that accepts different strategies as arguments.
class IFilterStrategy {
public:
virtual vector<int> filter(vector<int> input) = 0;
};
class RemoveNegativeStrategy : public IFilterStrategy {
public:
vector<int> filter(vector<int> input) {
vector<int> result;
for (int i = 0; i < input.size(); i++) {
if (input[i] >= 0) {
result.push_back(input[i]);
}
}
return result;
};
};
class RemoveEvenStrategy : public IFilterStrategy {
public:
vector<int> filter(vector<int> input) {
vector<int> result;
for (int i = 0; i < input.size(); i++) {
if (input[i] % 2 != 0) {
result.push_back(input[i]);
}
}
return result;
};
};
class Values {
public:
vector<int> values;
public:
void filter(IFilterStrategy* strategy) {
values = strategy -> filter(values);
}
void display() {
cout << "Values: ";
for (int i = 0; i < values.size(); i++) {
cout << values[i] << " ";
}
cout << endl;
};
Values(vector<int> values) : values(values) {};
};
int main() {
int arr[5] = {1, 2, -10, 5, -7};
vector<int> vec(arr, arr + sizeof(arr) / sizeof(arr[0]));
Values values(vec);
RemoveEvenStrategy removeEven;
RemoveNegativeStrategy removeNegative;
values.filter(&removeEven);
values.display(); // => Values: 1, 5, -7
values.filter(&removeNegative);
values.display(); // => Values: 1, 5
return 0;
};
Instead of implementing different filtering methods within the Values class,
we can create strategy classes using a strategy interface, which allows us to manage behaviors more easily.
For example, adding a new behavior only requires creating a new strategy by inheriting
from the abstract IFilterStrategy
class.
Dependency Injection
Dependency injection is a simple design pattern that makes use of the composition of an abstract class for objects with the same objective but different implementations, allowing them to share the same interface. A consistent interface among objects with the same objective enables a clear separation of concerns between the internal logic of objects and the objects that use them, resulting in more scalable and less repetitive code.
class IFileStorage {
public:
virtual void upload(string filename) = 0;
};
class AWSS3Storage : public IFileStorage {
public:
void upload(string filename) {
cout << "Uploading " << filename << " to AWS S3." << endl;
};
};
class GoogleCloudStorage : public IFileStorage {
public:
void upload(string filename) {
cout << "Uploading " << filename << " to Google Cloud." << endl;
};
};
class Uploader {
private:
IFileStorage* storage;
public:
void upload(string filename) {
storage -> upload(filename);
};
Uploader(IFileStorage* storage) : storage(storage) {};
};
int main() {
AWSS3Storage s3;
GoogleCloudStorage gc;
Uploader uploader1(&s3);
uploader1.upload("example.jpg"); // => Uploading example.jpg to AWS S3.
Uploader uploader2(&gc);
uploader2.upload("example.jpg"); // =? Uploading example.jpg to Google Cloud.
return 0;
};
The Uploader
class has a pointer to IFileStorage
as an attribute and uses the interface's method to reliably perform uploads,
regardless of which file storage implementation is used. Dependency injection is similar to the strategy pattern in that
it takes an interface as input; however, dependency injection accepts objects with the same objective as attributes,
whereas the strategy pattern accepts objects with different strategies as method arguments.
Conclusion
In this article, we discussed examples of behavioral design patterns: the observer pattern, the strategy pattern, and dependency injection. We can see that many of the design patterns we covered use interfaces and composition, suggesting that interfaces and composition tend to be more scalable and easier to manage than inheritance, as we previously discussed. However, it is important to emphasize that design patterns are not a one-size-fits-all solution that always works, and we need to carefully consider where and how to apply these design patterns.
Resources
- Code Aesthetic. 2024. Dependency Injection, The Best Pattern. YouTube.
- NeetCode. 2024. 8 Design Patterns EVERY Developer Should Know. YouTube.