Introduction
We all have used naked boolean parameters at some point during our lives, specially when we were starting to learn to program. Other times we add them when refactoring code, or we are just hacking somthing together. However, naked boolean parameters are a code smell and signs of a bad code.
First, they hurt the readability of the code and since we spend the vast majority of our time reading code, that is a big reason to avoid naked boolean parameters. Okay… some IDEs add nice information like the name of the parameters passed, but you do not have that when you are doing code reviews :D
Second, naked boolean parameters can be confusing when several of them are present in the same parameter list. Specially, if they are all together one after the other.
func openRemoteFile(url string, allowRetries, allowRedirects bool) {
if allowRetries {
fmt.Println("Redirects enabled")
}
if allowRedirects {
fmt.Println("Retries enabled")
}
}
When calling that method you will see something like the following
openRemoteFile("sftp://myserver.com/wololo.txt", true, false)
As you can see, the first thing you will have to do upon reading this code is to go to the definition of the function, just to know which parameter is being passed as true and which one is being passed as false.
Wrapper functions
One way to improve the readability of the function is to create wrapper functions that set specific values for the boolean parameters. This is useful if these wrapper functions are the public way to call the non-shared function that contains the naked boolean parameters.
func OpenRemoteFile(url string) {
openRemoteFile(url, false, false)
}
func OpenRemoteFileWithRetries(url string) {
openRemoteFile(url, true, false)
}
func OpenRemoteFileWithRedirects(url string) {
openRemoteFile(url, false, true)
}
func OpenRemoteFileWithRetriesAndRedirects(url string) {
openRemoteFile(url, true, true)
}
As you can imagine, the amount of these wrapper functions grows exponentially based on the number of boolean parameters. This option is only viable if the number of boolean parameters is low and the possible viable combinations of them are small.
Named booleans
Another way to improve the readability of the function is to use named booleans.
const (
allowRetries = true
disallowRetries = false
allowRedirects = true
disallowRedirects = false
)
Now, the call to openRemoteFile
will look something like this.
openRemoteFile("sftp://myserver.com/wololo.txt", allowRetries, disallowRedirects)
The main problem with this solution is that it only fixes the readability issue, but the problem of mixing the boolean parameters order still in place.
This is a totally valid code, but it does not do what you would expect by reading the parameters.
openRemoteFile("sftp://myserver.com/wololo.txt", allowRedirects, disallowRetries)
Bitmask
Bitmask is only an option if all the possible combinations of the boolean parameters are viable. Being viable means that all the combinations make sense.
type RemoteMark uint8
const (
Redirects RemoteMark = 1 << iota
Retries
)
func openRemoteFile(url string, mask RemoteMark) {
if mask & Redirects == Redirects {
fmt.Println("Redirects enabled")
}
if mask & Retries == Retries {
fmt.Println("Retries enabled")
}
}
If the openRemoteFile
function signature is changed to match the one above, then it can be called like
openRemoteFile("sftp://myserver.com/wololo.txt", Redirects | Retries)
This solution is both more readable and less error prone. It avoids the usage of multiple boolean parameters, however, this approach case can only be used if all the combinations of values of the boolean parameters are viable.
Typed booleans
Last, but not least, typed booleans. You can think of typed booleans as an improvement over named booleans. This is because typed booleans not only solve the readability problem, like names booleans, but they solve the problem of mixing the order of the boolean parameters as well.
type RedirectEnum bool
type RetryEnum bool
const (
DisallowRedirects RedirectEnum = false
AllowRedirects
DisallowRetries RetryEnum = false
AllowRetries
)
func openRemoteFile(url string, redirects RedirectEnum, retries RetryEnum) {
if redirects == AllowRedirects {
fmt.Println("Redirects enabled")
}
if retries == AllowRetries {
fmt.Println("Retries enabled")
}
}
If the openRemoteFile
function signature is changed to match the one above, then it can be called like
openRemoteFile("sftp://myserver.com/wololo.txt", AllowRedirects, DisallowRetries)
The snippet above shows code that is correct and readable. In addition, if you mix the order like in the following example
openRemoteFile("sftp://myserver.com/wololo.txt", DisallowRetries, AllowRedirects)
When compiling it, an error similar to the following would be displayed
./prog.go:29:17: cannot use DisallowRetries (type RetryEnum) as type RedirectEnum in argument to openRemoteFile
./prog.go:29:17: cannot use AllowRedirects (type RedirectEnum) as type RetryEnum in argument to openRemoteFile
Conclusion
Writing readable code is one of the most important parts of software development, as we spend way more time reading code than writing it. Therefore, if you make the effort to use the patterns described above the readability of your code base will increase.
I strongly recommend the usage of typed booleans and bitmasks when possible. They do not only improve the readability of the code, but they also improve the correctness of the code.