Hey guys! Welcome in the first post in a series about debugging process. This post will be treating about good practices which will help you in the debugging (and/or decrease amount of situations where you’ll need to do that). The second one will be about debugging process workflow, tools and how to use them in Visual Studio. I will focus on the most useful ones in my opinion. After these two posts, you should be equipped with a set of tools that, I think, are enough in most cases. This article collects code practices that are good in general, so even if you think you don’t need debugging (yet), you will make use of them. I decided to write this short series for two reasons. The good practices are good not because the big individuals said so. They are good for a reason. They make our development lives easier. I want to show you their usefulness during the debugging process because debugging is a reasoning about the code. It requires a lot of focus from us. This is a situation where code quality makes a huge difference. These practices make that reasoning easier. The second reason is that I see too many ‘easy’ questions on the Stack Overflow, Quora, and Facebook groups. By ‘easy’ I mean the problems that are fairly simple to solve during the debugging process. It seems like many of us just don’t debug at all. Maybe because they just don’t know how or it seems too hard or time-consuming. They prefer to delegate the problem to the Internet, what is usually more time consuming than debugging it by themselves. And the truth is, after fair amount of practice, you can find it almost easy and almost mechanical. No matter how good your code is, the bugs will happen sooner or later. You will have to get to the source of the problem. This is where the debugging begins and you better be prepared for that. Let’s get into it!
‘Debugging’ series
Part I – 7 good code practices that will help you in debugging (this)
Part II – What to do when Centralized Control does not work?
Content
1. One instruction per line
2. Keep variables’ span and live time the lowest possible
3. Move complex logical operations to a separate variables/methods
4. If you have getters/setters, don’t use variables directly
5. Program offensively/defensively
6. Keep nesting as shallow as possible
7. Write comments
Let me start with the quite ‘obvious’ advice, but a crucial one.
1. One instruction per line
It may sound almost like a cliché, but let’s compare it to reading a recipe. Below you have two instruction sets.
- Boil at least 250ml of water, take a cup to which you’ll need to put a tea bag and when the water boils, pour it into the cup until it’s full.
And…
- Boil at least 250ml of water,
- Take a cup,
- Put the tea bag inside the cup,
- Wait for the water to boil,
- Pour boiled water to the cup until it’s full.
While making a tea is fairly simple, imagine thai soup recipe (no cheating, you’re doing a curry paste!) written in one, enormous sentence. Besides easier reading, one instruction per line rule does one more thing. Assume that something went wrong and you can see line number of the error displayed in the console. You immediately know which instruction or method call is guilty before even launching a debugger. Write something like
{ // ... return ShowMatchSummaryWindow(GetWinner().Name, GetLoser().Name, Time.ToSeconds(GetMatchTime())).Owner == GetHost(); }
and you are screwed. You have to launch the debugger first and go through every method call to see where the game blew up exactly. It wastes your time.
Instead, you can write this:
string winnerName = GetWinner().Name; string loserName = GetLoser().Name; int matchTimeInMilliseconds = GetMatchTime(); float matchTimeInSeconds = Time.ToSeconds(matchTimeInMilliseconds); Player windowOwner = ShowMatchSummaryWindow(winnerName, loserName, matchTimeInSeconds); bool windowOwnerIsHost = windowOwner == GetHost(); return windowOwnerIsHost;
Yes, you can split it to even more lines, that’s okay. Do it until you know that there is only one thing that can go wrong on every line. Caching the value before returning it may be a good idea as well. Try to avoid things like return MethodCall();
2. Keep variables’ span and live time the lowest possible
Steve McConnel describes two metrics about variables that you should be aware of in his book, Code Complete. The first one is a live time which says for how many code lines the variable exists. If it has been declared in line 100 and its last use is in line 150, the variable’s live time equals 51. The second metric is a span. It references to a number of lines between next usages of the variable. Declare a variable in line 10, use it in line 12 and the span will be 1. Then use it again in line 20 and an AVERAGE span will be equal to 4. Minimizing variable’s live time and average span reduces a chance that somewhere in between the variable will be changed incorrectly. Keeping in mind what the variable is and how it is changed for tens of lines of code is difficult. A couple of such variables and you no longer know what you are reading. Maintaining these metrics low allows you to focus on little pieces of the code and keeping all the variable usages on one screen (in case of local/auto variables).
Here’s a quote about scope minimizing from the mentioned book:
The difference between the “convenience” philosophy and the “intellectual manageability” philosophy boils down to a difference in emphasis between writing programs and reading them. The maximizing scope might indeed make programs easy to write, but a program in which any routine can use any variable at any time is harder to understand than a program that uses well-factored routines. In such a program, you can’t understand only one routine; you have to understand all the other routines with which that routine shares global data. Such programs are hard to read, hard to debug, and hard to modify.
– Steve McConnel, Code Complete 2nd
3. Move complex logical operations to a separate variables/methods
Look at this:
public void AttackPlayer(Player player) { if (GetDistanceTo(player) <= rangeOfView && player.HasCamouflageActive() && !Physics.Raycast(this, player, rangeOfView)) { // attack the player } }
In a code like the above one, you have to read through the condition to see what is being checked. If you split it into separate variables and method, the code will look as follows:
public void AttackPlayer(Player player) { if (IsPlayerVisible(player)) { // attack the player } } private bool IsPlayerVisible(Player player) { bool isInRangeOfView = GetDistanceTo(player) <= rangeOfView; bool hasCamouflageActive = player.HasCamouflageActive(); bool isCoveredByObstacle = Physics.Raycast(this, player, rangeOfView); return isInRangeOfView && !hasCamouflageActive && !isCoveredByObstacle; }
And we both know that the conditions can become way more complicated than this. By moving them to separate methods and/or naming each condition, you have a code that is easier to understand and fix.
4. If you have getters/setters, don’t use variables directly
In C# they are called Properties, in C++ or any other language they might be just GetVariable()
and SetVariable()
methods. They are used to access the variable in a proper, cohesive way. Set it or get it and do something else in the middle. This ‘something else’ part might be serializing the variable to string format in getter or calling the StrengthChanged()
event in SetStrength()
so the other parts of the game can react to that change.
If the getters and setters are methods that do only getting and setting and nothing else, it may seem like in the class’ body it doesn’t matter which one you use. But that is not true. If you need to add ‘something’ to the accessors, your job should be done there. When you are using the variable directly, you have to take that change into account in every direct usage of the variable. And you can simply forget to do that.
Secondly, if you’re looking for all references to the variable (Shift + F12 in VS), you can do this for accessor method only, not for accessor AND for the variable. You’ve got full list of usages, instead of two scraps of it.
Sometimes you might think that it is better (or even that it is the only way) to use the variable directly, but stop yourself there and take a moment to rethink it. Very often it is a signal that the accessor method does too much. Every operation in the getter/setter should be an operation that can be called no matter how the world outside the class looks like or what is the current state of the class. It is a bit of generalization, but just consider it when you feel tempted to use a variable directly while having an accessor for it already. Try to move some of the operations that ‘shouldn’t be there’ out of the accessor and to a separate method. Then, after every accessor usage, you can call this new method if you need it.
5. Program offensively/defensively
Your game will be evolving very often during the design’n’development process. That’s the good thing. Preconditions for methods will stop to be fulfilled, you’ll have null pointer references, types of the variables will change and mismatches will appear. And that’s a natural order of things. Don’t be afraid of bugs during the development time. They are very valuable pieces of information about what went wrong. The worst thing you can do is to hide them or try to secure every possible case by writing a lot of additional code. This is a very important subject and I am gonna write a whole post about it, but for now, take this with you
public void RemoveCondition(ConditionType conditionType) { if (!_initialized) Initialize(); _conditionsManager.RemoveCondition(conditionType); }
You’re admitting here that you didn’t consider very well when the class can be used and when it shouldn’t be. When it is not initialized, you made a mistake in the code. Don’t try to ‘patch’ it here and there. Maybe you forgot about the class that initializes this one? The object that initializes it hasn’t been loaded during the level load? Fix it, don’t patch or mask it.
public void RemoveCondition(ConditionType conditionType) { if (!_initialized) return; _conditionsManager.RemoveCondition(conditionType); }
This is so bad and so frequent. Man, you’ve just called the method and you think that the condition has been removed. Your game is running. And weird things happen. Something went wrong. You don’t know what. You don’t know where or when you’ll see the results. You cannot even be sure that the results on the screen will be related to the original issue at first glance.
public void RemoveCondition(ConditionType conditionType) { if (!_initialized) Debug.LogError("Conditions Behaviour is not initialized, so the conditions won't be applied. Initialize it first."); _conditionsManager.RemoveCondition(conditionType); }
Boom! ‘Boy, you cannot use me, you forgot about the initialization!’. Your game is your spaceship. If something is wrong with it, you want to know what it is, so it can be fixed before the launch. You can ignore any checking and let things to be handled by the compiler, but I think it is not quite a good idea either. First, you cannot be sure if the compiler’s error will be displayed immediately or if the issue will be propagated for a few more methods before doing that. Secondly, you give up on your own, human, messages, written to your future self.
Don’t secure your game against every possible issue. That’s a lot of code that is just rubbish. Inform that there is an issue and expect that this particular case will be fixed. You don’t have to check if an index is within a range right before accessing an array. The compiler will do this for you. But on the other hand, if your compiler’s message, in this case, is a mess or you want to add additional information to the message – feel free to do that. Cooperate with the complier. As I said, I will talk about it later.
You can read more about defensive programming here and offensive one here.
If you found some great articles on these topics, feel free to share them with us in the comments.
6. Keep nesting as shallow as possible
There’s an if in another if, in a for loop in one of the switch cases… No. Try to keep number of nests below 4.
7. Write comments
Yeah, some people say ‘Don’t write comments, write a code that comments itself’. Other ones say ‘Write a lot of comments’. Or something like that. But you can be a more balanced. Write comments if you decided to write something ‘this way’, even if it may not be the most obvious way of all. Explain in the comment WHY you’ve written it like that. ‘Because here I don’t have a reference to that and this method is called before that one, and I didn’t want to…’. Yes, you’re describing ‘why’ you did it, not ‘what’ it does. Maybe consider writing comments for more complicated blocks of code. Even if your code is really good, treat it as a possibility to check yourself after reading. Low-level code usually requires more comments than the high-level one.
I think this is a good place to say it loud and clear. It is impossible to write and describe every case where the rule is applicable. To point out every ‘unless’ with an exception. It often HAS TO be a little simplified to make the text done and readable by a human being with a job and/or life. But that’s a good thing. The major goal of mine is to help you to be aware of the rules, good practices and what they are for. The main advice is to THINK what are the solutions for the problem, which one seems the best (and the simplest) NOW (for the current problem). Be AWARE of the good practices and rules, so you can write the code consciously. But the good part of it is, that very often you don’t have to find the perfect solution. Usually, your decisions are trade-offs anyway. But make them consciously and it will be okay. Calm down. Think. Go for it. And see you soon.
Leave a Reply