Best Practices in Programming Based on SOLID, KISS and Personal Experience
The guidelines a programmer should follow on a day to day basis are just as important as they are in any field. Applying these principles may initially seem tedious and time-consuming, but over time, the advantages won’t go unnoticed. Shortcuts, which may save time, can also lead to a variety of problems in the later stages of a project.
Empirical evidence shows there is no situation in which cluttered code won’t cause problems when it’s further extended. This is exactly why Robert Martin (a.k.a. Uncle Bob) created a set of rules for writing good code, which allow us to create simple, easily modified software. As a compliment to this approach, the term KISS, coined by Kelly Johnson, is applied as a basic principle.
What is KISS?
KISS is a term created in the 1960’s which was meant to reflect the approach of creating simple durable aircrafts that could be repaired easily in an emergency. This idea has since been adopted by many branches of science and engineering.
KISS is a term also used in developing solutions to programming problems. Literally translated, KISS means "keep it simple, stupid" or "keep it stupid simple".
However, it’s assumed, the statement was not supposed to sound negative, rather to suggest
a simple design makes service and maintenance child’s play.
The use of KISS in programming can be considered in three aspects:
1. Simplification for current and future cooperative development of your code. From my perspective, this is nothing but the skillful application of SOLID principles.
- Writing only enough code to pass a unit test, which relates to TDD and the first SOLID principle - Single Responsibility.
- Intuitive approaches to writing software, approachable and simply understood by everyone.
KISS alone sets no hard and fast rules regarding the implementation of code, as is the case with most SOLID principles, which are general guidelines for how to create SOLID code.
Let’s take a look at what makes something SOLID
Robert Martin, as previously mentioned, is responsible for establishing the 5 rules of SOLID:
Open / Close
Single responsibility pretty much speaks for itself. Each class or method should be responsible for one separate functionality. An example of breaking this rule can be seen in the code below:
In the example above, the rule is being broken by the fact that the Person class "exceeds its competences". Its only responsibility should be to represent the person's object and its properties, such as: name, age, address (preferably also as a separate object).
It is definitely not supposed to validate an e-mail address, postal code or add a product to the shopping cart.
Repairing the errors listed above and adapting the code to the single liability principle is as follows:
Applying the KISS principle is closely related to the TDD approach.
In order for a correct unit test to occur, one singular liability should be envisioned for it.
Open / closed, i.e. open to extension, closed for modification.
According to this principle, each method should be written in such a way that it can be easily expanded upon or overwritten, yet impossible to be directly modified.
What this means in practice is presented in the following examples.
Let’s start with the rule broken below:
Why is the code above breaking the Open / Closed approach? Because adding code to calculate an area, of e.g. a triangle, would require the direct modification of the area method from the Calculator class.
So how should the calculation of the area of a shape be implemented correctly? An example is shown below:
As you can see, to add a shape with the possibility of calculating its area, just create another class that inherits from the Shape class and define the calculation of the area method inside of it.
These first two rules are simple to use in any functionality, even small ones. Applying them, we can write code that will be easy to read and able to be interpreted by other programmers, as well as simple to extend.
At this stage, we’ve fully met the first principle of SOLID.
The next three rules are used in more complex solutions, or in other words, in extensive logic.
The third rule is Liskov substitution, which states that the interface, the abstract class and all methods of the derived class must remain 100% compatible.
What does this mean in practice? Accordingly, with the principles previously described here, this will be presented in two examples: broken and applied.
Typically, I present the incorrect variant first:
At first glance, everything is fine, there is the Animal class representing the animal and the inheriting Dog and Fish. So what is the problem? In the method enforced by the interface, the dog runs and it makes sense, but nobody has ever heard of a running fish.
This rule therefore is more of a reference to pure logic. The next example shows the correct application of it. It implements the logic of the tool which is most important for a programmer - the coffee machine:
Coffee is always brewed in the coffee machine, regardless of its type.
The next principle is Interface segregation.
A very good description of it is the following sentence: "many client-specific interfaces are better than one general-purpose interface".
In practice, this means that the interfaces created should be dedicated to only one functionality.
An example of breaking this rule is as follows:
In the example above, the print logic of various reports has been implemented. An interface was created with two methods declared: to print PDF and Excel spreadsheets. However, for the results of the school exam, you do not need an Excel spreadsheet.
In this case, this method is redundant, but since it’s forced by the interface, it must be executed. According to the principle of Interface segregation, a better solution is the declaration of several specialized interfaces.
To rectify the problem, we could create two separate interfaces: Printable PdfInterface and Printable ExcelInterface.
The last principle SOLID provides is Dependency inversion. It says that all dependencies should depend as much as possible on abstraction, not on a particular type. It sounds complicated, but it really is a very simple principle for declaring functionalities in a way that operates on the most common types of data. This essentially makes the code universal.
The following examples illustrate this well.
Breaking the rule:
Application of the rule:
The above principles should be treated as a set of tools for writing good sound code. However, it is not necessary to apply all these elements in every project.
Programmers should adapt this tool to the individual needs of the application. Intuitively, in a small project, we will not apply the last three principles, but the first two are definitely worth sticking to, even when building smaller functionalities.
From my own experience, I know that code written with best practices from the beginning is much easier to develop.