Design done right

In my first year of University, when I just started to know how to code something more than hello world, I spend countless hours working on projects that never led anywhere but were just for fun and a way to learn and practice coding.
Every time I had in mind something code, from a library to a small project, I immediately jumped in with both feet and started writing code. Needless to say, after just 30 minutes I was deleting lines, moving functions from one class to another one, and heavily refactoring code that I wrote a few minutes earlier. This not only slowed me down terribly but sometimes was discouraging me enough to just give up on the idea and move on to something else.
I don’t know if most people experienced this too in their early days, but I did, so I want to tell you what I learned and how I tremendously improved my productivity. Learning more about the language itself is a hard requirement to be proficient and productive during development, but I will not cover it here, I will focus more on patterns and small tricks.
Create a mental map
Probably the absolute most important step. Initially, it can be even more useful to create a schema, such as UML, but with time you will see that this step can be skipped.
When designing your architecture, you should take into account the following parts:
- The components: in a OOP you can think of it as the classes, but it can be any abstraction really. These will be the main actors of your software, so focus on the most important one, you first have to get those right.
- Their interaction: basically the arrows in the UML diagram.
- The flow: once you have the main components lied out, you should look at the flow of information and how the data passes across the components. More about this later.
It’s all about the flow
What is missing from a UML diagram is the execution flow. For execution flow, I mean the order in which each component is called in order to perform a certain elaboration on the information provided. This of it as the orchestrator in a distributed system where each node is nothing but one of the components above. For instance, in a web server, you will have a flow for each endpoint or a flow for each subprocess in a more complicated system.
Reuse as much as possible
This is a well-known tip for any software engineer, and it’s also known as DRY pattern. However, I want to push it even further. You should not only avoid repeating yourself but also avoid repeating others. With this, I don’t mean to use libraries if available (covered later), rather always look for a smart and creative way to reuse other components that might not have been designed for that specific use.
As a simple example, let’s suppose that you need a queue in python and there is no library available, so the first reaction would be to start and write one from scratch. What you can use instead is the standard list.
queue = []queue.pop()queue.insert(0, 'something')
In this way, you saved a dozen lines of code, hours of development, and hours of maintenance. Of course, this is a stupid example, but you get the idea, that there might be a way to use existing code that has not been designed exactly for your need. Needless to say, always take complexity and potential latency into account.
Go test-driven
Yesterday I was working on some refactoring of a 3-year-old service, specifically, I was changing a lot of things under the hood. While the majority of the code was very well tested, the main thread, which takes care of fetching messages, parsing them, and passing them through the pipeline, was not tested at all. So I simply decided to write some tests to be sure that the changes I was making were correct. However, when I started writing the tests for the main thread, I soon realized that it would take me ages to write the tests because the function was quite complex and doing a lot of things at the same time. So, instead of spending hours writing ad hot tests, I simply broke the code down into smaller and simpler pieces, that were extremely easy to test. The result was a 6 lines test, and 15 minutes refactor.
One can talk about SOLID principles and other designs pattern forever, but really understanding them and interiorizing them takes time and it’s not always straightforward.
“If it’s too complicated to test it, break it down or refactor it”.
There is (almost) always a library
One of the stupid errors I did countless times, was to be stubborn enough to think that my implementation would be better, that it would give me the control to do anything I needed. Spoiler: it’s almost never true. Yes maybe with time I will write software better than some other people, but what really matters is that it’s not worth it. Trust me, if you think that you need to custom write any library, it means that the overall architecture is wrong. Other people put much more time and effort into writing a library that you would use in a single line and nowhere else. Don’t be proud, but be smart and always look first if there is already a good and maintained (very important) library, and chances are that there is already (given that you are not working in a hell of a language).
Learn more design patterns
Given all the tools I just described, one cannot avoid learning new design patterns and paradigms. You don’t need to learn them all but starting with the most widespread should be enough to be able to deliver high-quality software and work with extensive frameworks on challenging projects.