There are three basic approaches to writing software that I know of. The first is always strive for technical excellence. This is great if you can do it – I’ve never worked in a place where technical excellence was always priority #1 with no compromise. If that’s you, then you probably don’t need to read this.
The second way is throw code around and hope it works – this is far too common. You look at the problem in front of you, and you grab a solution that’s good enough for now, doesn’t blow the budget (time and money) too badly, and leave the mess to be cleaned up (or worked around) later. Industry wide, this is the norm – a consequence of always focusing on the short term.
The third is a pragmatic compromise – try to make it as good as possible, while recognising that parts of the system will be, well, shit. That’s what I want to talk about here.
Let’s get something straight here. As a profession, a huge amount of code that we have written is, frankly, shit. And some of the code that you personally have written is especially shit. Yes, you in the back – your code reeks. And this is okay. Seriously. By and large, we’re not paid to produce brilliant elegant designs, perfect in every detail – we’re paid to produce shit that works. Shit is incredibly useful stuff. Most of the plants in the world wouldn’t grow if they didn’t get a regular supply of shit, and software systems need it as well. As long as you can use the shit code you produce as fertiliser, things work. The trick is to avoid delivering shit that is nutrient poor – because that doesn’t help anyone.
The above image illustrates this perfectly – all code has a number of “what-the-…” moments when you read it. This is because all code is shit. This is such a perfect summation of the industry that Bob Martin used the image shown above in the introduction to his book, “Clean Code” (a truly excellent read, BTW. If you haven’t read it yet, then stop wasting time reading my blog and go and read his book. Now!). So then – why do we produce shit code? And how do we make sure it works?
In my opinion, the second biggest reason we produce shit code is that we cave to schedule pressure. We start projects (oh, and projects are an anti-pattern!) with good intentions, all primed to make this one be the one that’s different. We get a bit of the way through, and then we realise we can’t deliver the perfect solution we thought of in time, and we scramble for anything that will do the job. We cut corners. And, eventually, we cut a corner so deep that the leg falls off. We end up with messy code, in a messy project, that delivers a messy solution. Very often, the shortcuts we take become the very reason the project is late – tricky bugs crawl out from between the hacks and slow progress to a crawl. And if they don’t kill your current project, they stand a good chance of killing the next project.
[Side-note: The biggest reason we produce shit code is that we let junior developers write too much without supervision, partly because there aren't enough senior developers to provide it, or the senior developers are overworked with other tasks. Junior developers working unsupervised will produce epic quantities of shit code - very clever shit code, in many cases (because most junior devs are clever people), but still shit. This is not the fault of the junior developers - by definition, they are inexperienced, and learning to write good software is a decade-long task at least. Heck, I've got 14 years of commercial experience, and I'm still learning how to do it. It's the fault of an industry that doesn't value training and experience enough. Fortunately, this can be managed, allowing the juniors to be productive and contribute - and not just around the edges. Pairing is one of the best ways to both upskill junior devs and to get massive value out of them]
Schedule pressure happens. It’s inevitable, so get used to it. Project managers are
idiots who don’t value engineering brilliance people who are juggling lots of different priorities, and your engineering priorities are only one of them. Most of the time, project managers will make the right call – for the project. Some of the time, they’ll get it wrong. It happens. And sometimes there will be differences of opinions about this, and some of those times you’ll be the wrong one. The point, though, is that you will have to compromise on building the best thing and instead build something “good enough”.
So, how do we do this? What can we do to produce “shit that works”, while meeting the project deadlines (or not exceeding them too badly), and acknowledging the commercial realities that drive the business? Well, I can’t say I’ve got all the answers, but this is what I try to do…
- Try and understand the project. Spend a bit of time on this – but not too much. It’s going to change, anyway, so don’t get bogged down in the details at the start, but spend enough time that you think you’ve got a good grasp on what is important to the project and what is less essential.
- Come up with a high-level design. Again, don’t spend too much time on this – not only is the problem going to change, but you’re going to make mistakes in the design that you’ll need to fix. The goal isn’t perfection, but to get an understanding of what you’re going to try and build. You want to get a “right design” – there will be more than one, but don’t go trying to get the “best” one. You’re not going to build that one, anyway.
- Test your design against good design principles. Here’s a quick summary of things I look for:
- Do you have any assumptions that are really wide spread? Pay special attention to your data model here – mistakes in this area can be expensive.
- Are the components of your design loosely coupled? If you were to make a serious change to one of them, would that have a large ripple-effect through the rest of the system?
- Is the system able to evolve over time? Can you upgrade it in place?
- Work out what parts of the system are the most important – in the long term, not necessarily just in the scope of the project. Get agreement on this with the project managers and relevant stake holders up front.
- Decide in advance what and how you will react when – not if – you are forced to sacrifice overall quality to deliver on time. In an ideal world, you’ll do this by cutting scope and dropping features. In the real world, you’ll also have to sacrifice quality. Work out where you are going to do that, and plan for it. Don’t plan to sacrifice quality in the really important areas.
- Make sure the tradeoffs that you do are worth it. In particular, ensure that any given tradeoff makes the rest of the system easier to build – or, at least, no worse. Be very skeptical of any tradeoff that makes things harder – you’re doing them to save time, and you want to be very certain that they will save you time, at least in the short- to medium-term.
- Remember how the system should be loosely coupled? Don’t make tradeoffs that require large scale changes through multiple components. If a tradeoff for quality has consequences like that, then you’re either making the wrong tradeoff or you’ve designed the wrong thing.
- Firewall all tradeoffs off from the rest of the system. Put up big visible signs to indicate that this is a danger area.
This means, at the implementation level for example, that you don’t put a hack into a class – spin the hack off into its own class. Put up an interface that looks right, but name the class in such a way as to make the shitty nature obvious. Never ever mix “clean” code with “shitty” code at that level – always do it at arms length, with your nose held shut. And wash your hands afterwards. Remember, it only takes a little bit of shit to make a nasty smell.
- Always plan how you’ll fix this afterwards. You’ll probably never get to do it, or if you do the situation will have changed so that your plan is irrelevant, but by planning to fix it, you’ll know you can. If you’ve built the system to be loosely coupled and to allow for evolutionary change over time, then the cost of fixing the problem shouldn’t grow over time.
- Build the most important things – the things you decided you wouldn’t compromise on – first, at the start of the project. Leave the things you’ve agreed to sacrifice until later.
- Finally – build a thin slice through the entire project ASAP. Get end-to-end going – with something going through all the components you want to build – as a high priority. Then expand out those components incrementally. If you try and build your project in layers (e.g. build the backend, then build a service layer, then build a UI), you’re going to screw it up. And, most importantly, you won’t be delivering those “most important things” first.
Never ever ever tradeoff the things that will make it easier to fix later. So you never tradeoff the ability to refactor, which means you never tradeoff unit tests, or clean interfaces, or loosely coupled code. You can tolerate an amazing amount of shit in the codebase if you can have confidence that you can change it later.
Also, remember that you are going to be spending a lot of time in the shit code. The code that works well and doesn’t need to be changed – you won’t go back there. Shit code breeds defects – so you’re going back to it a lot. Shit code isn’t flexible – so you’ll be needing to change it regularly to cope with new project requirements. Shit code is harder to understand – so you’ll be spending more time reading it, and more time working out how to change it. Shit code is harder to evolve – and in many cases there won’t be enough time to clean it up properly (or you won’t think there is), which means that the necessary changes are more likely to be done in a shit fashion. Shit code ferments.
This is why you need to put up warning signs. People reading the shit code need to be told it’s shit – they need to know that the code shouldn’t be used as an example. This is especially true for junior developers – we learn from example, and because of all of the above, the junior developers (like the senior developers) will spend far too much time in the shit code. It will become code that the developers know very well, and unless they know that the code is shit, junior developers will copy it. Always always always flag shit code.
This is even more important with shit code produced by the guidelines above – because it won’t always look that shitty. If you’ve done a good job up front working out where you’ll compromise the design, you can cut corners and end up with a three legged table. This can look beautiful, and even have some advantages (a three-legged table won’t ever be wobbly, for example). Do it right, and you’ll have something you’ll be satisfied with forever. The trick is to know when it’s time to clean up some of the shit you’ve made – when you need to reverse an earlier decision to sacrifice quality. This will usually be in response to a change in requirements (e.g. the three-legged table isn’t as strong), but if you’ve done your job right, then it shouldn’t be hard to clean up and move forward.
(If you have trouble identifying the shit code, look at where you spend most of your time. Do some analysis over your version history – see which files have the most commits in the last year. The code that gets worked on a lot is highly likely to be shit code)
Finally – don’t get discouraged. You will spend a lot of time in the shit code – more than you should, especially by proportion to the size of the code. This will probably get you down, especially if you’re like me. Just remember that the bulk of the code – the code you don’t go back and work in constantly – is probably pretty decent. And heck, maybe if you can clean up the code you work in constantly, then maybe – just maybe – all of the code you work in will be clean.