Open Closed Principle (OCP)

The open/closed principle states that software objects (classes, methods, functions, etc.) should be open for extension but closed for modification.

The ideal implementation of this principle is the interface. Nothing superfluous, nothing to modify, only expandable.

class IMyInterface {
public:
	virtual void execute() = 0;
};

Based on this, I will give an example that violates the principle under consideration. The example is taken from a fairly popular Russian-language site, although it is written in C#, I think it will be understandable to other programmers.

interface IRepository<T> : IDisposable 
        where T : class
{
    IEnumerable<T> GetBookList(); // получение всех объектов
    T GetBook(int id); // получение одного объекта по id
    void Create(T item); // создание объекта
    void Update(T item); // обновление объекта
    void Delete(int id); // удаление объекта по id
    void Save();  // сохранение изменений
}

Here are several violations limiting the flexibility of the code. Let's start with the first method, which returns all objects from the conditional storage. I want to ask the question, why do we need all the objects at once, isn't it easier to make a selection right away. If we add a predicate as a parameter, we will violate the principle of openness-closure for this interface. Therefore, it is better to separate the method into a separate interface.

The next method, getting one object, also limits flexibility. Again the question, what if it is necessary to get an object not by ID, but by another attribute. Such a method is not needed at all, because if you create a tool for getting elements by condition, getting one element is a special case.

The last four methods remain. It is obvious that the author of the example tried to apply the concept of CRUD – an acronym denoting the four basic functions used when working with databases: create, read, update, delete.i. But even the definition says that this is only an abbreviation, not requiring strict compliance. Therefore, it is not advisable to include all these methods in one class.

It is worth making a digression here. A sophisticated reader may point out that implementing CRUD in a single class is the ActiveRecord pattern.ii. But the template itself is quite controversial. The reader should read the definition, materials in the notes, and decide for himself whether to use it in development.

By the way, the author of the above example, when implementing it further, had to refuse to use the save method. Leaving it empty. After all, all interface methods are subject to mandatory implementation. Having violated the SOLID principles, even in a small example, an artifact was created.

Now, let's create our own implementation example that meets the concept of code flexibility and readability.

Let's create functionality for serializing an array of objects with data in JSON format, with the ability to expand for recording in other formats.

In order to comply with the open-closed principle, it is necessary to separate entities in such a way that when making changes there is no need to modify already existing classes.

The first step is to separate the data and the actions on it, i.e. the data serialization method must be separated from the data itself. The next step is to separate the tools for serializing one element from the list. The result is three entities, data, a serializer for one element, and a serializer for an array of data.

Class diagram

Class diagram

According to the task condition, we should be able to expand the functionality without violating the described principle. The Product class can be expanded by adding fields in child classes. We can create a tool for converting an element to a string for each child class with data, not excluding the base one. In addition, we can create new conversion formats other than JSON, without changing the program structure. The data array serialization class provides formatting of only the end elements, therefore it can be changed when the data format changes significantly, for example, from JSON to XML or SQL.

The explanatory class diagram looks like this.

Class diagram

Class diagram

Below is the complete example code.

enum Classifier { NONE, CEREALS, DRINKS, PACKS };

class Product {
private:
    std::string m_name;          // Наименование товара
    Classifier m_category;       // Классификатор товара
    double m_price;              // Цена
public:
    Product(std::string name, Classifier category, double price) :
        m_name(name), m_category(category), m_price(price) {}
    std::string name() const { return m_name; }
    Classifier classifier() const { return m_category; }
    double price() const { return m_price; }
};

class ISerialize {
public:
    virtual std::string serialize(const std::shared_ptr<Product> obj) const = 0;
};

class ProductToJSON : public ISerialize {
public:
    virtual std::string serialize(const std::shared_ptr<Product> obj) const {
        std::string str;
        str += "{name:" + obj->name() + ",";
        str += "classifier:" + std::to_string(obj->classifier()) + ",";
        str += "price:" + std::to_string(obj->price()) + "}";
        return str;
    };
};

class ISerialization {
    virtual void serialization() = 0;
    virtual std::string str() const = 0;
};

class SerializationToJSON : public ISerialization {
private:
    std::stringstream sstream;
    std::list<std::shared_ptr<Product>> products;
    std::shared_ptr<ISerialize> serializer;
public:
    SerializationToJSON(std::shared_ptr<ISerialize> serializer,
        const std::list<std::shared_ptr<Product>>& products) {
        this->serializer = serializer;
        this->products = products;
    }
    virtual void serialization() {
        sstream << "[";
        for (auto& elem : products) {
            sstream << serializer->serialize(elem);
        }
        sstream << "]";
    }
    virtual std::string str() const {
        return sstream.str();
    }
};

int main() {

    std::list<std::shared_ptr<Product>> products{
        std::make_shared<Product>("Product 1", Classifier::CEREALS, 500),
        std::make_shared<Product>("Product 2", Classifier::DRINKS, 400),
        std::make_shared<Product>("Product 3", Classifier::PACKS, 300)
    };

    SerializationToJSON serializer(std::make_shared<ProductToJSON>(), products);
    serializer.serialization();
    std::cout << serializer.str() << std::endl;

    return 0;
}

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *