Now you know most of the basic skills in Go programming, like using functions, creating variables, and writing a simple Go program. Real programming is not just staying at small-sized programs; we'll soon build bigger applications that we can't put all in one function. So, the technique we will learn in this topic is functional decomposition. Essentially, it means splitting a big function into smaller ones.
Real-life implementation
Decomposing the main function could be a real-life Golang example, but we'll use an imaginary piece of code.
For the sake of simplicity, we choose a not-so-long piece of code, but the principles of decomposition are still relevant to it.
Once upon a time, no computer existed to help humans solve problems. Magicians had to risk their lives: they sought to meet a dragon, answered the creature's riddle, and if the answer pleased the dragon, it would help the magicians solve their problems. Now imagine yourself standing in front of a dragon. Try to answer the dragon's question: "What is the meaning of life?". What would be your actions?
Be brave and answer the dragon directly;
Trick it by saying: "I don't know";
Fleeing away: maybe the dragon won't get angry and burn you to dust.
Let's implement this story in Go:
package main
import "fmt"
func main() {
// Dragon:
fmt.Println("What is the meaning of life?")
var action string
fmt.Scanln(&action)
// Users choose action:
switch action {
case "Answer":
var ans string
fmt.Scanln(&ans)
// The dragon reacts:
if ans == "42" {
fmt.Println("You are right!")
} else if ans == "I don't know." {
fmt.Println("You are wrong!")
} else {
fmt.Println("You are so afraid!")
}
case "Trick":
// The dragon reacts:
fmt.Println("I like what you're trying to do, but I can't help those who are too lazy to think!")
case "Run":
// Dragon reacts:
fmt.Println("Arr!! How dare you run away!?")
}
}It's difficult to follow what's going on because so many things are happening in this code. Despite its complexity, this script would work as we expect: we could leave it as it is, and it would solve our problem perfectly.
But what if we want to extend the program, add more action, or find more ways to answer the dragon's question? Perhaps we could also expand the story by adding another character?
In general, the more functionality we want our program to provide, the more we need to decompose the main function into smaller functions.
Functional decomposition and repeated patterns
Functional decomposition helps us to divide a problem into several smaller problems. Every function solves a particular small task, and we can combine them to get the desired results.
If a code pattern repeats several times, we know it's a sign that we need to move it into a separate function. In the example below, we create a new function to get the user's reaction. We'll call it getUserChoice.
func getUserChoice() string {
var choice string
fmt.Scanln(&choice)
return choice
}
func main() {
fmt.Println("What is the meaning of life?")
action := getUserChoice()
switch action {
case "Answer":
ans := getUserChoice()
if ans == "42" {
fmt.Println("You are right!")
} else if ans == "I don't know." {
fmt.Println("You are wrong!")
} else {
fmt.Println("You are so afraid!")
}
case "Trick":
fmt.Println("I like what you're trying to do, but I can't help those who are too lazy to think!")
case "Run":
fmt.Println("Arr!! How dare you run away!?")
}
}Readability
Another reason to apply function decomposition is readability. If moving a piece of code to a separate function makes the whole program more readable, we should do it.
The dragon's reply is based on the user's answer. Let's create the dragonReplyToAnswer function:
func dragonReplyToAnswer(ans string){
if ans == "42" {
fmt.Println("You are right!")
} else if ans == "I don't know." {
fmt.Println("You are wrong!")
} else {
fmt.Println("You are so afraid!")
}
}
func main() {
fmt.Println("What is the meaning of life?")
action := getUserChoice()
switch action {
case "Answer":
ans := getUserChoice()
dragonReplyToAnswer(ans)
case "Trick":
fmt.Println("I like what you're trying to do, but I can't help those who are too lazy to think!")
case "Run":
fmt.Println("Arr!! How dare you run away!?")
}
}As you may notice, every refactoring step makes our main function less cumbersome. It's already a lot easier to read than in the beginning.
Extensibility
To make our program easier to extend in the future, we should isolate our switch case. The reason behind it is that adding a new action should not affect the interaction between the user and the dragon: it's just a new case, but the logic is still the same.
You might find this decomposition too detailed, but we'll leave room to extend the dragon's reaction to different user's actions:
func dragonReactToAction(action string) {
switch action {
case "Answer":
ans := getUserChoice()
dragonReplyToAnswer(ans)
case "Trick":
dragonDenyAnswer()
case "Run":
dragonBecomeAngry()
}
}
func dragonDenyAnswer() {
fmt.Println("I like what you're trying to do, but I can't help those who are too lazy to think!")
}
func dragonBecomeAngry() {
fmt.Println("Arr!! How dare you run away!?")
}
func main() {
fmt.Println("What is the meaning of life?")
action := getUserChoice()
dragonReactToAction(action)
}As we can see, the dragonDenyAnswer and dragonBecomeAngry functions are just making the program longer. However, they are very convenient if you want to add more functionality in the future.
The result
Let's see our main function after all applied modifications:
package main
import "fmt"
func main() {
fmt.Println("What is the meaning of life?")
action := getUserChoice()
dragonReactToAction(action)
}During the decomposition, we added more functions and made our program longer, but now our program has many advantages:
It is more readable;
It's easier to extend or change the logic in it;
We don't have repeated pieces of code in it.
For example, if we want to add a new option for the user to choose from (like Fight), we will add a new reaction to the dragonReactToAction function. If we want to add more ways of how the dragon gets angry, we will add some lines of code to the dragonBecomeAngry function. If we need to add more ways to react to the user's answer, we will add them to the dragonReplyToAnswer.
Function decomposition also helps us in testing. Because every part of the code is a separate component, we can pass the input data to each of them and determine if they work as expected or not.
Conclusion
We've learned about a technique called function decomposition. This skill allows us to:
Structure the code better;
Make our code more readable;
Extend the program or change the logic more easily;
Test each function more conveniently.