{{announcement.body}}
{{announcement.title}}

Open Closed Principle: SOLID as a Rock

DZone 's Guide to

Open Closed Principle: SOLID as a Rock

Open-Closed Principle(OCP) is the second principle I will discuss with minimalistic examples in Modern C++ along with its benefits.

· Agile Zone ·
Free Resource

This is the second part of a five-part series about the SOLID as a Rock design principle. The SOLID design principles, when combined, make it easy for a programmer to craft software that is easy to maintain, reuse, and extend. Open-Closed Principle(OCP) is the second principle in this series which I will discuss here with minimalistic examples in Modern C++ along with its benefits and generic guideline.

By the way, If you haven't gone through my previous articles on design principles, then below is the quick links: 

  1.  SRP — Single Responsibility Principle
  2.  OCP — Open/Closed Principle
  3.  LSP — Liskov Substitution Principle
  4.  ISP — Interface Segregation Principle
  5.  DIP — Dependency Inversion Principle

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 and consumable(most of the time) in a 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 Jargon.

  • If you stumbled here directly, then I would suggest you go through What is a design pattern? First, even if it is trivial. I believe it will encourage you to explore more on this topic.
  • All of this code you encounter in this series of articles are compiled using C++20(though I have used Modern C++ features up to C++17 in most cases). So if you don't have access to the latest compiler you can use https://wandbox.org/ which has preinstalled boost library as well.

Intent

Classes should be open for extension, closed for modification

  • This means you should be able to extend a classes behavior, without modifying it. This might seems weird to you may raise a question that how can you change the behavior of a class without modifying it? 
  • But there are many answers to this in object-oriented design like dynamic polymorphism, static polymorphism, template, etc.  

Violating the Open-Closed Principle

Java
 




x
56


 
1
enum class COLOR { RED, GREEN, BLUE };
2
enum class SIZE { SMALL, MEDIUM, LARGE };
3
 
           
4
struct Product {
5
    string  m_name;
6
    COLOR   m_color;
7
    SIZE    m_size;
8
};
9
 
           
10
using Items = vector<Product*>;
11
#define ALL(C)  begin(C), end(C)
12
 
           
13
struct ProductFilter {
14
    static Items by_color(Items items, const COLOR e_color) {
15
        Items result;
16
        for (auto &i : items)
17
            if (i->m_color == e_color)
18
                result.push_back(i);
19
        return result;
20
    }
21
    static Items by_size(Items items, const SIZE e_size) {
22
        Items result;
23
        for (auto &i : items)
24
            if (i->m_size == e_size)
25
                result.push_back(i);
26
        return result;
27
    }
28
    static Items by_size_and_color(Items items, const SIZE e_size, const COLOR e_color) {
29
        Items result;
30
        for (auto &i : items)
31
            if (i->m_size == e_size &andi->m_color == e_color)
32
                result.push_back(i);
33
        return result;
34
    }
35
};
36
 
           
37
int main() {
38
    const Items all{
39
        new Product{"Apple", COLOR::GREEN, SIZE::SMALL},
40
        new Product{"Tree", COLOR::GREEN, SIZE::LARGE},
41
        new Product{"House", COLOR::BLUE, SIZE::LARGE},
42
    };
43
 
           
44
    for (auto &p : ProductFilter::by_color(all, COLOR::GREEN))
45
        cout << p->m_name << " is green\n";
46
 
           
47
    for (auto &p : ProductFilter::by_size_and_color(all, SIZE::LARGE, COLOR::GREEN))
48
        cout << p->m_name << " is green andlarge\n";
49
 
           
50
    return EXIT_SUCCESS;
51
}
52
/*
53
Apple is green
54
Tree is green
55
Tree is green andlarge
56
*/


  • So we have a bunch of products and we filtered it by some of its attributes. There is nothing wrong with the above code as far as the requirement is fixed(which will never be the case in software engineering). 
  • But just imagine the situations: You already shipped the code to the client. Later on, requirement changes and some new filters are required. In this case, you again need to modify the class and add new filter methods.
  • This is a problematic approach because we have 2 attributes(i.e. color and size) and need to implement 3 functions (i.e. color, size, and combination), one more attribute, and need to implement 8 functions. You see where this is going.
  • You need to go again and again in the existing implemented code and have to modify it which may break other parts of code as well. This is not a scalable solution. 
  • The open-closed principle states that your system should be open to extension but should be closed for modification. Unfortunately what we are doing here is modifying the existing code which is a violation of OCP.

Open Closed Principle Example

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

Java
 




