If you’ve ever worked on a project for more than a couple of weeks, you’ve likely experienced
a phenomenon known as “Technical Debt”.
Technical debt is the implied cost of additional rework that’s caused by choosing an easy
or limited solution instead of using a better approach that would take longer.
In other words, it’s the time, money, and effort that’s required to rework a project
that’s being held back by low quality code.
The term has a bit of a negative connotation because it sorta implies that developers should
know better and generally have the power to avoid it.
But in my experience that isn’t always the case.
Factors like tight deadlines, inexperience with certain technologies, and a poorly defined
scope can all play a role in producing technical debt that must be repaid.
However, one common cause that is in your control is “Delayed Refactoring”.
Refactoring is a controlled technique for improving the design of an existing code base.
It’s purpose is to improve the design, structure, and/or implementation of your code, while
preserving its functionality.
In many cases, delaying the refactoring of certain aspects of your code will almost certainly
lead to technical debt.
But like most things in software development, it isn’t as simple as it sounds.
Refactoring in itself is a skill that can take years of experience to perfect.
When I first started refactoring I’d mostly end up breaking my own code, and I’d honestly
get really frustrated and overwhelmed with the process.
But practice makes perfect and luckily over the years I’ve picked up a couple of guidelines
that have really helped me to improve.
One in particular is framing my refactoring sessions around a single goal.
For example, you can refactor for scalability by focusing on decoupling tightly coupled
code.
Or you can refactor for testability by focusing solely on externalizing your class’s dependencies
so they can be injected with mocks.
But in my opinion, the most important approach you can take is to refactor your code for
readability.
Because at the end of the day programming languages were designed for humans and one
of the easiest things you can do to get the most value out of your programming is to write
code that’s easy to read and communicates your intent effectively.
So in this video I’m going to share a few tips that you can use to refactor your own
code for readability.
To help us run through these tips, we’re gonna use some user submitted code that Jason
Storey and I reviewed during a livestream back in 2020.
Big shoutout and a thank you to Darexi for their submission.
It isn’t easy to put yourself out there and I want to be clear that there is nothing
functionally wrong with this code.
The code consists of a MonoBehaviour that represents a single Jenga piece.
If you’ve never played Jenga before, it’s a game where players take turns removing one
block at a time from a tower that’s constructed of 54 blocks.
The goal is to try to place your blocks at the top of the tower without knocking it down.
This code is a good example of why it can be difficult to approach refactoring because
there are a number of ways that we can rewrite this logic.
That’s why for this refactoring sesion we’re only gonna focus on improving this script’s
readability.
And we’re going to take it step-by-step so you can use the chapter markers below to
skip around or easily re-watch any section that you’d like to review in the future.
Let’s get started.
My first tip for improving the readability of your code is consistency.
Reading someone else’s code, or even your own code from a couple of weeks ago, is hard
enough as it is.
But when that code doesn’t have consistent indentations, naming conventions, and overall
code styling, your brain has to work harder to understand its contents, and the intent
of the programmer who wrote it can sometimes get lost.
Imagine reading a book where each page used multiple font families, paragraph sizes, and
margins.
It would be a nightmare to read and you’d probably never finish it.
So let’s jump into the code and clean up some of the inconsistencies in our JengaPiece
class.
For example, the indentation of lines 25 through 35 is inconsistent with the rest of the script,
so let’s fix that.
And the bracket position and spacing of the if-else-statements in the same section don’t
follow the standard set out by the rest of the script either.
The same can be said for a few of these functions found down below; let’s clean those up,
too.
Looking better already but consistency shouldn’t end with a single class.
I’ve worked on plenty of projects where each developer wrote their code in the style
that they were most comfortable with.
But that approach produces a codebase that is disjointed and really hard to work with.
You never can tell what you’re gonna get when you look at a script for the first time.
That’s why it’s important to follow the coding practices of whatever team you’re
working with so the entire project follows the same styling.
It’s not about being right or wrong, but rather about building a codebase that’s
predictable and comfortable to read once you get used to its flavor of styling.
To illustrate this, we’re gonna pretend that I am the lead developer on this project,
which would make me responsible for determining the styling.
As such, I’m going to explicitly add the private keyword to each private variable and
method in the class.
I like this example because whether or not you choose to add the private keyword is completely
subjective.
In fact, Jason Storey prefers leaving it out, so this is a case where he’d have to compromise
on his own standard styling.
Great, looks good to me!
It should come as no surprise that every developer has their own opinion about how you should
style your code.
When enough developers can agree on a particular style, a convention is born.
Microsoft has its own C# convention that you can find on their official documentation.
It describes everything from how you should name public versus private variables to what
format you should use when leaving comments.
Of course, you don’t have to follow Microsoft’s conventions just because they created the
language.
In fact, many development teams create their own conventions that you can typically find
as a style guide that’s outlined in the readme files for their project’s repositories.
That being said, if your goal is to empower as many developers as possible to help you
complete your project, I’d recommend going with Microsoft’s conventions as they’re
definitely the most widely used.
Let’s refactor some of those conventions into our JengaPiece class.
The first convention we’ll address is naming.
In terms of class members (i.e., fields, properties, and methods), the rule of thumb is to use
Pascal case when members are public and camel case with a leading underscore when they're
private.
So let’s do that now.
One additional convention that’s specific to Unity is that properties which are exposed
in the Unity editor should use camel case.
I’m still on the fence about whether or not I like it, so I’ve been trying it out
in my personal projects and it’s already starting to grow on me.
The next convention we’ll address is implicitly typed local variables.
Basically, if the type of the variable is obvious from the right side of the assignment,
just use “var”.
Okay, there.
There are plenty of other conventions but those were the only two that needed to be
refactored into this script.
Before we move on, though, you may have noticed that I used some shortcuts that were built
into my code editor to complete the refactor.
That’s because my IDE, as well as a few third party plugins that you can download,
can suggest and automate code styling changes for you.
You can customize them to fit your needs, but most (if not all) of them adhere to Microsoft’s
official C# conventions out of the box.
It just makes the whole process that much easier.
Up until now our refactoring process has been pretty straightforward.
It’s essentially been a clean up job.
But this next tip is a little more subjective.
Whenever possible you want to bake your intent right into your code.
In my experience, the easiest code to work with is code that reads almost like a summary.
It reveals exactly what the developer intended it to do in a very human readable way.
Looking at the Update method of the JengaPiece class, we can see a taste of what I’m talking
about.
Let’s try to read out the if-statement on line 24 as a summary of what it does.
“If the Jenga piece should assemble, then do a bunch of other stuff.
Otherwise, don’t do anything.”
That’s what this if-statement is currently communicating to me.
But what is this other stuff that needs to happen if the Jenga piece should assemble?
Well, I’ve already spent the time reading through it so I’ll spare you having to figure
it out.
All of this logic is responsible for moving the Jenga piece to its starting position.
We can improve the readability by extracting the logic into a method that describes this
intent.
There, now we can read this logic as, “if the Jenga piece should assemble, move it to
the starting position”.
Much better.
Extracting whole chunks of logic into separate methods (and even classes) is one approach
but there are even simpler actions that you can take to make your code more human readable.
For instance, the EnablePhysics method is a coroutine but that isn’t clear without
really looking at the logic.
Because of this, It would be easy for me to think that I could just call this method without
passing it into the StartCoroutine method, and that would lead to unexpected behavior.
Let’s clear that up by renaming it to “EnablePhysicsCoroutine”.
Of course, my name choice is subjective; Jason Storey prefers to prepend his coroutines with
the prefix “CO” and an underscore.
The important thing is that both names make it very clear that this is a coroutine.
The other thing that isn’t clear is what unit the duration parameter is expected to
be passed in as.
By reading the code we know that it’s seconds.
But another developer may assume that it’s milliseconds which, again, would lead to unexpected
behavior.
So let’s rename duration to “durationInSeconds”.
Great.
I’m happy with that.
One final example of baking your intent into your code can be found down here in the RandomForceVector
method.
It’s responsible for generating a random vector based on a bound that can be passed
in as a set of parameters.
What makes this method difficult to read is the inlining of the X, Y, and Z values in
the creation of the return vector.
Now you could certainly reformat this to improve readability, but I often prefer creating local
variables that explicitly describe what each inline value actually represents.
This example could go either way, but there are plenty of times where creating well-named
local variables is absolutely the better option.
But I wouldn’t fault you for just tidying up the formatting in this particular scenario.
My final tip for improving the readability of your code is to cut out any and all fat.
In other words, if you don’t need it and it doesn’t provide any value, get rid of
it.
The first example of this in the JengaPiece class is the keyword “this”.
The “this” keyword refers to the current instance of the class, and it’s always present
whether you reference it or not.
There are times when using it is necessary (which I won’t be talking about in this
video) but I generally only use it when I have to.
Simply because I believe that less is more.
So let’s get rid of all references to “this”.
My next example will probably be a little controversial.
Whenever possible (which is most of the time) I remove comments.
If you follow my previous tip and bake your intent into the code, you’ll never need
comments to explain what it’s doing.
Again there are a few cases where comments are useful but I won’t be covering those
cases here.
If you’d like to learn more about writing code that’s self documenting, check out
one of my older style videos on the topic here.
Also I know that these summary comments can be used to auto generate documentation but
this JengaPiece class isn’t part of a library and I’ll never need to include it in any
user facing documentation so away they go.
One more way to cut the fat out of your code is to identify and remove logic that belongs
somewhere else.
For example, this RandomForceVector method has nothing to do with Jenga so we can safely
move it to a static helper class.
Why don’t we do that now.
There — much better.
There are plenty more ways that we can continue to refactor this logic.
But having just focussed on improving its readability, I’d say we’ve made a lot
of progress.
I feel confident that I could step away from this code for a couple of weeks and still
have a pretty good idea of what it does when I look at it again.
If you’d like to take a closer look at this code yourself, I’ve made it available to
my tier two Patrons who can download it using the link in the description.
Don’t forget to sign up for the Level 2 Game Dev newsletter, and if you liked this
video, please like it, leave a comment, and subscribe.
Thanks for watching, I’ll catch you
in the next video.