When we write code, experiment and try different things to get our functionality to work, the solution we're left with isn't always optimal. It can be unneccessarily complex, or difficult to expand on in the future.
Refactoring is the process of taking code that already works, and improving it to make things simpler or more understandable.
Without constantly reviewing and refactoring our code to manage its complexity, the software we create quickly becomes limited.
If our functions and classes get too complex, it becomes increasingly difficult to add new features or fix bugs.
This can grind our project to a halt. To build more complex projects, we need to be able to manage that complexity. Good design, and proactive refactoring are how we do that.
Which of the following are examples of refactoring?
It is a lot easier to refactor code immediately after we have written it. At that point, it's still fresh in our memory, and we know exactly what the code is supposed to do.
Therefore, after we've created our new feature and confirmed it works as expected, we should review our code and see if we can improve it, before moving on.
In professional settings, this tends to be a required part of the process. We will ask other developers on the project to review our code and provide feedback before we merge it into the shared repository.
By restricting access to our classes in the way we did in the previous lesson, it also makes our code easier to refactor.
We know that other code is not going to be accessing the private
parts of our class, so we are free to rewrite, delete and modify those as needed.
In the previous lesson, we might have left our class looking something like this:
class Monster {
public:
void SetHealth(int NewHealth) {
if (NewHealth <= 0) {
Health = 0;
isDead = true;
} else {
Health = NewHealth;
isDead = false;
}
}
bool GetIsDead() {
return isDead;
}
private:
bool isDead { false };
int Health { 150 };
};
A common first step to refactoring code is to ask ourselves if we can delete parts of it. For example, do we need to be storing and maintaining the private isDead
boolean?
Because it's private, we know other code isn't accessing it - it's just being used in the GetIsDead
function.
However, we can update that function to just use the Health
value instead:
bool GetIsDead() {
return Health <= 0;
}
Now, we can remove the isDead
variable from our class entirely. With the variable gone, we no longer need to maintain it in the existing TakeDamage
function, nor in any future function we create. This keeps our code clean and simple.
class Monster {
public:
void SetHealth(int NewHealth) {
if (NewHealth <= 0) {
Health = 0;
isDead = true; // not needed
} else {
Health = NewHealth;
isDead = false; // not needed
}
}
bool GetIsDead() {
return Health <= 0;
}
private:
bool isDead { false }; // not needed
int Health { 150 };
};
In the previous example, the issue is that we were storing unnecessary variables in our class. This is one of the most common ways projects become too complex.
The amount of code we need to write to keep all of our variables in a valid state gets out of hand.
If the value of a variable can be derived from the value of another variable, it is almost always better to replace that variable with a getter that performs the derivation, This is much easier to maintain than attempting to keep both variables synchronized, especially as our class gets more complex.
Consider this function:
bool isEnraged() {
bool isEnraged;
if (Health <= 0) {
isEnraged = false;
} else if (Health <= 200) {
isEnraged = true;
} else {
isEnraged = false;
}
return isEnraged;
}
Which of the following are ways to refactor it?
This lesson introduced refactoring in a very simple context - refactoring a simple function. But, it is the concept that is important. We should constantly be asking questions about our code - just because our solution worked, that doesn't mean it was a good solution.
Restricting the ability for external code to change properties like Health
has introduced another limitation to our class.
It is no longer possible to initialise monsters with different Health
values. Whilst allowing other code to modify the Health
of monsters in an uncontrolled way was problematic, we might still want to provide the ability to choose the initial Health
value for our monsters.
We already know enough to make this happen. We could, for example, create a public function on our class. We could call this function something like Initialise
, and have it accept an argument for the initial health value.
We could even apply what we've learned to make sure this function is only called once per object:
class Monster {
public:
void Initialise(int InitialHealth) {
if (isInitialised) return;
Health = InitialHealth;
isInitialised = true;
}
private:
int Health { 150 };
bool isInitialised { false };
};
But, so common is this requirement to set the initial values of class members, there is a special type of function specifically for this purpose. Constructors will be the topic of our next lesson!
Become a software engineer with C++. Starting from the basics, we guide you step by step along the way