Single Responsibility Principle (SRP)

The single responsibility principle states that a software module should have a single responsibility, that is, it should have one and only one reason to change.

In order to understand how to reason in order to come to the observance of this principle, let's look at a couple of typical examples. The first is taken from a popular repository.

class textEditor {

    private:
    stack<char> leftStack; //Left stack
    stack<char> rightStack; //Right stack

    public:
        void insertWord(char word[]);
        void insertCharacter(char character);
        bool deleteCharacter();
        bool backSpaceCharacter();
        void moveCursor(int position);
        void moveLeft(int position);
        void moveRight(int position);
        void findAndReplaceChar(char findWhat, char replaceWith);
        void examineTop();
};

This example is notable because it can be used to illustrate several errors related to the single responsibility principle. We will not discuss the controversial solution of using stacks to store text to the left and right of the cursor for now. Let's focus on a set of methods.

First of all, it is worth highlighting two methods (deleteCharacter, backSpaceCharacter) for deleting a character. Logically, they duplicate each other. Quite a common violation of the principle under consideration, especially if the work is performed in a team. Similar algorithms implemented in different parts of the code are a significant problem when modifying.

In addition, the methods are clearly divided into three groups: working with data (insertCharacter, deleteCharacter), moving the pointer (moveCursor, moveLeft, moveRight), and the rest (insertWord, findAndReplaceChar, examineTop).

The group of methods for working with data has the right to be included in this class, but when designing, it is necessary to ask the question. Will the stored data change its type. For example, is it planned to use Unicode instead of ASCII. If the answer is positive, these methods should be excluded from the class, and the class itself with the stored text should be made based on the abstract one.

The pointer movement methods are included in the class due to the poor implementation of data storage. As soon as this changes, there will be no need for this. The examineTop method can also be included in this set, the meaning of which is closely related to the data storage method.

In the future, it is obvious that the search and replace method should be implemented separately from the data storage class and divided into two separate ones, but the method of adding a word should be considered from a different angle.

In the example above, the word is treated as an array of characters, but in the context of a text editor, characters are not key. Linguistics, in the section on syntax, treats a word as a unit of data, followed by a sentence and a paragraph. This is how users work with an editor, so we should organize data storage in this way.

But we can't do without access to individual characters, we need spell checking, prefixes, endings and other morphology. Therefore, we need to store all this data separately, characters, words, sentences and paragraphs, but creating so many storages is quite reckless. The rule says that data should not be duplicated. Therefore, discarding the rest of the C++ language containers, we will stop at a dynamic array (vector), which will store characters. A dynamic array is referenced through an iterator, and not through positions, like a string container, there will be no need for constant recalculation. The container with characters is the basis, other containers complement. They / it will store references to the array with characters, in places where words, sentences and paragraphs are located.

Dependency diagram

Dependency diagram

This example shows how by breaking down large tasks into smaller ones we not only achieve code flexibility, but also have the ability to prevent possible errors at the design stage. Of course, we have to sacrifice something, so the projects are divided into many small classes.

The following example shows the technique of dividing a complex algorithm into smaller ones. Calculating the cost of goods. When selling goods in an online store, the price of the goods takes into account various charges and deductions.

– If purchases are more than 10,000 units, the buyer is given a 3% discount.

– Delivery of goods worth more than 15,000 units is free. Otherwise, the delivery price is 5% of the amount.

The typical solution to this problem looks something like the first diagram, a method is created in the class that performs calculations. This solution is the most obvious and simple, but also the most inconvenient in terms of modification.

Let's imagine that the marketing department periodically holds promotions, sets discounts, bonuses, introduces new loyalty programs. Each time you will have to rewrite such a method. But the difficulty is not even in this, but in the fact that the new calculation functionality deletes the outdated one, it becomes impossible to return to it. You have to add new methods to the class, and modify the associated code, retest, etc., and this is not the fastest way. With long-term use of such software, the time spent on its maintenance becomes greater than on the implementation of new functionality.

