A Programming Voyage
Practically SOLID ============================================================ In this series of articles, we shall examine the [SOLID](https://en.wikipedia.org/wiki/SOLID) principles of object oriented programming (OOP) from the standpoint of practical software development. These articles are not meant to be an introduction to SOLID or OOP; there are countless such articles that serve that purpose already. Instead, these articles are meant to show how we may interpret these principles in a manner suitable for usage in a real world setting. The (S)ingle Responsibility Principle -------------------------------------------------------------- The first principle of SOLID, the single responsibility principle(SRP), may very well be the most well known of all of the SOLID principles, but it may also be the hardest to understand and apply correctly. Why this is the case we shall examine in the first part of this article. After which, we shall propose a new principle that in spirit accomplishes the same goal as the SRP but does so in a way that is much clearer to understand and reason about. The SRP, may be stated in the following two equivalent forms: - *An entity (class/module) should have only one reason to change.* - *An entity (class/module) should have only a single responsibility.* If an entity has only one reason to change, then it is responsible for only one thing. Likewise, if an entity is responsible for only one thing, then there is only one reason to change. In any case, we could say that an entity is responsible for doing something. But what could this something be? There are many such possibilities: it could be that the entity has the responsibility of representing something like a data container object like an array, a linked list, a hash map. It could also be that it has the responsibility of performing some operation like sorting an array. Such example responsibilities certainly provide us a sense of the SRP but to get a clear pictures of the difficulties of applying the SRP, we need to study an example in complete detail. Let's suppose we come across some code that appears to have the intention of representing a general purpose 4D vector: ```````````````````````````````````````````````cpp struct GeneralVector4 { float x,y,z,w; GeneralVector4(); GeneralVector4(float _x, float _y, float _z, float _w); // Addition and subtraction friend GeneralVector4 operator+(const GeneralVector4 & q, const GeneralVector4 & r); friend GeneralVector4 operator-(const GeneralVector4 & q, const GeneralVector4 & r); friend GeneralVector4 operator-(const GeneralVector4 & q); // Scalar multiplication friend GeneralVector4 operator*(const GeneralVector4 & q, const float s); friend GeneralVector4 operator*(const float s, const GeneralVector4 & q); // Vector multiplication friend GeneralVector4 operator*(const GeneralVector4 q, const GeneralVector4 & r); GeneralVector4 & operator=(const GeneralVector4 & q); }; ``````````````````````````````````````````````` We could say that this class does not actually satisfy the SRP as it has one responsibility to represent a 4D vector and another responsibility to represent the multiplication between 4D vectors, something that is not generally defined for 4D vectors. Yet, if we consider the source code of the vector multiplication, then we may see that it actually represents quaternion multiplication: ``````````````````````````````````````````````` GeneralVector4 operator*(const GeneralVector4 q, const GeneralVector4 & r) { return GeneralVector4( q.w * r.x + q.x * r.w + q.y * r.z - q.z * r.y, q.w * r.y - q.x * r.z + q.y * r.w + q.z * r.x, q.w * r.z + q.x * r.y - q.y * r.x + q.z * r.w, q.w * r.w - q.x * r.x - q.y * r.y - q.z * r.z); } ``````````````````````````````````````````````` From this, we could conclude that `GeneralVector4` meets the conditions to represent a quaternion, and in this sense, we could say that it satisfies the SRP. This is, of course, a rather bothersome inference; depending on our interpretation of what `GeneralVector4` does, we either determine that it satisfies the SRP or not. While it was probably a mistake of the original author to not name it a more appropriate name or leave an explanatory comment, we cannot assume that such mistakes won't happen in real world settings, and hence, we cannot guarantee that we shall be able to correctly determine if an entity, which we did not create, satisfies the SRP. Let's now assume that we have been using `GeneralVector4` in a project, and we find that there is a bug in our code related to our usage of it. To fix the bug, we are going to need to have a way to convert the values stored in `GeneralVector4` into a string so that it can either be serialized to disk or printed to the command line. Let's now define a new entity/class called `DebugGeneralVector4`. Its purpose is to represent a general vector that is suitable for our debugging purposes: ```````````````````````````````````````````````cpp #include struct DebugGeneralVector4 : public GeneralVector4 { ... std::string ToString() { ... } } ``````````````````````````````````````````````` We can say that this class satisfies the SRP as it only has one responsibility, the responsibility to represent a debuggable general vector. Unfortunately, this class is probably not the most practical to use since we would need to replace existing occurrences of `GeneralVector4` with `DebugGeneralVector4` so that we can gain access to `ToString`. Even more troublesome is that `DebugGeneralVector4` now directly includes the C++ string header, which has a non-trivial cost on the build time[^build_times]. If we need to replace instances of `GeneralVector4` in header files, then we shall surely pay a price for this in terms of build times. All together introducing `DebugGeneralVector4` was probably a pretty bad idea; we should have just introduced a free standing function to convert the vector into a string. The SRP, however, did not in any way prevent us from making this bad choice simply because the SRP has no influence on what a single responsibility we assign to an entity does. This is all rather problematic since this implies that we can make an entity do as many things as we like so long as we find a general enough responsibility. [^build_times]: Although we could reduce these compile times by forward declaring std::string, this is not a universal solution as we may wish to have an inline function definition for reasons of performance or to avoid the creation of source files. ## The Simple Dependency Principle From the above discussion, we have encountered two main problems with the SRP: - We must know what responsibility an entity has before we can determine if the entity satisfies the SRP. - We cannot use the SRP in determining if an entity's responsibility is overly general. To understand how to fix these problems, we must understand the SRP in more depth. The SRP at its core is a goal along with a strategy or principle to accomplish that goal. As we have seen before, this strategy of the SRP is rather indirect, but its goal is actually not so. We may state the goal of the SRP as: - Modifying any part of an entity's functionality should not result in many functions or files that depend on the entity but not on that particular functionality needing recompilation, retesting, or modification. This is a very clear goal, and it also naturally leads to a concisely stated strategy to fulfill it: - Any class/module of a code base that depends upon an entity should depend upon all functionality expressed in that entity. In comparison to the goal, this strategy more precisely defines the dependency of a code base on an entity through either a class or a module. This notion of dependency provides just enough coarseness to reasonably expect that all parts of the entity would be used within that portion of a code base. This strategy/principle as such we may refer to as the Simple Dependency Principle(SDP) because of its goal of simplifying the dependencies a code base has upon an entity. Unlike the strategy of the SRP, this strategy is rather precise, enough so that we could expect it to be implemented through an algorithm. A further difference from the SRP is that the SDP does not involve an entity in isolation; it considers an entity in the context of a code base. This means that it can only be applied after an entity has already been integrated into a code base or after an entity's usage has been envisioned within the context of a hypothetical code base. Such differences between the SRP and the SDP may be better understood by turning our attention back to the prior examples. The class, `GeneralVector4`, satisfies the SRP depending on what it is supposed to do. In the case of the SDP, we must first understand how this class would be used in the context of a code base. Such a class would likely be used as a unit quaternion to represent rotations in a code base related to computer graphics. In such a case, we would then expect that operations related to addition or scalar multiplication would not be used as they are not operations that make sense for a unit quaternion. Consequently, we would conclude definitively that in typical circumstances `GeneralVector4` does not satisfy the SDP. In the case of `DebugGeneralVector4`, we had concluded that it undoubtedly satisfies the SRP despite not satisfying the goal of the SRP. Whether or not this class satisfies the SDP comes down once again to how it is used within a code base. It could be that we need to debug a vector instance isolated to some class or module in which case we would find that all parts of the functionality of this vector are used. On the other hand, if the vector instance that we sought to debug was used in multiple locations but only one of which required debugging, then we would conclude that `DebugGeneralVector4` through this instance does not satisfy the SDP. This shows that while the SDP can be quite helpful at times it is also limited by an entity's usage in a code base. The more distribution that an entity has in a code base, the better the SDP is able to make a definitive statement. From these examples comparing the SRP and the SDP, we have seen that the SDP is able to make more definitive and more accurate statements about an entity's nature than the SRP. But, this is all under the limitation that either an entity be directly integrated into a code base or envisioned within a code base. This idea of not being able to apply a principle to an entity in isolation certainly goes against the idea of SOLID in general; we are supposed to be able to use SOLID to make good design choices upfront without consideration for the exact code base the entity is going to be used in. But, from a practical standpoint, this is not such a bad thing. Entities in software development are not by and large universally well defined things like mathematical objects or data containers. They are, instead, things that frequently change whenever the design of the project they are used in changes. Satisfying the SRP upfront may make it easier for these entities to change (and be refactored), but it may also make the design of these entities prematurely optimized, resulting in an overly complex code base. ## Summary Within this article, we delved into the SRP and discovered a problem, namely the indirect nature of the SRP. We saw how this indirect nature led to multiple problems that do not have clear solutions. To overcome these problems, we proposed a new principle based directly on the goal of the SRP. This, principle unlike the SRP, has a posterior nature to it. That is, the principle more generally applies after an entity has already been integrated into a code base or after it can be envisioned how the entity will be integrated into a code base. While this certainly is a limitation to an extent, it is also a limitation that realistically speaking is not truly important. Designing entities perfectly upfront is highly unrealistic, but what is realistic and also highly important is the ability to detect a poorly designed entity and fix it both of which the SDP is designed to help with.