Today I want to talk about an aspect of our work that is very important to me and something which separates mediocre from professional developers: craftsmanship. What does it mean to possess this virtue? How can we show it in our work? And how does it help improve our code?
It is quite straightforward to show craftsmanship if you perform physical labor because the details of your work can be easily inspected. For instance, a lot of electricians like to adjust the screws in the wall sockets so that all grooves are lined up vertically. This shows that they care, that they have paid attention to detail. A lot of their work is hidden behind that socket, so the least they can do to show they cared is to line up those screws. And whenever someone notices these details, they will immediately have more faith in the quality of the work they can't see.
It is not that different for us, developers. Most of our work is hidden inside code bases which are only seen by other developers. Still we need to show our proficiency and leave a good impression. If not for others, than at least for ourselves. Even a job we don't like can offer us some fulfillment if we do it well. So what we can we do besides writing code that does the bare minimum and just works?
Design and architecture
A clean code base is like a well tended garden, there are no weeds, the grass is cut and there are dedicated places for the different herbs and flowers we want to grow. In our code we can achieve this goal with a well thought out design and architecture. There are many design patterns we can use, such as those mentioned by the "gang of four" and principles like "SOLID". Don't fret about having to know all of them by heart, it's sufficient to know that they are available in our toolbelt and we can use them where and when we want to. Like trees and hedges, functions and classes can grow out of control so make sure to trim them in time. Functions and classes should do one thing, they should do it only and they should do it well.
There are many different kinds of testing, but for this article we will focus on unit testing as that is the level of testing that developers mostly deal with. A good code base is well tested; strive for 100% coverage without worrying about ever getting there. 100% test coverage is like the pot of gold at the end of the rainbow. My experience is that going from 90% to 100% coverage offers little substantial gain and can take about as much effort or even more as going from 0% to 90%. Don't overvalue coverage, but do not undervalue it either. Obviously 0% coverage is awful and doesn't show that we care. In fact, it shows we don't care at all about the maintainability of our code base. Conversely, 100% coverage doesn't automatically mean our application can never fail. We should strive to have meaningful coverage.
If you have a test which covers 1,000 lines of application code, but doesn't assert or verify anything, you will have great raw coverage, but 0% meaningful coverage. The purpose of a test is to break if the code changes or doesn't do what you expect it do. A test which can never fail is useless, delete it and add a proper test instead. Each time a test breaks for the right reasons, don't agonize, rejoice! Increasing meaningful coverage can be achieved by utilizing frameworks which perform mutation testing. Mutation testing messes with your application code to see if any tests fail -- they should. When you have a method which calls another method, what should happen to your test if the return value of the called method changes? Your test should fail, right?
Focus your initial testing efforts on the roads most travelled. Many applications have certain code paths which are taken much more often than other paths so it's good to focus on them initially to make sure the most critical parts of your application always work. This doesn't mean you should never test theoretical edge cases in dark corners of your application, but your return on investment for those is far lower. Cover your bases first, then cover the rest.
How thorough you should test code and validate data depends on your level of trust. For starters, you should trust yourself and your own code. Assume that the methods you create return the values you expect. You don't have to add nullability checks everywhere in your code if you never return null, it would just clutter your code. You can add nullability annotations to assist the compiler in reasoning about nullability or try to let the compiler infer it.
You should be sceptical about third party libraries, but if the libraries are well known and used by many developers it is safe to assume that the developers of these libraries have tested their code and that it is perfectly fine to use them. If you use a well known logging framework, don't waste your time checking whether each line is actually written to
stdout. Your tests should only check that your code uses the library in an appropriate manner. For example, if you use a metrics library, it might be useful to verify whether a library method is called with appropriate parameters so that no metrics are lost when this code is accidentally changed.
Tests act like a magnifying glass for your code, revealing hairy details, for instance whether functionality is well isolated and doesn't have too much responsibility. If you need to mock 20 different classes and / or methods to make a test work, your code could probably use some refactoring. If you find you need to change your code a lot just to be able to test it, well, that doesn't bode well either. Tests should assume nothing about the inner workings of your code and use publicly accessible entrances and exits only. Private methods should be covered by being able to access them through non-private gateways.
Enough about testing, let's talk about something else which is always very exciting for developers: documentation. No developer likes to write documentation. Many of us enjoy this profession because we like building things, not because we like describing how we built them. It is a necessary evil, because other people than ourselves (or ourselves in the future) need to understand what we have done. This ranges from code comments to user manuals.
Some zealots may say that code comments are inherently evil, because instead of writing comments, one should just write readable code. While you definitely should attempt to write code which can be easily understood, sometimes you still need comments. Comments are a great place to tell about the 'why' instead of the 'how'. The 'how' is the code itself, but you can use comments to explain things like "we attempted X, but that didn't work so we decided to do it like this" or "this external method does something strange, so we had to do Y". Write comments when the actual behavior differs from the behavior you would expect from just reading through the code. Do not write
TODO comments, create tasks in your issue tracker instead to make sure you will eventually resolve those issues.
Keep your documentation close
Having no documentation at all may be bad, but having antiquated documentation which is no longer correct may be even worse. It leads to false expectations and frustrated users and colleagues. A helpful method to prevent documentation from going stale is to embed it in your code repository. When you implement a new feature or change something, you can easily update the documentation as well, it's in the same code base after all. If you ask your colleague for a code review, they can review the documentation at the same time. There are many tools which can parse this documentation and present it in a readable manner.
Visualizing data flows can be a powerful way to document your application. You can use something like the C4 model to start from a bird's eye view and zoom in to various levels of detail. Conveying complex ideas and interactions between parts of a system or application is just a lot easier with the proper diagrams. It helps both the people inside and outside your team to understand what your application's role in the ecosystem is and how it should work.
There are of course other aspects we haven't discussed in detail, but which are also important, such as proper version control, automation with a CI/CD pipeline or keeping your dependencies up to date, but I consider these more of a given than the points I mentioned.
We developers can do a lot of things to show our craftsmanship, from writing clean code to proper testing and comprehensible documentation. There are always improvements we can make to increase the quality of our software. If you find yourself wondering whether something would be worth the effort, just use yourself as a reference. Not yourself right now, but yourself in one year. Imagine you are looking at this code base one year from now, what would your first impression be? Would it be "which idiot wrote this code?" (to which the answer is all too often 'yourself') or "wow, I can't believe the quality of this code base, it looks like someone really cared".