We’ve all been there: having to rush something to production to meet deadlines, customer demands, management’s whims, [.AddRange(your own List<ReasonToRushCode>)]…
How often do we look back (way back or just back to the last piece of code written) and feel proud of what we’ve produced? It’s not about it being properly tested and running as expected, as those should be a given, but knowing you’ve done your best in the ways of clarity, readability, maintainability, testability and all other ‘bilities you can think of. It’s also about knowing your own limitations, having submitted your code to reviews by your peers, accepted suggestions for improvements and learned in the process.
As an exercise, I prompt you to look back at some of your recent developments and ask yourself: “Am I Proud of what I’ve produced?”; “Would I be comfortable enough to share it publicly?”.
Less than optimal code
There’s quite a few reasons to produce less than optimal code, if there’s ever such thing as “optimal code”:
- Rush to meet tight deadlines
- Existing code
- Low (team) standards
- Lack of knowledge
- Utter laziness
Also, “good code” is a very subjective term, often influenced by personal judgement and style but we generally know good code when we see it.
The danger of over-engineering
This Pride must, however, consider the inevitable constraints that surround software development, like time and cost. As engineers striving to deliver the best possible solution, it’s too easy to fall for the trap of over-engineering solutions by injecting unneeded patterns and practices from our years of experience or learned about in books or online courses. Truth is, we can’t always have it our way.
There’s a thin line between delivering an elegantly engineered working solution and an overly complex one that takes many times longer than needed to be completed. Experience is the main source of wisdom to find the best balance.
There’s a thin line between delivering an elegantly engineered working solution and an overly complex one
Aim for simplicity
As we all know, software development is a highly creative function and we often must build new stuff from the ground up or come up with clever solutions to new problems, no matter how hard we try to reuse code and libraries. It sure helps to think and devise good plans but there’s no chance we can account for all variables and future needs, and that’s why we should, first and foremost, keep things simple as much as possible. As someone from the fifteenth century once said, “Simplicity is the ultimate sophistication”. You may have heard of him: his name was Leonardo da Vinci and he knew a thing or two about designing stuff.
As our knowledge builds up, it’s easy to disregard simple solutions. This is because we tend to try and employ that very knowledge, sometimes to the point of showing off.
As our knowledge builds up, it’s easy to disregard simple solutions
Iteration is one of our best friends in achieving a well-engineered solution. Usually our first iteration isn’t the optimal one. That’s Ok and even desirable. Evolution is Nature’s example of this very principle. Plus, we all know “Premature optimization is the root of all evil”.
When we have no option but to compromise on any of the above quality vectors (those ‘bilities), there must be awareness, and measures should be taken to make sure these trade-offs are addressed at a later stage. These compromises even have a name and, if you’ve been in the business of software development for some time, you must have heard of it: technical debt. Just as with any other kind of debt it’ll chase you up and catch up with you at the least desirable time. There’s plenty of articles on tech debt online so let’s not disperse ourselves.
Iteration is one of our best friends in achieving a well-engineered solution
A path to Pride and accomplishment
As someone who has done some things in a multitude of environments, I’ve come to have my own set of steps to ensure you get to the end with a sense of accomplishment. Here are a few I wanted to share:
1. Gather the critical scenarios involved
Nowadays we’re all “agile”, meaning our processes embrace change that inevitably can and will come. While we generally no longer write huge volumes of requirement documentation, knowing at the very least the most critical scenarios and use cases will help determine potential and actual points of change and design accordingly. There’s likely little advantage in designing for eventual scenarios that may never be required. This is known as “YAGNI” (You Aren’t Going to Need It).
2. Agree on and document external contracts
As far as your users or consumers are concerned, your code is only as good as what consumers perceive from it. You can have the most well architected solution in place and still fall short of expectations. Your contracts are what consumers know about you and they should be at the forefront of your initiatives. While that doesn’t mean you should concede to every request, setting them at an early stage can save you lots of trouble later and help avoid further rewrites. There’s a whole methodology on this: Spec Driven Development.
3. Write a working solution
Leverage all resources and plumbing already available on your project to streamline development of the simplest working solution. This “quickie” is no excuse for sloppy code or lack of unit testing but refrain from prematurely optimizing. Ideally, this won’t be your final solution, however, if urgency comes, you now have something to go live with that fulfills requirements. You’re not proud but it works. Be sure to not let this tech debt fall into oblivion.
4. Write your acceptance and integration tests
These will guarantee that the agreed contracts are honored, and the app/service behaves as expected. They will also ensure that whatever changes you later make inside the project will not break
5. Iterate on refactoring your solution
Now that you have a functional baseline is time to improve all those aspects you left behind for the sake of functionality. This refactor is not meant to be a performance oriented one but rather address adaptability, maintainability and readability. However, any obvious, uncostly performance optimizations should be done at this stage. Take the chance to apply those SOLID principles and design patterns but be careful not to overdo it. Don’t go and add factories all over the place just because. Beware that, consequently, you’re likely to have to also refactor your unit tests as you’ll probably segregate some classes.
I recommend that you have at least one code analysis tool, like SonarLint, Resharper™ or Visual Studio’s own.
6. Re-run all unit, integration and acceptance tests
Fast-forward to some point in the Future
So, you have a beautiful, well written, well structured solution which you’re proud of. Pack your stuff, go home and feel good about yourself; you’ve done a good job. However, If you’ve been in this business for a while you know there’s a good chance that you will revisit this code sooner or later, either to fix some bug (yes, good code can have bugs too, go figure!) or to add new features. This can happen in a week, a month or six months. And the thing is, the code you were so proud of then, doesn’t look that good anymore. And you know what? That’s fine! It means your skills have improved and that you can look at your own code with unbiased eyes. Now, you should be proud of yourself for wanting to do even better… and roll up your sleeves and get to work. Don’t get angry at past work…, it’s there…, in the past.
Here are a few recommendations, in no particular order (despite the numbering):
- Be constructively critical of other people’s code. Be eager and take time to do code reviews. Provide constructive feedback. Learn from other people’s code and approaches.
- Be A LOT more critical of your own code. Put yourself in someone else’s shoes and try to review your own Pull Requests from a distance. You may be genuinely proud of what you’ve produced today, only to revisit it later and think “What the f*** was I thinking?”. Don’t panic; as I said before, this is a great sign! It’s a sign that you’re self-critical and want to improve. It’s also a sign that you’ve been learning. Your decisions were correct and your solution was probably the best one at that time considering all circumstances. Do foster this practice, whether actively or when implementing new stuff of fixing bugs.
- Be open and enthusiastic to receiving feedback. Good feedback truly is an invaluable tool for self-improvement, even (especially) not being a straight “thumbs-up”, so long as it’s constructive and you’re open to it. Don’t let the seniority (or lack thereof) of the reviewer influence the importance of the feedback; countless times have I gotten great ideas and feedback from supposedly junior developers.
- Don’t let points of improvement you identify along the way fall into oblivion. Put whatever software development management tool is in place (Jira, Github, Bugzilla, …) to good use and create tasks or user stories on your backlog. Push for these to gain priority and eventually be pulled to sprints during refinement/grooming/planning sessions. Bugs, of course, are a whole different story.
- Question establishments and “the system”. In my view, the most dangerous and innovation inhibiting phrase in the software industry is “This how we’ve always done it.”, immediately followed by the “consistency” argument. Indeed, consistency is a good thing… unless things are consistently BAD.
- No team is alike. Some resist addressing technical debt and implementing good practices and need reeducation and/or training. If that’s your case, be part of the effort. Keep in mind that middle management too often need reeducation and evangelization. However, there’s no “one size fits all” recipe.
- Strive to maintain your codebase technically up-to-date. It’s much easier and more productive to do controlled small, incremental technical or framework updates than it is to be forced to take a huge step up and risk unforeseen bugs and backwards incompatibility.
- Pride can come in many degrees, but in the end, it’s important that there’s much more of it than of frustration.
Wow, I sure got carried away here. In reality, this is all easier said than done! Our day to day routine rarely allows us to put our feet on the ground and apply all this stuff. But try as we might, we’ll get there.
Now, let’s push some code to “Proud-uction”.
António Pinho is an Engineering Lead at AddCode.io
We believe in Code excellence, with People at the Heart of our organisation.
AddCode is a newly created company, based in Porto, with fully dedicated teams focused on developing long term projects for our clients.
We offer a multicultural environment, collaborative space combined with knowledge sharing and our remote working policy helps our people to have a healthy balance between work and personal life.