Lesson 5
Stepping into Refactoring Code in Go
Stepping into Refactoring Code

Welcome to our captivating session on refactoring, a powerful tool for tidying up code, much like organizing a messy toy box or finding a faster route to school.

Just as each line of code is as essential as a brick in a building, clumsy code may lead to an unstable structure. Today, we'll focus on enhancing the readability, maintainability, and performance of our code through refactoring.

Recapping Crucial Concepts

Let's briefly revisit a few key concepts using Go:

  • Code Smells: Indicators that our code needs refactoring, akin to clutter calling for cleanup.
  • Refactoring Techniques: We've familiarized ourselves with Extract Function, Rename Function, and Substitute Algorithm techniques in earlier lessons.
  • Go's Structs and Interfaces: We leverage structs for data organization and interfaces for defining behaviors, enabling cleaner and more maintainable code.
  • Code Decoupling and Modularization: Techniques to organize code effectively, minimizing dependencies and coupling, making the code easier to manage.

We'll use these concepts as guiding stars as we traverse the cosmos of refactoring.

Practice Problem 1: Taming a Complex Function

We'll start by rewriting a complex game score computation function in Go. Let's look at it:

Go
1package main 2 3type Player struct { 4 Power int 5} 6 7func ComputeScore(player Player, monsters []int) int { 8 score := 0 9 for _, monster := range monsters { 10 if player.Power > monster { 11 score += player.Power - monster 12 } else { 13 score -= player.Power - monster 14 } 15 } 16 return score 17}

This code uses an algorithm to adjust the score based on the player's and monsters' power. The parts player.Power > monster and player.Power - monster recur in this function, indicating room for refactoring. We'll apply the Extract Function and Rename Function techniques to untangle this:

  • We'll extract the scoring logic into a separate function, ScoreChange.
  • We'll rename the original function to ComputeGameScore.

With these adjustments, our improved code might look something like this:

Go
1package main 2 3type Player struct { 4 Power int 5} 6 7// New function to calculate score changes. 8func ScoreChange(power, monster int) int { 9 if power > monster { 10 return power - monster 11 } 12 return monster - power 13} 14 15// Refactored function to calculate the game score. 16func ComputeGameScore(player Player, monsters []int) int { 17 score := 0 18 for _, monster := range monsters { 19 score += ScoreChange(player.Power, monster) 20 } 21 return score 22}

This refactoring has simplified the function and made it easier to modify in the future.

Practice Problem 2: Refactoring with Interfaces and Struct Composition

Let's consider another example where the game has multiple types of monsters. Each monster type behaves differently when encountered by a player.

Go
1package main 2 3import "fmt" 4 5type Player struct { 6 Power int 7} 8 9func MonsterReaction(monsterType string, player Player) { 10 if monsterType == "ghost" { 11 if player.Power > 5 { 12 fmt.Println("The ghost flees in terror!") 13 } else { 14 fmt.Println("The ghost grumbles and attacks!") 15 } 16 } else if monsterType == "goblin" { 17 if player.Power > 3 { 18 fmt.Println("The goblin groans and retreats!") 19 } else { 20 fmt.Println("The goblin hacks with its sword!") 21 } 22 } 23 // more monster types... 24}

This scenario could also benefit from refactoring using OOP and Code Decoupling:

  • First, we'll introduce an interface Monster with a method Reaction that can be implemented by each type of monster.
  • Then, we'll create struct types Ghost and Goblin that implement the Monster interface and provide their own Reaction methods.

Under the revised structure, our game code would look like this:

Go
1package main 2 3import "fmt" 4 5type Player struct { 6 Power int 7} 8 9// Monster interface defines behavior for different monster types 10type Monster interface { 11 Reaction(player Player) 12} 13 14type Ghost struct{} 15 16func (g Ghost) Reaction(player Player) { 17 if player.Power > 5 { 18 fmt.Println("The ghost flees in terror!") 19 } else { 20 fmt.Println("The ghost grumbles and attacks!") 21 } 22} 23 24type Goblin struct{} 25 26func (g Goblin) Reaction(player Player) { 27 if player.Power > 3 { 28 fmt.Println("The goblin groans and retreats!") 29 } else { 30 fmt.Println("The goblin hacks with its sword!") 31 } 32} 33 34// Game function where slice of Monsters is managed 35func main() { 36 player := Player{Power: 4} 37 monsters := []Monster{Ghost{}, Goblin{}, Ghost{}, Goblin{}} 38 for _, monster := range monsters { 39 monster.Reaction(player) 40 } 41}

Now, our code dealing with multiple monsters is easier to manage and can be extended to accommodate more types of monsters by simply creating new structs that implement the Monster interface.

Wrapping Up and Looking Ahead

Phew! We've done an excellent job working through two practical problems, enhancing our refactoring skills, and learning how to identify code smells and apply refactoring techniques.

The more you practice, the better you'll become at spotting code that could benefit from refactoring. Brace yourself for more practice tasks, and remember, always keep your code lean and efficient!

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.