Reducing cognitive load in your code
In the previous article I wrote about cognitive load during the development process in context of communication and human interactions. Today let’s have a look at the code itself and how can we make it better for ourselves.
The big ball of mud (or spaghetti)
We all worked on codebases which are literally not maintainable. Every small change brings more regressions. Small stories and tasks take ages as it’s hard to figure out how to achieve our goals or there is a need for dirty hacks to just make it work.
If we look for a reasons I like the saying that the quality of the software reflects the communication within the organisation as it proved itself in a few situations I’ve been. So after all it’s not as simple as writing good code but we can and we should make it as much as we can to make our lifes a bit easier.
First step we can make is to take DDD approach where we can, even if it’s not exactly business language we use, we make communication within the team a bit smoother.
IMO the most important thing is keeping things simple. If something is called within company as X let it be X, trying to introduce new concepts just because it’s a different context adds no value. With different contexts the entity can have the same name or, if you don’t like it, prefix name of entity with the context name. StockInvestor is better than Account as the name of entity.
There is a joke that every problem can be solved with another level of abstraction. If something is complicated and it’s hard to explain it’s probably too big chunk of a problem and there are some details missing from the picture. Adding more abstraction really can help as it creates new terms in the developer language and allows to explain things better.
Extracting abstractions not only helps with communication though. Smaller classes help with testing as it’s way easier to test small pieces of functionality. They also allow you to reuse them in a very readable way. Having a TransactionType as a separate enum is way better than few constants in your controller. It also communicates that TransactionType is somehow important in the context of the application.
Order of dependencies
After we named out classes and methods correctly and split them in digestible chunks we can take care of how we compose them together.
For me the most important concept is what I call knowledge graph. It comes down to asking yourself question if Component A really need details from Component B or perform some tasks.
As an example we can take a dinner in a restaurant. You are dependent on restaurant to make your food but you don’t need to know how to cook. The same other way around – the restaurant has a public contract – called menu – which you can use to order your dish. The information where they’ve bought cabbage or what type of mayo they used in the salad stays hidden.
In terms of software the question is on the side of – do I need those details to perform the operation or I’m adding it for convenience of querying or reporting? Often additional data aren’t necessary or all what’s needed is a reference, not the whole record.
On the topic of references it also applies to foreign keys in relational databases. We all love and use ORMs and enjoy calling Entity.getChildren(). It can lead to hard dependencies and making system more complex and potent to regressions.
To illustrate this we can look no further than a simple blog application where you want to show posts of specific user on a page. We could do ORM style query – User.getPosts(). This way we’re in the space of the knowledge creep. Posts page knows about User as an entity. Do we really need that information? What we can do is to query posts by User’s identifier. This way we can keep domain of the User as separate concept and avoid oversharing information.
Keeping standardized approach
It’s the part of software development process or code practice commonly used all around the industry and working well. For example semantic versioning is one of those standards. REST API can be another one in slightly different context. Same with design patterns and class naming. You may remember validator in a Factory class.
Long story short – keep things boringly obvious as much as possible. Why? Because everyone knows how certain things work and will assume your codebase is not different. If you change how obvious things work you’ll add a lot of confusion and immense level of translation layers needed to understand what is happening. On top of that you’re adding a lot of maintenance cost to the process as custom solutions need custom implementation therefor you’ll have to write them on your own.
Semantic versioning works and has a set of rules. Sometimes it’s decorated with 4th number containing build number and it’s tied to the functionality delivered by the program. If you’ll tie it to the business level versioning you’re loong any value of publishing the number as it’s not tied to the types of changes in the program and you’ll need to add annual process to make sure you’re deploying correct versions. If you really need business information attached to the code you can use separate metafile with all the details you need which won’t affect tooling expecting artifact version number to behave in a specific way.
As for REST API design I’ve written a big post some time ago how to design them well. In context of cognitive load, again, keep things obvious and try to avoid adding custom contract improvements. If you need, just call it a RPC service. I know RPC isn’t that cool. But if it’s RPC service, calling it differently won’t change the fact of itbeing a RPC service.
Last example is at the code level and touches common concepts in development. Validator is a very common pattern. Either you’re validating form or API call it return a list of violations. If the list is empty it means that data all alright. It’s not always a case though. I’ve seen validators working as entity factories, persisting data and routing events. Single responsibility principle comes to mind but also keeping the name of the class in line with this functionality.
Keep it boring
All of those will make your work boring. Boring in the good way. Not having to think about small things saves you time to work on important and fun problems. Therefore a decent amount of effort will brig you into creative laziness. It’s being too lazy to fix dependencies in 3 components because of clean and independent data structures. It takes time at the start but it will save you a lot time and stress in the long run.