Writing Code That People Can Read
Developers are often told their code will be read more than it is written. Our teammates and our future selves will inevitably have to be able to understand, debug, and modify our old code. However, we don’t always put forth the effort required to make this an easy process. In truth, it doesn’t take a great deal of work. Here are a few simple practices that can make your code look less like a jumbled mess and read more like a story.
Note that I’ll be using C# in the following examples, but you can apply these concepts to your language of choice.
Don’t be afraid of long names
We’re often instinctively trying to come up with the most concise names for our classes, methods, and variables, as if making our code take up less space on the screen will make it run more efficiently, or maybe we just don’t like to type a lot. (I suppose it may actually save a few bytes of memory, but who cares? No one.) Since we’re preparing our code to be read by others in the future, we can afford to spend a few extra seconds writing it the first time.
Variable names should be long enough to fully explain their purpose. When someone without a lot of prior knowledge reads your variable names, they should come away with a pretty good idea of what is going on. For this reason, try to avoid using acronyms, abbreviations, and the var
C# keyword in your code. Which would you prefer to read? var sdwMsg
or string methodDigitalWorksMessage
? Don’t worry, the max length for an identifier – in C#, at least – is 511 characters. There’s plenty of space to be descriptive.
Comments on comments
People have different opinions on how much we should comment our code. Personally, I was sometimes required to comment everything in college. It was slow, boring, and often involved repeating the same words that were in the code I was commenting. Then I graduated and started a working on a project with 100,000 lines of code, and comments were nowhere to be found save for the occasional //wtf?
.
If you’re creating a library to be utilized by other developers working on other projects, comments describing the public methods are very helpful. Otherwise, I’ve found that if you think comments are necessary to explain what a section of code is doing, it’s an indicator that you should reorganize your code to be more self-explanatory. Often times, this is as simple as moving the otherwise confusing statement into a new, clearly-named method.
Consider this fairly complex if
statement that I just made up:
// if the entity has a valid date range
if (entity.StartDate < entity.EndDate
&& entity.StartDate >= DateTime.Now
&& entity.EndDate - entity.StartDate <= TimeSpan.FromDays(30))
Move the logic to its own appropriately-named method, and you convey the same information as the comment, with the added bonus that this statement no longer needs to worry about the exact criteria that determines a date range’s validity.
if (HasValidDateRange(entity))
One situation where comments are necessary for readable code is not explaining what you’re doing, but why you’re doing it. If you’re forced to create a workaround for something, let the other developers know with something like “Hey y’all, I know this looks weird, but it’s the only way it will work with {third party library}
because it has these {various limitations}
.” This will help prevent issues if someone tries to refactor your weird-looking code in the future.
Keep your methods short
Your childhood English teachers hopefully taught you that run-on sentences and really long paragraphs are bad. Information is much easier to digest when it’s broken down into smaller packages. This concept holds true in programming as well. Long methods with lots of different operations are naturally more confusing, but we can break them up into separate, smaller methods and/or classes for each thought we’re trying to express.
To illustrate this point, I dug up some old code I wrote several years ago, before anybody taught me this stuff about code readability. It’s from a game project – a 2-D side-scrolling platformer. It’s fairly long, has a lot of math, and required a significant amount of time for me to recall what its purpose was. It appears that we have a player’s location stored in a MapPosition
variable and a Levels
grid which tells us which tiles are solid walls and which are open space.
public void CollisionDetection()
{
if (Velocity.X == 0)
return;
MapPosition.X += Velocity.X;
Rectangle boundingBox = new Rectangle(
(int)MapPosition.X, (int)MapPosition.Y,
63, 63);
for (int y = (int)(MapPosition.Y / Levels.TileSize); y <= (int)((MapPosition.Y + 63) / Levels.TileSize); y++)
{
for (int x = (int)(MapPosition.X / Levels.TileSize); x <= (int)((MapPosition.X + 63) / Levels.TileSize); x++)
{
if (y >= Levels.Height || x >= Levels.Width ||
(Levels.level_1[y, x] > 0 && Levels.level_1[y, x] < 4))
{
Rectangle tileBoundingBox = new Rectangle(
Levels.TileSize * x, Levels.TileSize * y,
Levels.TileSize, Levels.TileSize);
if (boundingBox.Intersects(tileBoundingBox))
{
if (Velocity.X < 0)
MapPosition.X = (x + 1) * Levels.TileSize;
else
MapPosition.X = x * Levels.TileSize - 64;
Velocity.X = 0f;
}
}
}
}
}
I’m sure we’ve all encountered single methods longer than this. This one is actually trimmed down a bit to make an easier example. What stands out as particularly complicated or confusing here?
- The two
for
loops contain some divisions that aren’t explained. - The number 63 appears a lot for some reason.
- The first
if
statement inside the for loops is pretty long. - The code that changes
MapPosition.X
if the bounding boxes intersect is a little confusing without prior knowledge of how the tile system works. - And even the two uses of the Rectangle constructor could be split out into their own methods.
Now with some creative use of copy and paste, we can break this method down into something like this:
public void CollisionDetection()
{
if (Velocity.X == 0)
return;
MapPosition.X += Velocity.X;
Rectangle boundingBox = GetPlayerBoundingBox();
for (int y = GetIndexOfTile(MapPosition.Y); y <= GetIndexOfTile(MapPosition.Y + boundingBox.Height); y++)
{
for (int x = GetIndexOfTile(MapPosition.X); x <= GetIndexOfTile(MapPosition.X + boundingBox.Width); x++)
{
if (!IsTileOpen(x, y))
{
Rectangle tileBoundingBox = GetTileBoundingBox(x, y);
if (boundingBox.Intersects(tileBoundingBox))
SetPlayerPositionToEdgeOfWall(x);
}
}
}
}
private Rectangle GetPlayerBoundingBox()
{
return new Rectangle(
(int)MapPosition.X, (int)MapPosition.Y,
63, 63);
}
private int GetIndexOfTile(int position)
{
return position / Levels.TileSize;
}
private bool IsTileOpen(int x, int y)
{
return y >= Levels.Height || x >= Levels.Width ||
(Levels.level_1[y, x] > 0 && Levels.level_1[y, x] < 4);
}
private Rectangle GetTileBoundingBox(int x, int y)
{
return new Rectangle(
Levels.TileSize * x, Levels.TileSize * y,
Levels.TileSize, Levels.TileSize);
}
private void SetPlayerPositionToEdgeOfWall(int tileX)
{
if (Velocity.X < 0)
MapPosition.X = (tileX + 1) * Levels.TileSize;
else
MapPosition.X = tileX * Levels.TileSize - 64;
Velocity.X = 0f;
}
Technically, this is more lines of code than before, but it’s worth it. Notice how the CollisionDetection
method now looks a lot like pseudocode, which exists for the purpose of human readability. The method names are now serving as descriptions of the game’s logic, neatly packaged within the code itself.
Also, unlike the single method approach, we aren’t required to keep track of everything in our heads at the same time. If I don’t need to know the specifics of setting the player’s position to the edge of the wall, that’s fine. I can trust that the SetPlayerPositionToEdgeOfWall
method knows what it’s doing, skip it, and move on. It allows the readers to decide when they are ready to dig deeper into the details.
As a rule of thumb, if a single method is longer than 10 lines, it will probably read more clearly when broken out into several smaller methods.
You can do it too
The great thing about these simple tips is that they are just that: simple. You can easily apply them to existing, “ugly” code without much risk of breaking anything in the process. And if you regularly put them into practice, before long, they will become a natural part of writing new code. Who knows, your teammates may even start to like you more when you stop making them have to think so hard to work with your code!