Welcome to our captivating session on refactoring, a powerful tool for tidying up code, much like organizing a toolbox for efficiency.
In C++, each line of code is akin to a foundational component in a complex system, and poorly organized code can lead to unwieldy and unstable software. Today, we'll focus on enhancing the readability, maintainability, and performance of our code through refactoring.
Let's briefly revisit a few key concepts:
-
Code Smells: Indicators that our code needs refactoring, such as tangled dependencies or redundancy that call for cleanup.
-
Refactoring Techniques: We've familiarized ourselves with
Extract Method
andRename Method
techniques in earlier lessons, using C++ functions and member functions to maintain clarity and abstraction. -
OOP in Refactoring: We've harnessed Object-Oriented Programming principles to enhance our code's structure.
-
Code Decoupling and Modularization: Methods to manage code by minimizing dependencies while organizing through header and implementation files.
We'll use these concepts as guiding stars as we journey through the refactoring process in C++.
Let's begin by refactoring a complex game score computation function. Observe the initial scenario in C++:
C++1int computeScore(const Player& player, const std::vector<int>& monsters) { 2 int score = 0; 3 for (const auto& monster : monsters) { 4 if (player.getPower() > monster) { 5 score += player.getPower() - monster; 6 } else { 7 score -= player.getPower() - monster; 8 } 9 } 10 return score; 11}
Here, the repetition of the player.getPower() > monster
and player.getPower() - monster
calls suggests an opportunity for refactoring. We'll apply the Extract Method
and Rename Method
to improve this:
- We'll extract the scoring logic into a separate function,
calculateScoreChange
. - We'll rename the original function to
computeGameScore
.
The refactored code will enhance clarity and modifiability:
C++1// New function to calculate score changes. 2int calculateScoreChange(int power, int monster) { 3 return (power > monster) ? (power - monster) : (monster - power); 4} 5 6// Refactored function to calculate the game score. 7int computeGameScore(const Player& player, const std::vector<int>& monsters) { 8 int score = 0; 9 for (const auto& monster : monsters) { 10 score += calculateScoreChange(player.getPower(), monster); 11 } 12 return score; 13}
This refactoring makes the code easier to understand and adapt for future changes.
Next, consider a scenario where a game includes various types of monsters, each reacting differently when encountered by a player. Initially, our code might look like this:
C++1void monsterReaction(const std::string& monsterType, const Player& player) { 2 if (monsterType == "ghost") { 3 if (player.getPower() > 5) { 4 std::cout << "The ghost flees in terror!" << std::endl; 5 } else { 6 std::cout << "The ghost grumbles and attacks!" << std::endl; 7 } 8 } else if (monsterType == "goblin") { 9 if (player.getPower() > 3) { 10 std::cout << "The goblin groans and retreats!" << std::endl; 11 } else { 12 std::cout << "The goblin hacks with its sword!" << std::endl; 13 } 14 } 15 // more monster types... 16}
To improve this, we'll refactor using OOP principles and Code Decoupling:
- We'll introduce a base class
Monster
with a virtual methodreaction
. - We'll create derived classes
Ghost
andGoblin
inheriting fromMonster
, each implementing its ownreaction
method.
In the revised structure, our code will look like this:
C++1class Monster { 2public: 3 virtual void reaction(const Player& player) = 0; // Pure virtual function. 4 virtual ~Monster() = default; // Virtual destructor. 5}; 6 7class Ghost : public Monster { 8public: 9 void reaction(const Player& player) override { 10 if (player.getPower() > 5) { 11 std::cout << "The ghost flees in terror!" << std::endl; 12 } else { 13 std::cout << "The ghost grumbles and attacks!" << std::endl; 14 } 15 } 16}; 17 18class Goblin : public Monster { 19public: 20 void reaction(const Player& player) override { 21 if (player.getPower() > 3) { 22 std::cout << "The goblin groans and retreats!" << std::endl; 23 } else { 24 std::cout << "The goblin hacks with its sword!" << std::endl; 25 } 26 } 27}; 28 29std::vector<std::unique_ptr<Monster>> monsters; 30monsters.push_back(std::make_unique<Ghost>()); 31monsters.push_back(std::make_unique<Goblin>()); 32 33for (const auto& monster : monsters) { 34 monster->reaction(player); 35}
Now, managing different monster types is cleaner, and adding new types becomes straightforward through the extension of the class hierarchy.
Excellent work! We've refactored two practical problems, sharpening our ability to identify code smells and apply effective refactoring techniques.
As you continue to practice, you'll become more adept at recognizing and improving sections of code that can be refactored. Stay tuned for more practice tasks, and keep your code clean, efficient, and maintainable!