This article will present the Single Responsibility Principle (SRP). Along with the introduction of this topic, examples from MechLynx project will be used (https://github.com/CharlieGearsTech/MechLynx)
SOLID principles were introduced by Robert C. Martin around 2000. These principles are “guidelines”, they are not rules due that the application of these principles for certain software systems will not be feasible or recommended in some cases. The goal of these principles is to create reusable software modules that are able to tolerate changes over time and are easy to understand.
The design of a software system should be well made from the different perspectives of SW: Architecture, SW Component Design and Source Code. Well-made software has two fundamental pillars: they should work and they should be easy to change. Easiness of change has more priority than the workiness of the system, but of course this can have a grade of flexibility allowing, for example, a legacy source code that works “fine” to not be promotable to changes and that this Software System will not require further changes at architectural level or SW Component level. Some Software Systems might have a terrible architecture but a good source code design, so in these cases remind that architecture’s role is nonessential and might not infer critical authority this is not meaning that architecture is not important, architecture is critical for deployment, maintenance and further development of the Software system.
Common Closure Principle- Architecture level SRP
SRP for an architecture level dictates: “A specific SW Component for each layer should be responsible for only and only one, reason to change (actor)”.
Architecture level SRP is called Common Closure Principle (CCP) which can be stated from a different perspective “Every class that changes for a specific reason should be grouped to a specific SW Component”. Layer of SW Components can be defined for example, as the Business Rules layer, or the Utility layer.
The following figure illustrates a software system that violed CPP.
The problem with this simplified system is that a SW Component is serving two actors. So a change demanded by one actor can affect the functionality of the other actor, then if for example Actor Expert requires a change, this change might affect the Asker actor functionality. This is a larger scale of what a SW Component with two responsibilities will do, but that case will be seen in the next chapter.
If the previous Software system is separated into SW Components based on layer ( Business Layer, Utilities Layer), and then define SW Component that encloses classes and modules that change for only one reason, the MechLynx SW system will not violate CCP. The following image illustrates this procedure result.
Notice that in this diagram, no SW Component depends on Inference Kernel, this is due it is an illustration of the Dependency Inversion Principle that will be seen in later articles.
Inference kernel is the principal SW Component which contains the Business Rules since it takes the Knowledge base data and processes it as the requirement specifies. PropCompiler is a very stable SW Component which processes the compilation of the knowledge base sentences and uses PropCompiler Interface to deliver results to both SE View and Inference Kernel. SE View is the GUI that interacts with the Asker.
Using this last approach, the developer will be able to know which SW Component should be changed based on the actor request of change, making development, deployment and maintenance of the system to excel.
Although in reality, changes from a specific actor can affect various SW Components, using the CCP principle, the Software team will try to minimize the amount of SW Components changed by actor request, this will yield a design where a SW Component contains classes that change only by one actor.
SW Component level SRP
SRP for a SW Component level dictates: “An class should have only one reason to change”.
Inside a SW Component, a class should avoid as much as possible to have more than one responsibility. By doing this, this class avoids that:
- There can be heavy dependencies to libraries that only one responsibility needs but the other responsibilities will not.
- One change to the operation of the class demanded by one responsibility can affect the other responsibilities.
- Changes triggered by one responsibility will force unnecessary rebuilding, retesting and redeployment that affect the other responsibilities.
The following image shows a class that violates the SRP:
This Software System design forces the Inference kernel class to have two responsibilities, specifically, publish results of the inference to the SE View and obtain the rules to infer from the Knowledge Base.
Due to the responsibility from SE View, Inference Kernel is forced to include QWidget features, then having dependencies from this part of the Qt Framework, this means larger times of compilation and deployment. This QWidget dependency is not necessary for the Knowledge Base features, so this dependency will be carried for every change that Knowledge Base can require.
For example, when one change from Knowledge Base can affect implicitly the function that SE View uses. This can cause unexpected issues or the need for a retest of SEView features. This is going to cause more effort to test, and develop solutions.
When a change from SE View is performed, then it is required to build features of the Inference Kernel that belongs to Knowledge Base, also the developer should ensure that every kind of test of Knowledge Base is passing and this developer should need to redeploy Knowledge Base source code.
SRP dictates that the Inference Kernel should be splitted into separated classes that only have one responsibility: one set of classes to serve Knowledge Base and another set of classes to serve SE View. The next image shows a possible approach to comply with SRP:
This new software system design splitted the knowledge base features into a class called Workspace, and the SE View features into the Inference Kernel class. In this way, changes from the Inference Kernel class affect the Workspace class.
Changes in one class are not going to affect the other class, for example changes of the Inference kernel are not going to affect Workspace. There are no unnecessary dependencies between classes, the dependency from Inference Kernel is moved to an external interface ; Workspace class is not required to include these dependencies during its development, testing, maintenance and deployment.
Source Code level SRP
There is a refactoring principle that is similar to SRP which dictates: “A function should do only one thing at a time”. Functions tend to grow as they get older and make their maintenance, development and test harder to read and change.
The following function from MechLynx performs the syntax compilation of the PROPCompiler SW Component.
This function has poor readability and might content redundancy. This type of function might save time for the developer at a time, but it will cost time to this developer and the whole team periodically every time that they analyze this code.
At first glance, this function can be splitted into several functions based on the responsibilities that it is performing:
- Process Workspace file
- Process of EOL Lexemas
- Process of EOF Lexemas
- Process of valid Lexemas
- Process Semicolon Token
- Process Semicolon Token
- Check Rule Availability
- Process Workspace file
Each element of this list can be transformed into a function. Many IDEs contain the functionality to extract functions from specific instructions. The recommendation is to extract functions until they are not more than 5 lines of code per function, this can vary depending on other circumstances, big switch statements that make clearer the logic, avoid passing too many arguments, avoid returning huge objects, ect.
Usually huge functions hide class implementation, where the majority of local variables can be private variables, instruction transformed into functions, and arguments and return values can be interfaces.
Even when the extraction of function is performed correctly, the names of the functions are also important for readability as they act as signposts when developers navigate through the code; then both extraction of functions and well-written function names should coexist together. The following code is a good example of this cohesion.
prepareConclusionToBeDisplayed() arranged conclusion atoms to be displayed for any presenter. The preparation process is separated into a few steps: Clear the latest set of conclusions, set atoms for Modus Ponens verification, set atoms for Modus Tollens verification and append different types of atoms/conclusions. The reason that these names are large is because this implementation is deep down a complex logic, which dictates shorter names. The idea is that functions names should be more descriptive as the logic goes down.