Software Development Best Practice #3 — Keep It Simple

Milan Gatyás
Life at Apollo Division
7 min readNov 10, 2020
Photo by Lakeisha Bennett on Unsplash

The aim of the first article, “Software Development Best Practice — #1 Do Not Repeat Yourself”, was to familiarize you with the problems often resulting from source code duplication. The second article, “Software Development Best Practice — #2 Loose Coupling”, warned you about the tricky nature of tight coupling and the problems that can hit you long after you allow tight coupling in your source code. This article will be about keeping your source code clean and simple, so that your colleagues can enjoy reading it without wanting to erase you from the surface of the earth. The techniques and tips presented in this article will help you, based on my personal experience, to be more effective and confident in your programming and make you happier while doing your daily work as the Software Developer.

Before we start, I would like to mention that flexibility and convenience abstractions usually come with a slight performance cost. Therefore, in certain edge cases, it might not be an option to invest in them. However, in most cases, especially in enterprise applications, the benefits of them outweigh the performance degradation cost. The examples in the article are going to be presented in the .NET C# language; however, the principles apply to the majority of widely used programming languages.

Single Responsibility Principle

I believe that no one likes to sweat while reading through complex source code constructs. There is no fun in the need to absorb a component of monstrous size just to be able to understand it. I bet a lot of you have experienced that situation when you were about to open that ol’ almighty, one-thousand-line god class, and instead went to brew yourself a coffee to delay that moment. Surely, there are cases where it is convenient to keep a bunch of the source code segments together. However, in my experience, most of the time it is not the case. And in most scenarios, violation of the single responsibility principle (SRP) is the root cause. The SRP demands that each code segment (module, class, method, etc.) should only have a single responsibility, or in other words, only a single reason for a change. By complying with the principle, your code will naturally segment itself into blocks that can be isolated for better understandability and reusability. Let us look at an example.

As you can see, there are a lot of things going on in the Process method, such as input validation, retrieval of data via HTTP, storage of data into database engine, and publishing of reporting messages via the AWS SNS. That is quite a lot of responsibility for the single method. If the code snippet would include all the source code required in the real-world scenario, the method could easily be about 200 lines long. Now if you are debugging an application and are interested only in the reporting part of the workflow, you still need to crawl through the whole method source code. Furthermore, unless like in the snippet above, the code fragments of different “responsibilities” are usually mixed, so you need to focus on the whole method body and categorize which code segment belongs to concrete responsibility. As a remedy, let us try to separate the individual responsibilities into isolated segments.

It is now easy to recognize individual responsibilities and their source code scope. The size of the class however got even bigger by all those new method signatures. Furthermore, the snippet is violating the loose coupling software development principle. What if data will be stored in a different database in the future, or reporting will not be done via the SNS anymore?

The snippet above loose coupled the code by the separation of the individual responsibilities into the dedicated interfaces with dependencies ensured via composition. It is now possible to reuse the ISerialCodeValidator in different places if necessary or replace the IReportingService underlying infrastructure without changing the implementation of the MyService itself. Equally important, unit testability of the code is also better as the code is segmented into smaller blocks, so generally, you need to mock fewer dependencies and focus on the testing of a single functionality per class. We can see a specific pattern you will often recognize while writing your source code. There is an orchestrator class, dictating the flow of actions by executing methods of its dependencies. Each dependency can act either as an orchestrator, or as an executor by executing its responsibility without further dependencies.

The Complexity Shift

What happens with the complexity when we adhere to the SRP? It does not go away. Rather, it transforms into a different type of complexity. Instead of reading through complex class structures containing mixed responsibility logic, you will read through a sea of micro-classes connected through dependencies. It brings up its own types of difficulties. I think you already heard that one of the most challenging parts of programming is “how to name stuff” and “where to put stuff.” This is especially true when adhering to the SRP because you create many single-purpose classes and models. However, I do believe that the benefits of the principle outweigh the negatives by a great measure.

Open-Closed Principle

Closely related to the SRP is the open-closed principle (OCP). The principle says that the code you write should be open for extensions while closed for modifications. In other words, we should be able to add functionality to the code without the need to modify the existing code. What is this supposed to mean? Let us show it in the example.

Is the snippet above adhering to the SRP? I guess we could say mostly yes. It is focusing on a single task, to validate the email. At the same time, we could say that it is doing three different validation types. Is it adhering to the OCP? It is not, because if we are going to add another type of email validation, we will most likely modify the source code of EmailValidator. Furthermore, it might be challenging to unit test the class since it is not possible to isolate different validation types and test them separately.

What about now? Is the code above adhering to the SRP? I would say yes as each validator is focusing on a single validation type, and EmailValidator is acting as an orchestrator. The unit testability of the code is also increased as we can now unit test each validation type separately. Is the code adhering to the OCP? Not entirely, as an extension of email validation with a new validation type will require the change of the EmailValidator source code. Let us introduce one more design change.

The code snippet above fulfills the OCP. The addition of a new email validation type does not require a change in any of the classes. Of course, the change will be required in the inversion of the control tool configuration. I do hope that the difference from the initial code form is visible. We started from a state where we have been afraid of new validation types due to the need to modify part of the source code and adjust the unit tests. We ended up fully prepared for new validation requirements and embracing them with grace!

You might argue that the initial form of the code looks simpler than the final form. And it might be true for certain scenarios. You should always consider the benefits gained from the following of OCP against the additional level of abstraction introduced by it. However, keep in mind the boost to the unit testability and the fact that writing a new code is usually safer, faster, and more fun than rewriting existing code. The benefits are even more remarkable when you find the possibilities of OCP application on more complex tasks. For myself, it is very rewarding when I design the component in a way to fulfill the requirements with support to extensions that are naturally fitting into the system. Lastly, if you look closely at the final shape of our email validation components, you might see that the validation flow is universal and does not need to necessarily apply only to the validation of emails. With clever usage of the generic types, you will easily be able to write a generic validation mechanism component reusable for all different kinds of validations composed from multiple validation types. The application of OCP often helps you to uncover these monadic algorithms which will help you to design your code in a reusable and declarative manner.

Let me wrap it up here. I do hope that you enjoyed the article, and it gave you some interesting topics to think about. Feel free to share your thoughts and experience in the comment section.

We are ACTUM Digital and this piece was written by Milan Gatyas, .NET Tech Lead of Apollo Division. Feel free to get in touch.

--

--