0

I've been reading a bit through Clean Code and I came across the Single Responsibility Principle. The principle states that every class should have only one responsibility.

An example taken from chatGPT gives me this:

#include <iostream>
#include <vector>
#include <string>

class Product {
public:
    Product(std::string n, double p) : name(n), price(p) {}

    std::string getName() {
        return name;
    }

    double getPrice() {
        return price;
    }

private:
    std::string name;
    double price;
};

class Inventory {
public:
    void addProduct(Product product) {
        products.push_back(product);
    }

    void listProducts() {
        for (const auto& product : products) {
            std::cout << "Product: " << product.getName() << ", Price: $" << product.getPrice() << std::endl;
        }
    }

private:
    std::vector<Product> products;
};

In this example the class Product holds all the variables related to a Product it doesn't allow to do any operations on them. Inventory instead is a class whose responsibility is to hold a list of products.

Many examples I've seen around are very similar to the one above (including the one provided in Clean Code).

What I don't understand however is how would the principle apply to a class like the following

#include <iostream>
#include <string>

class Vector2D {
    public:
    
    Vector2D(double x, double y):mX(x),mY(y) {}
    Vector2D operator+(const Vector2D& rhs) const {
        return Vector2D(getX() + rhs.getX(), getY() + rhs.getY());
    }
    
    double operator*(const Vector2D& rhs) const {
        return getX()*rhs.getX() + getY()*rhs.getY();
    }
    
    double getX() const {
        return mX;
    }
    
    double getY() const {
        return mY;
    }
    
    void setX(double x) {
        mX = x;
    }
    
    void setY(double y) {
        mY = y;
    }
    
    private:
    double mX;
    double mY;
};

In this class I see two responsibilities. The first is holding the data of the 2D vector while the second is to perform operations with other 2D vectors.

This sounds natural to me, but I think if I had to follow the SRP I should probably separate the two responsibilities with either a separate class modelling the operators on 2D vectors or maybe having a namespace with only just operators.

Is this interpretation of mine of the SRP for this example correct? Any examples from literature or just some random code would be useful to clarify the idea.

11
  • 3
    A Vector2D with all its operations is a common, cohesive abstraction for a useful mathematical object with well-known operations. Hence, my best judgement about it is, it usually makes sense to see this class as "one thing", something with a single responsibility ("representing a 2d vector of real numbers").
    – Doc Brown
    Aug 18, 2023 at 5:35
  • 2
    ... and yes, the other question and its top-most answers are more general and includes yours: the top most answer says "the SRP is opinionated, use your best judgement".
    – Doc Brown
    Aug 18, 2023 at 5:37
  • 1
    Related, possible duplicate: How to determine if a class meets the single responsibility principle?
    – Doc Brown
    Aug 18, 2023 at 6:12
  • 2
    ChatGPT can be great, but be careful when you ask it for information. It is quite literally making stuff up on the go, and its primary purpose is not to give you accurate answers or distill "the wisdom of the masses", but to make the answer appear plausible to a human. It literally goes word by word, and chooses the next highest probability word according to its model. It doesn't understand what it's doing, and if you ask it for its reasoning, it won't tell you why it did or chose something (it doesn't know why) - instead it will come up with a fake but plausible-sounding answer. Aug 18, 2023 at 8:09
  • 2
    I'm just saying don't rely on it too much if you're looking to understand something with some depth. If you want to learn about SRP and other design principles, quick example code is not going to cut it - among other things because the principles are not about static code, but about maintaining evolving software, and that has aspects to it that can't be derived/understood from code examples alone. Aug 18, 2023 at 13:26

4 Answers 4

6

No useful interpretation of the SRP prohibits the implementing of operations on the type that the class represents. If we obeyed that constraint, we would have only records (fields but no nontrivial methods) and utility classes (methods but no fields). A "single responsibility" must therefore mean something else.

For a vector with a given width, putting vector operations into that class usually makes sense. (There are special cases such as C++ where there are lower-level reasons to make operations "friends" rather than "members" in the strict sense, but the principle holds.)

In the "Product" example, an "Inventory" is a lot more complex than a mere Product; it requires a new level of organization in a way that vector addition, compared to mere vectors, does not. For instance, an inventory can hold a lot more than at most two products, it can hold multiple instances of the same product, there are further complications arising from the totality of all products (e.g. restrictions on how much dangerous goods may be stored together).

All this means that it would distort the domain model too much to mix these responsibilities with the responsibilities of single products (e.g. calculating the volume given the dimensions).

1
  • The first sentence of this answer can be trivially proven with OP's own example. The Inventory methods are not "operations on the product" as OP claims, they are operations on the collection of products, which is what Inventory represents. The operations are defined by the type on which they operate.
    – Flater
    Aug 21, 2023 at 5:26
1

Your specific case

In this example the class Product holds all the variables related to a Product it doesn't allow to do any operations on them.
Inventory instead is a class whose responsibility is to hold a list of products.

I disagree with your observation, to the point where you're actually proving the opposite case.

Inventory does not contain any operations on the product. Operations on the product would be things such as changing the existing product, or creating a new product from the old one(s). None of that is happening here. No matter how often you call any of these methods, the products themselves will not be altered (nor will new ones be created) in any way.

