This is the second part of a five-part series about SOLID as Rock design principle. The SOLID design principles, when combined together, make it easy for a programmer to craft software that is easy to maintain, reuse & extend. Open-Closed Principle(OCP) is the second principle in this series which I will discuss here with minimalistic example in Modern C++ along with its benefits & generic guideline.
/!\: Originally published @ www.vishalchovatiya.com.
By the way, If you haven't gone through my previous articles on design principles, then below is the quick links:
The code snippets you see throughout this series of articles are
simplified not sophisticated. So you often see me not using keywords
like override, final, public(while inheritance) just to make code compact & consumable(most of the time) in single standard screen size.
I also prefer struct instead of class just to save line by not writing "public:" sometimes and also miss virtual destructor, constructor, copy constructor, prefix std::, deleting dynamic memory, intentionally. I also consider myself a pragmatic person who wants to convey an idea in the simplest way
possible rather than the standard way or using Jargons.
Note:
classes should be open for extension, closed for modification
enum class COLOR { RED, GREEN, BLUE };
enum class SIZE { SMALL, MEDIUM, LARGE };
struct Product {
string m_name;
COLOR m_color;
SIZE m_size;
};
using Items = vector<Product*>;
#define ALL(C) begin(C), end(C)
struct ProductFilter {
static Items by_color(Items items, const COLOR e_color) {
Items result;
for (auto &i : items)
if (i->m_color == e_color)
result.push_back(i);
return result;
}
static Items by_size(Items items, const SIZE e_size) {
Items result;
for (auto &i : items)
if (i->m_size == e_size)
result.push_back(i);
return result;
}
static Items by_size_and_color(Items items, const SIZE e_size, const COLOR e_color) {
Items result;
for (auto &i : items)
if (i->m_size == e_size && i->m_color == e_color)
result.push_back(i);
return result;
}
};
int main() {
const Items all{
new Product{"Apple", COLOR::GREEN, SIZE::SMALL},
new Product{"Tree", COLOR::GREEN, SIZE::LARGE},
new Product{"House", COLOR::BLUE, SIZE::LARGE},
};
for (auto &p : ProductFilter::by_color(all, COLOR::GREEN))
cout << p->m_name << " is green\n";
for (auto &p : ProductFilter::by_size_and_color(all, SIZE::LARGE, COLOR::GREEN))
cout << p->m_name << " is green & large\n";
return EXIT_SUCCESS;
}
/*
Apple is green
Tree is green
Tree is green & large
*/
There is more than one way to achieve OCP. Here I am demonstrating
the popular one i.e. interface design or abstraction level. So here is our scalable solution:
Adding the level of abstraction for extensibility
template <typename T>
struct Specification {
virtual ~Specification() = default;
virtual bool is_satisfied(T *item) const = 0;
};
struct ColorSpecification : Specification<Product> {
COLOR e_color;
ColorSpecification(COLOR e_color) : e_color(e_color) {}
bool is_satisfied(Product *item) const { return item->m_color == e_color; }
};
struct SizeSpecification : Specification<Product> {
SIZE e_size;
SizeSpecification(SIZE e_size) : e_size(e_size) {}
bool is_satisfied(Product *item) const { return item->m_size == e_size; }
};
template <typename T>
struct Filter {
virtual vector<T *> filter(vector<T *> items, const Specification<T> &spec) = 0;
};
struct BetterFilter : Filter<Product> {
vector<Product *> filter(vector<Product *> items, const Specification<Product> &spec) {
vector<Product *> result;
for (auto &p : items)
if (spec.is_satisfied(p))
result.push_back(p);
return result;
}
};
// ------------------------------------------------------------------------------------------------
BetterFilter bf;
for (auto &x : bf.filter(all, ColorSpecification(COLOR::GREEN)))
cout << x->m_name << " is green\n";
For two or more combined specifications
template <typename T>
struct AndSpecification : Specification<T> {
const Specification<T> &first;
const Specification<T> &second;
AndSpecification(const Specification<T> &first, const Specification<T> &second)
: first(first), second(second) {}
bool is_satisfied(T *item) const {
return first.is_satisfied(item) && second.is_satisfied(item);
}
};
template <typename T>
AndSpecification<T> operator&&(const Specification<T> &first, const Specification<T> &second) {
return {first, second};
}
// -----------------------------------------------------------------------------------------------------
auto green_things = ColorSpecification{COLOR::GREEN};
auto large_things = SizeSpecification{SIZE::LARGE};
BetterFilter bf;
for (auto &x : bf.filter(all, green_things &&large_things))
cout << x->m_name << " is green and large\n";
// warning: the following will compile but will NOT work
// auto spec2 = SizeSpecification{SIZE::LARGE} &&
// ColorSpecification{COLOR::BLUE};
pure virtual method called
terminate called without an active exception
The terminal process terminated with exit code: 3
=> Extensibility
"When a single change to a program results in a cascade of changes to dependent modules, that program exhibits the undesirable attributes that we have come to associate with 'bad' design. The program becomes fragile, rigid, unpredictable and unreusable. The open-closed principle attacks this in a very straightforward way. It says that you should design modules that never change. When requirements change, you extend the behaviour of such modules by adding new code, not by changing old code that already works."
— Robert Martin
=> Maintainability
=> Flexibility
Keep in mind that classes can never be completely closed. There will always be unforeseen changes which require a class to be modified.
However, if changes can be foreseen, such as seen above i.e. filters, then you have a perfect opportunity to apply the OCP to be future-ready when those change requests come rolling in.
Have Any Suggestions, Query or Wants to Say Hi?
Take the Pressure Off, You Are Just a Click Away.🖱️
Previously published at http://www.vishalchovatiya.com/open-closed-principle-in-cpp-solid-as-a-rock/