Eighth in a series of posts on Software Design for Unity. Read the introduction here.
It’s a seemingly inherent desire in most programmers to make every solution as general as it possibly can be. That is to say, to design code such that it can handle almost any input, any situation.
The attraction is obvious – design one amazing bit of code, and you can use it all over your codebase, rather than having to hack together lots of messy special case solutions. Beyond that, for a lot of coders (myself most definitely included) there’s a drive to create elegant code for its own sake. There’s an art and a beauty in powerful but clean code, and a lot of the time that’s kind of why we do what we do.
I’ve previously touched briefly on generalising and specialising, but I want to talk about the dangers of going too far in both.
The Cost of Generalisation
As a rule, generalised code takes longer to write than specialised code. If you don’t know exactly what situation the code will be used in, you’ve got to make sure it works with a wide range of them. Specialised code, on the other hand, requires a certain context and need only take account of that context. One case rather than many.
Furthermore, it’s often quicker to write two or three special case algorithms rather than one general version which works for all of them.
Generalised code is harder to optimise, and easier to break
There are indirect costs as well. Generalised code is harder to write well. Special case code, due to the constraints on input and context, can easily be optimised. Most optimisations centre on assumptions, and taking shortcuts that won’t ruin the output as long as those assumptions are true. By definition, the more general your code the fewer assumptions you’re making, so the less room there is to optimise.
An extension of that is that generalised code is going to be buggier. Again, fewer assumptions means more edge cases where your code breaks.
When Not to Generalise
Although it’s often not performance-driven, the desire to generalise can often be thought of in terms of premature optimisation. It’s similar in that you’re often looking further ahead than you should.
Often, you’re predicting future use-cases for your code. And most of the time, you’ll be wrong.
It feels bad, but special case code is better than general code
The problem is there are far more ways to be wrong than right. You could find out it actually doesn’t work in the future case(s) you planned for. You could be wrong about how it’ll need to be used – a small change in assumptions can change the entire algorithm. Most likely, you might just be straight-up wrong about needing to use this algorithm in any other situation at all. In other words, You Ain’t Gonna Need It.
It doesn’t save you any time to generalise before you know A) that you need to and B) you know exactly what’s needed.
It feels bad, but a good rule of thumb is – special case code is better than general code. Always try to write special case code first, and only generalise it later if you really need to.
When Not to Specialise
Over-specialisation is a different beast, and more slippery. A such this section is a bit more of a rant!
While specialisation for your own code is good, over-specialisation usually occurs when writing code for others to use. Usually it takes the form of providing too many specialised features in an effort to create a codebase which is itself attempting to be generalised. In many ways, it’s actually an extension of the drive to generalise.
This often comes up when writing plugins, libraries and assets for third-party use. Many products on the asset store fall foul of this – they attempt to account for all use cases and create lots of highly-specialised features to do so. The end result is often that you’ll pick up an asset thinking it’ll allow you to do something, then find out that unless you do things exactly as the asset expects, you can’t do anything.
It’s very difficult to write highly-specialised code for others to use. The problem is that you don’t know how they want to use it. This isn’t to say that you should try to write hugely generalised code in these cases – again, you’ll likely fail!
There’s a balance to be found. Ideally a plugin / library / asset should identify the right level of granularity to give users what they need. Too much granularity and the user may as well write the code themselves. Too little, and it’ll be over-specialised. Really, a user should be able to specialise on top of your code. You don’t know what specialisations they need, so give them a solid set of building blocks and let them do the rest.
There are plenty of examples in both directions. Often, a single plugin will do both – provide some massively over-specific features, while others are so low-level that you have to understand all of the author’s code to use themI have specific examples, but it doesn’t seem kosher to name and shame..
It’s difficult to sum this up as concrete advice, unfortunately. The best thing, perhaps, is to try to consider how easy it is for users to use bits of your code for slightly different purposes. And rather than then trying to write new features to support that, open those parts up to them to use as they wish!
Note, also, that simply open-sourcing your code is not the answer. Users don’t want to trawl through your codebase to work out how to do the apparently simple thing they want. You should consider what they’re likely to want to do, and pull those things out as first-class citizens of your API. And be prepared to do that in response to user feedback, tending toward that rather than writing entire new features based on one user’s highly-specific needs.
Despite natural tendencies toward both extremes, there are hidden costs to generalisation and specialisation. When writing code for your own project, take this to heart – special case code is better than generalised code. It’s better for clarity, for bugs, and most importantly your valuable time. Only generalise when you really know you need it, and what your use cases are.
When you’re writing code for others, you need to balance your approach. You can’t know exactly what their use cases are, so don’t over-specialise your code. You can’t plan for unknown use casesand trying to may well just lead to worse overspecialisation, but try to consider how easy it would be to repurpose your code outside your expected parameters. Identify the important building blocks of your library and expose them to the API, without requiring the user to understand the internals to do anything at all.
Footnotes [ + ]
|1.||↑||I have specific examples, but it doesn’t seem kosher to name and shame.|
|2.||↑||and trying to may well just lead to worse overspecialisation|