One of the first lessons we learn as software developers is about code reuse. Every book, every piece of documentation, every blog breaks down code into small reusable chunks i.e. classes and methods. The reasoning behind the choices made are rarely discussed because there is an implicit assumption that everyone understands it. Writing the same code multiple times is considered bad, and so we organise it in such a way as to allow us to use it wherever we want. Even though we rarely plan to reuse the code at the time of writing, we have this thought that someone might want to in the future.
On the surface this seems like a really good thing. We want to make it easy for our future selves to make changes. The problem is that as a species humans are not good at predicting the future. We abstract code out into classes in an attempt to future-proof it, but the imagined future rarely happens. As a result we find ourselves working in code with hundreds of service/utility/helper classes containing single-use pieces of functionality and code that is usually nowhere near as easy to understand or change as we first thought.
So what can we do about this? I would suggest that we need to rethink the way we approach writing code, based on the following principles:
As developers we have been conditioned to organised code according to its place in the architecture. The project templates in Visual Studio come with folders named with things like Views, Models, Controllers. Developers tend to follow this pattern, adding other folders named Services, Repositories or Utils. Consider what this means for a typical piece of functionality in an API e.g. an endpoint to add or update a customer record.
In the .Net world, the common approach would be to add the endpoint to a class called CustomerController. This class may call a method in the CustomerService class which will then call the CustomerRepository to write the data to a database. Along the way it may use a CustomerValidator and a CustomerMapper, or even a CustomerHelper.
Each of these classes will be in different folders and the developer will need to hunt through the codebase to find them. Most of the methods will be used in exactly one place, although of course they will be reusable, because someone might want to use them in the future.
Notice how all these classes are named CustomerSomething. This is to help developers find them amongst all the other classes.
Also while it might be clear what a CustomerValidator or CustomerMapper does, what does a CustomerService do? Or a CustomerHelper?
This way of organising code is partly a result of the desire to future-proof and make code reusable.
An alternative would be to have a folder called Customers which contains all the functionality related to customers. This where the controller could be, or if using a library like Ardalis ApiEndpoints there might be a class for each endpoint. A good way to start would be to put all the code to process the request into a single method.
In the .Net world at least, I’ve observed that very few developers practice Test Driven Development – although everyone agrees on the importance of tests.
If there aren’t any tests already, this would be the time to write the first one. Or even several! These tests should cover the full functionality of the endpoint.
Having written some tests it may make sense to abstract some of it out into other well-named methods for readability.
Having followed this approach all the code related to adding or updating a customer would be in one place which is easy to find. There wouldn’t be classes vaguely related to customers scattered over the codebase.
As we continue developing some of this code may turn out to be reusable and then it can be abstracted into classes. The tests will enable refactoring to be done safely because they cover the whole endpoint.
Another word for future-proofing is guessing. When we do this we often get it wrong. We write more code than is needed and abstract ideas prematurely. Code becomes harder to reason about and harder to change. Some people might call this over-engineering. Ultimately this leads to bugs and longer cycle times.
As a reminder here is how I would define the path to success: