I have yet to talk to a developer that has told me that they were purposefully writing bad software. I think this is something that is part of being a developer, that you write software that is as good as you can possibly make it within the constraints that you have.
In our effort to write the Best Software Ever (TM) we read up on all the programming best practices: design patterns, refactoring and rewriting code, new concepts such as Domain-Driven Design and CQRS, all the latest frameworks and of course we test our code until we have a decent code coverage and we sit together with our teammates to do pair programming. And that’s great. It is. But it isn’t.
In my lightning talk for the PHPAmersfoort meetup on Tuesday, January 9th, 2018, I ranted a bit about best practices. In this blog post, I try to summarize what I ranted about.
Test Coverage
Test coverage is great! It is a great tool to measure how much of our code is being touched by unit (and possibly integration) tests. A lot of developers I talk to tell me that they strive to get 100% code coverage, 80% code coverage, 50% code coverage or any other arbitrary percentage. What they don’t mention is whether or not they actually look at what they are testing.
Over the years I have encountered so many unit tests that were not actually testing anything. They were written for a sole purpose: To make sure that all the lines in the code were “green”, were covered by unit tests. And that is useless. Completely useless. You get a false sense of security if you work like this.
There are many ways of keeping track of whether your tests actually make sense. Recently I wrote about using docblocks for that purpose, but you can also use code coverage to help you write great tests. Generating code coverage can help you identify which parts of your code are not covered by tests. But instead of just writing a test to ensure the line turns green, you need to consider what that line of code stands for, what behavior it adds to your code. And you should write your tests to test that behavior, not just to add a green line and an extra 0.1% to your code coverage. Code coverage is an indication, not a proof of good tests.
Domain-driven design
DDD is a way of designing the code of your application based on the domain you’re working in. It puts the actual use cases at the heart of your application and ensures that your code is structured in a way that makes sense to the context it is running in.
Domain-Driven Design is a big hit in the programming world at the moment. These days you don’t count anymore if you don’t do DDD. And you shouldn’t just know about DDD or try to apply it here and there, no: ALL YOUR CODES SHOULD BE DDD!1!1shift-one!!1!
Now, don’t get me wrong: There is a lot in DDD that makes way more sense than any approach I’ve used in the past, but just applying DDD on every bit of code you write does not make any sense. Doing things DDD is not that hard, but doing DDD right takes a lot of learning and a lot of effort. And for quite a few of the things that I’ve seen people want to use full-on DDD recently, I wonder whether it is worth the effort.
So yes, dig into DDD, read the blue book if you want, read any book about it, all the blog post, and apply it where it makes sense. Go ahead! But don’t overdo it.
Frameworks
I used to be a framework zealot. I was convinced that everyone should use frameworks, and everyone should use it all the time. For me it started with Mojavi, then Zend Framework and finally I settled on Symfony. To me, the approach and structure that Symfony gave me made so much sense that I started using Symfony for every project that I worked on. My first step would be to download (and later: install) Symfony. It made my life so much easier.
Using a framework does make a lot of sense for a lot of situations. And I personally do not really care what framework you use, although I see a lot of people saying “You use Laravel? You’re such a n00b!” or “No, you have to use Symfony for everything” or “Zend Framework is the only true enterprise framework and you need to use it”.
First of all: There is no single framework that is good for every situation. Second of all, why use a pre-fab framework when you can build your own?. And sometimes you really don’t need a framework. Stop bashing other people’s solutions and start worrying about solving your own problems. Pick the right tool for the job and fix stuff.
Event sourcing + CQRS
Event sourcing is a way of storing and retrieving data that does not hold a single truth. It uses events to communicate changes to your data. At any point in time, you can replay those events to get to the current state of your data, but it also allows you to look back into your history for other states of the data. It is a great concept for storing data where you need a paper trail (for instance for audit purposes) or where you need versioning of your data.
CQRS is a method of separating your C, R, U, and D. In most places where I’ve seen it applied it is a separation of reading data from the datastore and writing data to the data store.
Both are, like Domain-Driven Design, a big hit in the programming world at the moment. There’s a lot of fanaticism around it. Of course, you should do event sourcing, preferably on all your data. Of course, you should use CQRS, it is such a great way of separating responsibilities.
And while I agree with the arguments, I don’t think they should be applied to every situation. In many projects, a “traditional” relational database will work. Or the previous big hit, document databases, will work as well. And for your average project separating read and write are not a huge requirement either. Sure, it will add some structure to your code, but also some overhead while developing. As Martin Fowler puts it:
For some situations, this separation can be valuable, but beware that for most systems CQRS adds risky complexity.
Pair programming
Now here’s a programming practice that I truly love: pair programming. Sit down with another developer and start coding. One developer is the “driver”, they type the code and offer implementations of the road that the “navigator” lays out. The navigator sets next to the driver and comes up with ways of implementing the task at hand.
There is something about this way of working together that makes a lot of sense. My way of looking at a problem is probably different from the person sitting next to me, and by combining our approaches and picking the best of both world, the solution will be better than any solution our individual selves could’ve come up with.
Having said that, I don’t think any developer would say “yes, let’s do pair programming full-time”. Or if they do, they’re not like me.
Pairing full-time would exhaust me. When I do full-day pairing sessions (which I occasionally do) I am completely dead by the end of the day. When I do it a couple of days in a row, I need the full weekend just to recover from that, meaning I have very little time to actually do fun stuff. The amount of social interaction while pairing would kill me if I do it full-time. The intensity of pairing as well. Because pairing is intense. Instead of just having to think of your own solution, you now have to combine that with the input of the other half of the pair and together you have to decide what is the way to go. And there is such a thing as Decision Fatigue.
Instead, and I’ve done this several times to great success, you should combine pairing sessions with individual work time. Do pair programming for an hour, or maybe two hours, then split up and work on parts of the task individually, then come back together to combine your individual work. This still gives you the benefit of working together but won’t burn you out in two weeks time.
Refactoring + Rewriting
Refactoring is the process of changing parts of your code while keeping the outward behavior the same. It improves the code quality without impacting the code that relies on your code.
Rewriting code is basically refactoring without giving a shit about backward compatibility. It’s refactoring YOLO style. You completely replace the old code with new code, and the behavior of the code may change according to your wishes.
Depending on who you’re talking to, every bit of legacy code should be refactored or rewritten, as soon as possible.
And while I agree on the fact that we should refactor or rewrite legacy code, I probably disagree on the definition of “as soon as possible”.
Refactoring and rewriting code or great tools to improve the quality of your codebase and with that the quality of your application. They are extremely powerful tools, but with great power comes great responsibility. Given unlimited time and funds, I am of the belief that any developer in this world would continually keep refactoring and rewriting their code, and never ship a damn thing. Because as we develop our software and as we develop our skill set, we find out about new and different ways of solving the same problem. And every time we discover a fancy new way to solve a problem, all code we have written until then becomes instant legacy code. This is a never-ending cycle.
Legacy code is fine if it works, performs and is secure according to the business specifications and requirements. It is possible that, from a technical point of view, you may want to fix some issues that the code has, but there has to be a balance between delivering code improvements and delivering functionality. We should not refactor or rewrite parts of the code as we encounter them, but instead, keep track of what we have found in a central place and determine, in close collaboration with the business, what to fix at that time. If you really need a quick solution, you encapsulate the legacy with a small layer of better code. That way you can use the legacy while having a nice and “modern” interface to it.
Consider ALL THE BEST PRACTICES
All of the above examples are just examples of different best practices that you need to consider. When writing code, you should, of course, keep all the best practices in mind that you can think of. But there is no need to consider them all at the same time. Make a balance between code quality and speed of development, applying the best practices that apply to the situation you’re in at that point. The best practices are best practices for a majority of the situations, but they are generalized so as to apply to a majority of the situations. This also means they may not apply to your situation, or there may be more important things you should weigh in. So read up on all the best practices, keep them in mind, but think before you do. Apply the best practices wisely after weighing all the factors that apply to your situation. And please, please use your common sense.
Leave a Reply