xxxxxxxxxx
1
37


 
1
template <typename T>
2
struct Specification {
3
    virtual ~Specification() = default;
4
    virtual bool is_satisfied(T *item) const = 0;
5
};
6
 
           
7
struct ColorSpecification : Specification<Product> {
8
    COLOR e_color;
9
    ColorSpecification(COLOR e_color) : e_color(e_color) {}
10
    bool is_satisfied(Product *item) const { return item->m_color == e_color; }
11
};
12
 
           
13
struct SizeSpecification : Specification<Product> {
14
    SIZE e_size;
15
    SizeSpecification(SIZE e_size) : e_size(e_size) {}
16
    bool is_satisfied(Product *item) const { return item->m_size == e_size; }
17
};
18
 
           
19
template <typename T>
20
struct Filter {
21
    virtual vector<T *> filter(vector<T *> items, const Specification<T> &spec) = 0;
22
};
23
 
           
24
struct BetterFilter : Filter<Product> {
25
    vector<Product *> filter(vector<Product *> items, const Specification<Product> &spec) {
26
        vector<Product *> result;
27
        for (auto &p : items)
28
            if (spec.is_satisfied(p))
29
                result.push_back(p);
30
        return result;
31
    }
32
};
33
 
           
34
// ------------------------------------------------------------------------------------------------
35
BetterFilter bf;
36
for (auto &x : bf.filter(all, ColorSpecification(COLOR::GREEN)))
37
    cout << x->m_name << " is green\n";


  • As you can see we do not have to modify the filter method of BetterFilter. It can work with all kinds of now. 

For Two or More Combined Specifications

Java
 




xxxxxxxxxx
1
30


 
1
template <typename T>
2
struct AndSpecification : Specification<T> {
3
    const Specification<T> &first;
4
    const Specification<T> &second;
5
 
           
6
    AndSpecification(const Specification<T> &first, const Specification<T> &second)
7
    : first(first), second(second) {}
8
 
           
9
    bool is_satisfied(T *item) const { 
10
        return first.is_satisfied(item) &andsecond.is_satisfied(item); 
11
    }
12
};
13
 
           
14
template <typename T>
15
AndSpecification<T> operator&&(const Specification<T> &first, const Specification<T> &second) {
16
    return {first, second};
17
}
18
 
           
19
// -----------------------------------------------------------------------------------------------------
20
 
           
21
auto green_things = ColorSpecification{COLOR::GREEN};
22
auto large_things = SizeSpecification{SIZE::LARGE};
23
 
           
24
BetterFilter bf;
25
for (auto &x : bf.filter(all, green_things &&large_things))
26
    cout << x->m_name << " is green and large\n";
27
 
           
28
// warning: the following will compile but will NOT work
29
// auto spec2 = SizeSpecification{SIZE::LARGE} &&
30
//              ColorSpecification{COLOR::BLUE};


  • SizeSpecification{SIZE::LARGE
    &andColorSpecification{COLOR::BLUE}will not work. Experienced C++ eyes can easily recognize the reason. Though temporary object creation is a hint here.  If you do so, you may get the error of pure virtual function as follows:
Plain Text
 




xxxxxxxxxx
1


 
1
pure virtual method called
2
terminate called without an active exception
3
The terminal process terminated with exit code: 3


  • For more than two specifications, you can use a variadic template.

Benefits of the Open-Closed Principle

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 behavior of such modules by adding new code, not by changing old code that already works."
Robert Martin 

Maintainability

  • The main benefit of this approach is that an interface introduces an additional level of abstraction which enables loose coupling. The implementations of an interface are independent of each other and don’t need to share any code. 
  • Thus, you can easily cope-up with client's keep changing requirements. Very useful in agile methodologies.

Flexibility

  • The open-closed principle also applies to plugin and middleware architecture. In that case, your base software entity is your application core functionality.
  • In the case of plugins, you have a base or core module that can be plugged with new features and functionality through a common gateway interface. A good example of this is web browser extensions. 
  • Binary compatibility will also be in-tact in subsequent releases.

The Yardstick to Craft Open Closed Principle Friendly Software

  • In the SRP, you make a judgment about decomposition and where to draw encapsulation boundaries in your code. In the OCP, you make a judgment about what in your module you will make abstract and leave to your module’s consumers to make concrete, and what concrete functionality to provide yourself. 
  • Many design patterns help us to extend code without changing it. For instance, the Decorator pattern helps us to follow the Open Close principle. Also, the Factory Method, Strategy pattern, or the Observer pattern might be used to design an application easy to change with minimum changes in the existing code. 

Conclusion

Keep in mind that classes can never be completely closed. There will always be unforeseen changes that 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.

Topics:
coding basics ,cpp ,design pattens ,programming

Published at DZone with permission of Vishal Chovatiya . See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}