Inventory contains operations on the collection of products. These operations (when called) will alter the collection of products. What type represents the collection of products? Oh, that's right, Inventory.

So if you consider the implementation of Inventory to be correct; then you must invariably also agree that the implementation of Vector2D is correct. In both cases, the operations operate on the type which defines these operations, leading to the original object either being altered or a new object being created from the older one(s).

I'm very intentionally not discussing immutability versus mutability for these operations, as it's not related to the current question being asked.


The general point

These kinds of questions always hinge on the person asking the question relying on some subjective definition of what a "responsibility" is and why [this other thing] is clearly not part of their definition, therefore allegedly violating SRP.

The short answer here is that your definition of a responsibility is too strict, which is causing you to mistakenly flag things that are not SRP violations; they're just things that you didn't think of before you coined your definition of a "responsibility".

The definition of a "responsibility" is intentionally kept vague because it's impossible for one rule to encompass every possible application and its business context. That is simply not a reasonable expectation.
For example, in an application that is only used nationally, a price can be represented using a number type. However, in an application that is used internationally, you need to consider the currency being used. And then beyond that, e.g. for a stock trading tool, you also need to consider the fluctuations of all these variables over time, e.g. you couldn't express someone's net worth as a fixed monetary value, you'd need to factor in the current market rate for all of their holdings.

There is not just one definition of what constitutes an expression of an amount of money, and therefore there is not just one definition of this responsibility.

What matters here is whether your class is expressing a singular concept, relative to your business context. In the example of a Vector2D, is it being used in a way that is thematically coherent to the concept of what you would refer to as "a 2D vector"?

And the answer to that is yes. Someone familiar with the problem domain, when they hear mention of 2D vectors, will inherently assume the existence of vector arithmetic. This is simply part of what makes a vector a vector.

Pedantically, the single "responsibility" of the Vector2D class is to represent a 2D vector, and that's precisely what it's doing.

0

In this class I see two responsibilities. The first is holding the data of the 2D vector while the second is to perform operations with other 2D vectors.

No, in reality that is only one responsibility, to encapsulate the 2d vectors. In practice, there are many nuances and there is no clear boundaries on where to split the responsibilities; in time practice will make it better.

I think it happens a lot to over-engineer when starting to use design patters. For me in order to decide on which responsibilities to assign to a class or create a new one, I rely on single responsibility principle in conjunction with interface segregation principle:

  • do I need to use different groups of methods(like to separate interfaces) in different places all over the place?
  • there is not much overlap between those functionalities?
  • is more likely that I need to change some functionality but not other(based on open close principle), then I might need to split in 2.

The second thing I usually do, is to keep one class, then refactor and split it in 2 when the responsibilities become clear.

0

SRP is not about doing one thing (see another chapter in Clean Code), but according to the clarification of R.C. Martin, about reasons to change:

The SRP states that each software module should have one and only one reason to change. (...) These questions can be answered by pointing out the coupling between the term “reason to change” and “responsibility”. (...)And this gets to the crux of the SRP. This principle is about people.

In the same article R.C. martin provides a better definition of SRP:

Gather together the things that change for the same reasons. Separate those things that change for different reasons.

This gives a practical criteria to apply sound separation of concerns, a principle that predates SRP by several decades:

  • In your example, the internal data structure is just a mean to implement the different operations of a vector. It's not a separate responsibility. You could very well decide to go for polar coordinates behind the scene: only internal code would change, not the publicly exposed members that could be used everywhere.
    On the other hand, you could decide to enrich the operations with other usual operations of mathematical vectors. So there is only one reason to change the public interface.

  • In your other example about products, product management attributes and operations are often discussed with other people (having in mind what a product should be) than inventory of products.
    Inventory is independent of the internals of the product and only needs to know the public product interface. The reasons for inventories to change are mostly business process related (how logistical operations are performed, and in which place, independently of the products). So it's a different concern and different reasons to change. Unfortunately, many toy examples for inventories and products are oversimplified and therefore confusing (one may not necessarily realize that every warehouse or point of sales have their own limited Inventory regardless of all the possible products that where created in the system).

3
  • The definition of "reason for change" is as flawed as the definition for "responsibility" is - there's no reason that one of these terms is any more descriptive than the other. Neither of them unequivocally explain exactly what the spirit of SRP is. Your answer is not incorrect and you are referencing the correct material, but as a matter of fact that source material is hopelessly unhelpful when trying to resolve OP's alleged conflict (or confusion, if you will).
    – Flater
    Aug 21, 2023 at 5:31
  • @Flater let me clarify: SRP makes only sense as a practical criteria to facilitate separation of concerns. A part of the confusion (to start withe RC Martin's argumentation that tried to invent something new as substitute for SoC) is also related to examples that do not help to prperly understand boundaries if one does not know the field.
    – Christophe
    Aug 22, 2023 at 8:00
  • "examples that do not help to properly understand boundaries" I think the overall issue here is that no such example could exist that would encompass every possible definition of a discrete concern (or responsibility, whatever word you prefer). Given that we can't universally define such a boundary, the rule (or guideline if you will) will always be liable to misinterpretation. "Separation of concerns" is no better than "single responsibility" in terms of being universally descriptive and objectively applicable.
    – Flater
    Aug 22, 2023 at 23:04

Not the answer you're looking for? Browse other questions tagged or ask your own question.