Focus on the problem, not on the solution: Demystifying software architecture
At the time there was a lot of “design is subjective”, “design is a matter of taste” bullshit going around. I disagreed. There are better and worse designs. These criteria aren’t perfect, but they serve to sort out some of the obvious crap and (importantly) you can evaluate them right now. The real criteria for quality of design, “minimizes cost (including the cost of delay) and maximizes benefit over the lifetime of the software,” can only be evaluated post hoc, and even then any evaluation will be subject to a large bag full of cognitive biases. Kent Beck [1]
One of the steps that makes a quality upgrade in the career of a software engineer is the use of design patterns. When you start reading about paradigms like SOLID, DRY, sophisticated design patterns like the ones exposed in the Design Patterns by the GangOfFour, or anti-patterns like STUPID you want to somehow apply them the more, the better. In addition, there is a certain aura about those software engineers who use these patterns, being almost like irrefutable knowledge.
It makes no sense to discard this knowledge at all, but might be good to demystify it. It’s useful to solve many complex problems, but let me change the perspective thinking on what we want to achieve and not how we want to resolve it.
We must not ignore the fact that design patterns introduce accidental complexity and here is the key: how to balance the benefits against the complexity involved in their implementation and subsequent maintenance.
We should not think about applying patterns or building architecture in an elegant or sophisticated way. Likewise, it is a mistake to think of a use case and its hundreds of edge cases. Nor is it a good idea to develop thinking about a future that may never come.
The first goal and what we have to keep in mind when building a solution is to make friendly code. You are creating a friend for life. Life does not give you these opportunities, so think that tomorrow you will see him again, and you want to continue having a good time with him!
This can mean a lot of different things, but to be more specific: if I had to choose when it is necessary to apply or rethink an architectural change in my code, I would be guided by the following points.
Chain effect
When in a code, a modification involves changing other scattered lines of code for other functions or files, it is a clear sign that we are generating a coupling problem. This problem is further aggravated when that modification point is a central point of our domain since we are constantly going to have leak points in our code. This is very common with module functions like utils, or very general classes in OOP.
This problem is relatively easy to detect if we have a good test suite. If every time we make a change, tests that aren’t related to the current use case break, we have a high coupling in our code. This usually happens through abusive use of abstractions or by indiscriminately applying the DRY pattern.
A rule of thumb would be, to create an abstraction or reuse code when the case is clearly boilerplate code or similar when this case doesn't include edge cases for every function that uses it and especially don't create anecdotal abstractions.
Intuitive tests. Easy to create and easy to read them.
An application is well built and well-architected when it is easy to build tests on top of it. And I mean to build readable tests in a simple way. With the introduction of certain design patterns or frameworks to develop quickly, a minimum separation of layers has been avoided.
That is why the TDD mantra is important because it helps define simple tests and is built with that layer of independence in mind. It is also important to test using as few complex tools from the test suite as possible. By this, I mean not creating mocks with sophisticated testing tools but using our layer separation.
Focus on testing your use cases, because are the real core of your application. Think about tomorrow, you will be grateful if you can go to the suit test and know all about your use cases with just a glance. I know, it’s a dream come true!
It is also worth mentioning, that while TDD is a perfect methodology to keep your architecture in good shape (by constantly refactoring and not anticipating designs), tests written through TDD should be taken as tests that guarantee that your code works as you expect, not as tests that validate that our application is bug-free.
A code review should start with the tests, from where everything that our code does and/or does not do is documented. That is why the tests are the index of our code and sometimes are called specs.
Pay special attention when code is reused
Reuse, either through abstractions or with general solutions, causes us to design excessively complicated solutions. Initially, it may seem many times that we are violating the DRY principle and we seek to repeat the less code the better. Also, Kent Beck talks about no duplication. But wait a moment, don’t get confused about that.
The reality is that many times a generalization is created through an anecdotal case. No duplication is not about code lines, is about use cases or business logic.
We tend to think of grouping similar code, the more general the code, the more I can reuse it. The reality is different, the code must be adapted to the particularities of every submodule and that makes it hard to maintain.
The code is read by other colleagues in an easy way
When Kent Beck developed Extreme Programming back in the 90s, he already mentioned that we should keep in mind that the code reveals intent.
This means that the code is easy to understand. Readers of your code must understand what the purpose of the software you have written is.
It may look easy, but it is actually the vaguest and most difficult to apply. This is because it does not depend on us but on others. When someone does not understand how we solved the problem, it may be a good reason to rethink our solution.
That is why it is necessary to establish a good dialogue with the reviewers, a dialogue of trust and openness to change.
On the other hand, let the code rest. The code does not look the same after a day. Reread your code and try to understand your past self.
Code smells
We can make our code review more complex and go a step further through code smells. Code smells are a compendium of "smells" that Martin Fowler compiled in his excellent Refactoring book.
With those smells, we are able to detect possible problems when we look at a code. As we have already mentioned before, they are assumptions and not axioms so not always a code smell will run into a bad code.
In software development, it is not about seeing beyond when solving a problem, but about solving the current problem in the most understandable, intentional, and testable way. Focus on the problem, not on the solution.
Bonus point: Reviewers vs. pairs
During this article, we talked about PRs and reviewers to make it easier to communicate our points. But maybe reviewers could be avoided. What if pair programming our colleague acts as our reviewer too?
We’ll talk about this in a future article, stay tuned!
Founding member of The Crafters Lab
Rubén is a software developer and founding member of The Crafters Lab.