What Does Design, Maintainability, and Testability Have to do with... Food?
Hi Friends.
There’s a common saying that everyone has a unique programming style, and that we leave a ‘signature’ in the code we write. I’d agree that people often leave clues, though I’ll also say that the way I write code has changed for me over time. I wanted to look at one of the more fun ways to describe general coding structure this week.
And it has something to do with food…
Spaghetti Code
Let’s start with a term that I’m sure we’ve all heard before: Spaghetti Code.
When you look at a plate of spaghetti, it’s difficult to see where the beginning and end of each strand are. You might be able to pick up a few and give them a gentle tug to find out where lead. However, there’s also a chance that doing so could cause the strands to break.
And this metaphor carries over to code.
When I first learned to code, I’d put everything in one file. On the course I was on, we were sometimes given templates where we were asked to fill in missing algorithms and implementations. While they seemed it at the time, the programs I wrote during my studies weren’t particularly large or complex. But habits tend to stick. And as I progressively wrote larger programs, I continued with the pattern I knew.
Moving between methods in large code files can involve a lot of jumping around in the editor. This can make things difficult to follow – think about being on a screen-sharing session where a remote colleague is doing a walkthrough and continually jumps to different places within the code.
Another thing that can complicate things is use of state. This can include class-wide variables and those of even broader scope (e.g. application-wide variables). Don’t get me wrong: using state is essential sometimes. But if we pass in exactly what’s needed for a method, we no longer need to:
Remember values for variables that are declared in other parts of the file (or potentially in other files).
Worry about external factors that could modify the variables, especially in multi-threaded environments.
By keeping a method’s logic and variables close to each other, the code becomes easier to read: the phrase ‘out of sight, out of mind’ no longer applies.
Code with less structure can still fulfil its purpose. However, fixing bugs and adding new features can become increasingly challenging or even risky. With everything intertwined, we need to make sure that new changes do not break existing functionality. This is difficult to know if the project has little or no automated test coverage, which is also hard to produce if the code is not written in a modular way.
Lasagne Code
I eventually came to work on backend code alongside others in a professional team. I started noticing patterns that were new to me, that other software developers had already ingrained into the codebase.
Requests would first be received by API controllers. They would then pass through multiple layers of services (classes containing related methods) until they reached their destination: this was usually a repository, where data would be written to either a cache or database.
This was the first time I encountered a separation of responsibilities that grouped related logic into ‘themed’ services. It was one step closer to the Single Responsibility Principle (SRP) – which essentially suggests that each class should have exactly one purpose – than I had been at the time. I liked this structure. It felt more refined to me than my previous implementations of the MVVM pattern (Model, View, View Model), where I had put everything that wasn’t UI markup and model objects into a single view model.
Much like a lasagne, the code was structured in layers. But the layers themselves were impossible to separate. The various service classes were large and generally interdependent. They also made occasional use of state. This made tests difficult to write because the setup portion for each test quickly became long and complex.
Ravioli Code
A few years later, I decided to learn Test Driven Development (TDD) in an effort of self-improvement. The fundamental concept is very simple:
Write a test.
Add any missing models/structure to allow the code compile.
Run the test; it should fail (‘Red’).
Write the minimum amount of code required to make the test pass (‘Green’).
Refactor the code, ensuring all tests continue to pass.
In essence: ‘Red, Green, Refactor’.
The difficulty comes in designing software in modules that fits this philosophy. My initial attempts resulted in tiny classes. Some would contain only a single small method, arguably taking the SRP to the extreme. While this coding style gave me more confidence about my changes (through the additional testability), it introduced two notable negative side effects:
My projects’ file counts skyrocketed. This made the projects difficult to navigate and manage.
The individual classes felt fragmented and isolated. They had lost the sense of cohesion that larger classes have.
Much like ravioli, my code consisted of small and self-contained packages. Without looking deeper, it was difficult to see how the classes were related.
Summary
Like most things in software development, good code structure and class design involves finding a compromise between many factors. Ideally our code should be maintainable; this eases the process of fixing bugs and adding new features. Having a suite of automated tests gives us a safety net for these activities, as well as refactoring; it might not catch everything, but it gives you more confidence in the latest changes.
While software should be designed with testability in mind, we should also aim to achieve high cohesion and low coupling. Code with everything intertwined is sometimes referred to as Spaghetti Code and has a high coupling factor.
When we start applying the SRP, we can come to a design sometimes labelled Lasagne Code. Here the coupling is loosened but still too strong, resulting in layers that can’t be meaningfully separated.
However, applying the SRP too strongly can leave us with a structure sometimes called Ravioli Code. Here we achieve low coupling, but we (undesirably) lower cohesion too. We end up with code where it feels unclear how the pieces fit together.
I would highly recommend trying TDD if you haven’t already. Even if you end up not strictly applying its principles every day, it will offer you insight into many things including code modularity. And once you’ve seen the range in class design possibilities, you’ll find getting the balance for optimum program design so much easier.