This article is Part 2 of the “Write Better Laravel” series.
Read Part 1 here.
One of the most important elements of good software design is the mindset you adopt when working on programming tasks. Many organisations encourage a tactical mindset, focused on getting features working as quickly as possible. However, if you want a good design, you must take a more strategic approach where you invest time to produce clean designs and fix problems.
The problem with tactical programming is that it is short-sighted. When programming tactically, you’re trying to finish a task as quickly as possible, possibly due to a deadline. Thinking about the future becomes secondary, which means you don’t spend much time looking for the best design; you just want something working right now. You tell yourself that a bit of complexity is OK and that you will revisit it and improve it later. But I’m sure we all know by now, that “later” rarely ever comes.
This is how systems become complicated. A single complex change won’t affect much, of course, but if you continue with the tactical mindset, the system will accumulate more complexity over time, making the whole system that much harder to work with.
Before long, some of the previously “innocent” complexities will start causing problems, and you will begin to wish you hadn’t taken those early shortcuts. But, having a tactical mindset, you will tell yourself that getting the next feature out is more important than refactoring the complexity of the existing code. Pretty soon the code is a mess, but by this point, it would take several months of work to clean it up, it feels like small improvements won’t make much of a difference, so you just keep programming tactically.
“Almost every software development organisation has at least one developer who takes tactical programming to the extreme: a tactical tornado. The tactical tornado is a prolific programmer who pumps out code far faster than others but works in a totally tactical fashion. When it comes to implementing a quick feature, nobody gets it done faster than the tactical tornado. In some organisations, management treats tactical tornadoes as heroes. However, tactical tornadoes leave behind a wake of destruction. They are rarely considered heroes by the engineers who must work with their code in the future. Typically, other engineers must clean up the messes left behind by the tactical tornado, which makes it appear that those engineers (who are the real heroes) are making slower progress than the tactical tornado.”
Excerpt From “A Philosophy of Software Design” by John Ousterhout
First of all, you must realise that working code is not enough. You cannot introduce unnecessary complexities in order to finish the task faster. The most important thing is the long-term structure of the system. Most of the code is written by extending an existing code base, thus your most important job as a developer is to make future extensions easier. Writing code that works should not be the primary goal, although of course still required. Your primary goal must be to produce a great design, which also happens to work. This is strategic programming.
It requires an investment mindset. You are investing extra time in the beginning to realise a better, more sustainable and extendable design that will speed up your development in the long term.
Some of the investments will be proactive. For example, it’s worth taking a little extra time to find a simple design for each class. Rather than implementing the first idea that comes to mind, try thinking of a few alternative designs, weigh the pros/cons and pick the cleanest one. Try to think of ways in which the system might need to change in the future and make sure your design will make them easy to implement. Writing good documentation is also a good proactive investment.
Other investments will be reactive. No matter how much time you invest in the beginning, there will be mistakes in your initial design decisions. Over time, these mistakes will become obvious as you accumulate more knowledge about the system and its usage. When you discover a design problem, don’t just ignore it or patch around it; invest a little extra time to fix it properly. Strategic programming means you will continually make small improvements to the system design as you learn new information. This is the opposite of tactical programming, where you are continually adding small bits of complexity that compound into big problems in the future.
If you invest 10-20% of your development time upfront into better designs, it won’t be long before you’re developing at least 10-20% faster than you would if you had programmed tactically. At this point your investments become free: the benefits from your past investments will save you enough time to cover the cost of future improvements. You will quickly recover the cost of the initial investment as illustrated in the figure below.
Conversely, if you program tactically, you will finish your first projects 10-20% faster, but over time your development speed will slow down as complexity increases. It might even hinder or prevent certain features from being implemented due to the limitations of previously introduced complexity. If you had never worked in a badly degraded code base, talk to someone who has; they will tell you that poor code quality easily slows down development by at least 20%.
What about startups?
In some environments, like startups, there’s enormous pressure to get new features out as quickly as possible. It might seem that even a 10-20% investment isn’t affordable and thus end up going the tactical programming route, spending very little time on the initial software design, or cleaning up any problems that occur later. The common rationale is that, if successful, the startup will have enough money to hire more and better engineers to clean up the accrued technical debt.
If you have ever worked on a large codebase, you should know how nearly impossible it is to fix spaghetti code. You cannot touch one part of the system without breaking something else, seemingly unrelated. That is the cost of bad design at the beginning and it increases your further development costs significantly. Having saved the 10-20% of the time in the beginning, your developers are now spending 20%+ more time on new features because of the increased complexity that compounds even more with every new release.
One of the most important factors for the success of a company is the quality of its engineers. The best way to lower development costs is to hire great engineers: they don’t cost much more than mediocre engineers, but they have tremendously higher productivity. The best engineers care deeply about good design. If your code is a disaster, word will soon get out and it will become difficult to hire great engineers. That, in turn, will increase future development costs as mediocre engineers increase the complexity of the codebase even further.
Facebook is a great example of a startup that encouraged tactical programming in the beginning. For many years the company’s motto was “Move fast and break things.” New junior engineers dove immediately into the company’s code base and it was normal for engineers to push changes into production during their first week on the job. Although this gave the engineers a lot of freedom and trust, the code base has suffered because of the tactical approach; much of the code was unstable and hard to understand, with very few comments or tests, and painful to work with. Luckily, over time the company realised that its culture was unsustainable and, eventually, Facebook changed its motto to “Move fast with solid infrastructure” to encourage its engineers to invest more in good design. Of course, Facebook’s code probably isn’t much worse than the average startup; Facebook just happens to be a particularly visible example.
On the other spectrum, we have Google, which grew up around the same time as Facebook but had a very different approach to coding - strategic programming. Caring about and spending extra time on good design did not stop Google from becoming one of the most successful companies in the world. It also allows them to hire the best technical talent.
The examples above show that a company can succeed with either approach. However, it’s a lot more fun to work in a company that cares about software design and helps you become a better developer as well.
Good design doesn’t come for free. It has to be something you invest in continually, so that small problems don’t grow and compound into big ones. Fortunately, the good design eventually pays for itself, often sooner and more than you think.
Make sure you are consistent when applying the strategic approach and think of the investment as something you do today, not tomorrow. It can get tempting to put off cleanups until after the feature is done, but that’s a slippery slope. After this feature is done, there will be another, and another. If you don’t prioritise strategic thinking up front, it will be much harder to find time for it later. The longer you wait to address the problems with the codebase, the worse it will get, the more intimidating they will get, which makes it easy to put them off even more. Make it a habit and continuously make small investments in good design.
Understanding your tools and enemies is an important step to becoming better at any craft, and software development is no different.
If you consciously think about complexity in your code and take time to think strategically about its implementations, you will notice not only an improvement in the system but also an improvement in your own confidence to write great software.
Make sure to subscribe to receive new articles of the "Write Better Laravel" series right in your inbox!