An architecture or design is in compliance with the open closed principle (OCP) when the design supports the addition of new features with no code changes or without modifying the existing source code. As you know, based on your professional industry experience, every single project you have come across was extensible in one way or another. This is how you were able to add new features to your product. However, the design will be in compliance with the OCP when such a feature extension is done without you modifying the existing code.
Let's take a simple Item class, as shown in the following code. For simplicity, only the essential details are captured in the Item class:
#include <iostream>
#include <string>
using namespace std;
class Item {
private:
string name;
double quantity;
double pricePerUnit;
public:
Item ( string name, double pricePerUnit, double quantity ) {
this-name = name;
this->pricePerUnit = pricePerUnit;
this->quantity = quantity;
}
public double getPrice( ) {
return quantity * pricePerUnit;
}
public String getDescription( ) {
return name;
}
};
Assume the preceding Item class is part of a simple billing application for a small shop. As the Item class will be able to represent a pen, calculator, chocolate, notebook, and so on, it is generic enough to support any billable item that is dealt by the shop. However, if the shop owner is supposed to collect Goods and Services Tax (GST) or Value Added Tax (VAT), the existing Item class doesn't seem to support the tax component. One common approach is to modify the Item class to support the tax component. However, if we were to modify existing code, our design would be non-compliant to OCP.
Hence, let's refactor our design to make it OCP-compliant using Visitor design pattern. Let's explore the refactoring possibility, as shown in the following code:
#ifndef __VISITABLE_H
#define __VISITABLE_H
#include <string>
using namespace std;
class Visitor;
class Visitable {
public:
virtual void accept ( Visitor * ) = 0;
virtual double getPrice() = 0;
virtual string getDescription() = 0;
};
#endif
The Visitable class is an abstract class with three pure virtual functions. The Item class will be inheriting the Visitable abstract class, as shown here:
#ifndef __ITEM_H
#define __ITEM_H
#include <iostream>
#include <string>
using namespace std;
#include "Visitable.h"
#include "Visitor.h"
class Item : public Visitable {
private:
string name;
double quantity;
double unitPrice;
public:
Item ( string name, double quantity, double unitPrice );
string getDescription();
double getQuantity();
double getPrice();
void accept ( Visitor *pVisitor );
};
#endif
Next, let's take a look at the Visitor class, shown in the following code. It says there can be any number of Visitor subclasses that can be implemented in future to add new functionalities, all without modifying the Item class:
class Visitable;
#ifndef __VISITOR_H
#define __VISITOR_H
class Visitor {
protected:
double price;
public:
virtual void visit ( Visitable * ) = 0;
virtual double getPrice() = 0;
};
#endif
The GSTVisitor class is the one that lets us add the GST functionality without modifying the Item class. The GSTVisitor implementation looks like this:
#include "GSTVisitor.h"
void GSTVisitor::visit ( Visitable *pItem ) {
price = pItem->getPrice() + (0.18 * pItem->getPrice());
}
double GSTVisitor::getPrice() {
return price;
}
The Makefile looks as follows:
all: GSTVisitor.o Item.o main.o
g++ -o gst.exe GSTVisitor.o Item.o main.o
GSTVisitor.o: GSTVisitor.cpp Visitable.h Visitor.h
g++ -c GSTVisitor.cpp
Item.o: Item.cpp
g++ -c Item.cpp
main.o: main.cpp
g++ -c main.cpp
The refactored design is OCP-compliant, as we would be able to add new functionalities without modifying the Item class. Just imagine: if the GST calculation varies from time to time, without modifying the Item class, we would be able to add new subclasses of Visitor and address the upcoming changes.