So the first step when you are faced with a problem like this is to separate the data and the actions on it, which is reflected in the second diagram. Now let's see why instead of combining all the conditions in one method, we split them into separate ones. In this problem, there are only two calculations, in reality this sequence can be quite long, and the worst thing is that it can change. For example, when marketing decides to swap the calculations, the shipping amount is calculated first, and then the discount is applied.

Class diagram

Class diagram

It is for these reasons that in such situations it is better to use step-by-step execution of actions, and this of course does not apply only to trading, such situations occur quite often.

But to break down activities, you don't need a tool to manage them, and the most obvious tool is the Chain of Responsibilities pattern.

First, let's create an abstract class on the basis of which we will implement the chain.

class AbstractCalculate {
protected:
    std::shared_ptr<AbstractCalculate> handler;
public:
    AbstractCalculate(std::shared_ptr<AbstractCalculate> handler) {
        this->handler = handler;
    }
    inline virtual std::shared_ptr<Product> calculate(std::shared_ptr<Product> prd) const = 0;
};

A detailed description of the pattern can be found in the reference literature, its essence is in the recursive call of the method. Therefore, we will continue with the creation of a set of static values ​​for the implementation of conditions.

class DiscountRule {
public:
    const static double amount;
    const static double percent;
};

const double DiscountRule::amount = 10000.0;
const double DiscountRule::percent = 0.03;

class DeliveryRule {
public:
    const static double amount;
    const static double percent;
};

const double DeliveryRule::amount = 15000.0;
const double DeliveryRule::percent = 0.05;

Here they are grouped into separate classes, but if the conditions are more complex it makes more sense to create separate predicates based on constexpr.

The next step is to create specific implementations of the calculations and a test example.

The complete implementation code is given below.

class Product {
private:
    std::string m_name;          // Наименование товара
    double m_price;              // Цена
public:
    Product(std::string name, double price) :
        m_name(name), m_price(price) {}
    std::string name() const { return m_name; }
    double price() const { return m_price; }
};

class AbstractCalculate {
protected:
    std::shared_ptr<AbstractCalculate> handler;
public:
    AbstractCalculate(std::shared_ptr<AbstractCalculate> handler) {
        this->handler = handler;
    }
    inline virtual std::shared_ptr<Product> calculate(std::shared_ptr<Product> prd) const = 0;
};

class DiscountRule {
public:
    const static double amount;
    const static double percent;
};

const double DiscountRule::amount = 10000.0;
const double DiscountRule::percent = 0.03;

class DeliveryRule {
public:
    const static double amount;
    const static double percent;
};

const double DeliveryRule::amount = 15000.0;
const double DeliveryRule::percent = 0.05;


class Discount : public AbstractCalculate {
public:
    Discount() : AbstractCalculate(nullptr) {}
    Discount(std::shared_ptr<AbstractCalculate> handler) : AbstractCalculate(handler) {}
    inline virtual std::shared_ptr<Product> calculate(std::shared_ptr<Product> product) const override {
        std::shared_ptr<Product> prd = product;
        if (product->price() > DiscountRule::amount) {
            prd = std::make_shared<Product>(
                product->name(),
                product->price() - product->price() * DiscountRule::percent
            );
        }
        if (handler != nullptr) {
            return handler->calculate(prd);
        }
        return prd;
    };
};

class Delivery : public AbstractCalculate {
public:
    Delivery() : AbstractCalculate(nullptr) {}
    Delivery(std::shared_ptr<AbstractCalculate> handler) : AbstractCalculate(handler) {}
    inline virtual std::shared_ptr<Product> calculate(std::shared_ptr<Product> product) const override {
        std::shared_ptr<Product> prd = product;
        if (product->price() < DeliveryRule::amount) {
            prd = std::make_shared<Product>(
                product->name(),
                product->price() + product->price() * DeliveryRule::percent
            );
        }
        if (handler != nullptr) {
            return handler->calculate(prd);
        }
        return prd;
    }
};

int main() {
    auto product = std::make_shared<Product>("", 11000.0);

    auto chainlet = std::make_shared<Discount>(
        std::make_shared<Delivery>()
    )->calculate(product);

    assert(chainlet->price() == 11203.5);

    return 0;
}

Similar Posts

Leave a Reply

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