Shortcut to seniority
Home
Go to main page
Section level: Junior
A journey into the programming realm
Section level: Intermediate
The point of no return
Section level: Senior
Leaping into the unknown
                    Go to main page
A journey into the programming realm
The point of no return
Leaping into the unknown
Our classes should do only one thing, and not fill up a class with the functionality that can be split in multiple, smaller classes. In other words, do a job, and do it well.
This principle can be applied everywhere, from the logic of a function, to classes, or to software components.
If you have the entire functionality of the software into one place, you will have to change the code / functionality in that place, as soon as requirements change. If your class does more than one thing, it will be changed more often than it should – more than it would have been if it were split into two classes from the beginning, for example.
“Could be so, but I will do anyway the same number of changes, either be them in one place or split in multiple classes” – you might say. But when you change a class, it impacts the dependencies of that class as well. Therefore, you will have to recompile and update the dependencies that have nothing in common with what you changed, but with the other responsibility of that class.
Other than that, if the class has only one responsibility, they are much easier to explain, to understand, and to implement, and this directly impacts the number of bugs that can exist in that class.
The principle says that we should write the code so that we can add new functionality without changing the existing code. This can be achieved by using interfaces and inheritance. Interfaces also introduce an additional level of abstraction, which enables loose coupling - the classes that implement the interfaces are independent from each other.
The main idea is simple: If we don’t follow this principle, and we have to modify old code to accommodate the new functionality, we may introduce new bugs into the existing code, and we will eventually write more code than we should.
If we have some logic that needs to be changed each time we add new code, we may think if we can extract an interface from that code and move the logic into separate classes that will inherit from the interface and implement the functionality in those classes.
The principle comes as a continuation of the Open / Closed principle by focusing on the behavior of a base class and its subtypes, and in other (less complicated) words, it says the following:
If we have a base class and other classes inherit from it, we should be able to replace objects of the base class with objects of its subclass without breaking the application. This principle enforces subclasses to behave in the same way as the parent class.
We can achieve this by following a few rules:
An overridden method of a subclass should be able to accept the same input parameter values as the methods of the base class. Less restrictive validation rules can be implemented, but not stricter ones.
The return value of a method of the subclass need to comply with the same rules as the return value of the base class’s method. Stricter rules may apply here, so we can return a subset of the valid return values of the base class.
If we have an interface with many methods, and classes that implement this interface only override a few methods, while others are there just so the software will compile, then we’re failing this principle.
This may not look like such a big deal, but what would happen if we want to change the signature of a function from the interface? We will have to update the code for all classes that implement that interface, even if they don’t need / use that function at all.
We should decouple the high-level and the low–level modules, by introducing an abstraction between them. We want this so we can reuse the modules and change the low-level modules without affecting the high-level ones.
Therefore, by adding an abstraction layer, both high level and low level modules will depend on the abstraction and not on direct implementation.
This will allow you to change higher-level and lower-level components without affecting any other classes, as long as you don’t change any interface abstractions.
Composition over inheritance states that we should prefer using composition (to contain instances of another class) instead of inheriting from them.
With composition, it is easy to change the behavior at runtime, through setter functions for example.
It also makes it easier to uphold the Single Responsibility Principle.
However, you’ll often have to write a couple of extra lines that would not be necessary if you had used inheritance, often requires delegation (a method which simply call another method of another object).
Inheritance has a few disadvantages, such as:
We should use composition if we want only parts of the behavior exposed by one class, and inheritance if we want to use one class (the derived class) wherever the other one (the interface / base class) is expected.
As a guideline, we can think of it as such:
Composition refer to a “has a” relationship (a human has legs, a car has wheels), while inheritance refer to a “is a” relationship (a man is a human, a car is a vehicle).
However, there is no correct answer here.
As this is a tricky principle, as a rule of thumb, we should follow this one instead: Before using inheritance, consider if composition makes more sense.
Code reuse means we should make use of the reusability principle by using existing software to build new ones. This will help us save time by not reinventing the wheel, and by using software that is already tested by other people, and much more secure than what we would implement ourselves.
Nowadays, we have A LOT of good, commercial and open source libraries, which does the required job for us.
These libraries are already intensively tested and used by the community.
One of the disadvantages would be that we may have to come up with a more general interface than we originally intended, because the clients may have additional / different expectations or requirements.
If we will not follow this principle, we will spend hours, days, or even months, to write something that can already be found as functionality in a library. In the end, the implementation that was created from scratch will probably be more buggy than the one available on the market / as open source.
Cohesion refers to the way methods and fields are linked together. As a rule of thumb, methods and fields that are similar or that are used together should have high cohesion (for example to be placed in the same class, or namespace). High cohesion in classes and components allows for more understandable code structure and design.
If we will not follow this principle, we will have code scattered throughout the project, which depends on each other, and which will be built together when we change something on one side. This will increase the build times, will reduce the quality of the code by making it harder to reason about the code, to maintain it, or to add more functionality.
Loose coupling is a sign of a good design in the application, and it refers to how individual design components should be constructed so that we reduce the amount of unnecessary information they need to know about the other one.
Combined with high cohesion, it allows for high readability and maintainability. Two classes, components, or modules, are coupled when one of them is using the other one. When a component is not strongly coupled in the system, it can be easily changed or replaced.
If we will not follow this principle, we will have too many dependencies in the system, and any change we do in one place will affect the dependencies as well. Other than that, unless we decouple the classes through abstraction, we will have a hard time adding future functionality.
DRY is a design principle that is focusing on reducing any kind of repetition in the code.
If we will not follow this principle, we will end up with duplicate code. Any change in such code will require us to go and modify manually in all the places where that code or variable is found, making the code harder to read, debug, and reuse.
At the same time, a large number of bugs in the code are due to copy and paste errors.
KISS is a design principle that is focusing on simplicity. The principle states that simplicity should be a key goal in design, and that unnecessary complexity should be avoided.
If we will not follow this principle, the unnecessary complexity will increase the debugging time and may also slow down the application.
The pareto principle (also known as the 80/20 rule) is stating that 80% of the effects come from 20% of the causes.
In software development, we can think of it like this: 20 percent of the code has 80 percent of the errors.
For performance areas, focusing on 20% of the code (the hot path – the code paths that are executed most often) will increase the speed by roughly 80%.
If we will not follow this principle, we may spend hours, days, or even more, to improve the performance of some piece of code that will not have too much effect on the final result.
Separation of concerns is a design principle that is focusing on modularity. Having a modular application simplifies its development and maintenance, allowing for reusing modules, and also for implementing and updating them independently. Separation of concerns is achieved by encapsulating information inside a section of code that has a well-defined interface.
If we will not follow this principle, we will have multiple functionality in one place, and we will affect one functionality when we change another one, although they could have nothing in common.
You aren’t gonna need it refers to the fact that a programmer should not add functionality (features, extra logic) unless there’s a clear need of it.
If we will not follow this principle, we might have features that are not used and that add unrequired complexity to the code.
Every class should have a single responsibility, and that responsibility should be entirely encapsulated by the class.