Catégories
Programmation

Why reflection can be a smell, even in Go.

Reflection is often viewed as a Smell in Java, C#, etc. Even if Golang have interface{} in too many places, you are using a static typed language, how did you come not to know which type do you use?

Some usages are perfectly legal, but it’s quite regularly a smell, especially when if multiply or the switch grows.

Almost every time you see some typeof (Java) or some .(Type) (Go), you should search for an interface refactoring.

Example

Below, you can see the same code, one with a type assertion, the second with a type switch. Do you smell it?

Type Assertion

package main

import (
	"fmt"
	"log"
	"time"
)

type Work struct{}

func (w Work) Work() {}

type Leisure struct{}

func (l Leisure) Chill() {}

func main() {
	activity := FindSomethingToDo()

	if a, ok := activity.(Work); ok {
		fmt.Println("doing: work")
		a.Work()
	} else if a, ok := activity.(Leisure); ok {
		fmt.Println("doing: leisure")
		a.Chill()
	} else {
		log.Fatalln("doing: well...")
	}
}

func FindSomethingToDo() interface{} {
	if time.Now().Weekday() == time.Sunday {
		return Leisure{}
	}
	return Work{}
}

Type Switch

package main

import (
	"fmt"
	"log"
	"time"
)

type Work struct{}

func (w Work) Work() {}

type Leisure struct{}

func (l Leisure) Chill() {}

func main() {
	activity := FindSomethingToDo()

	switch a := activity.(type) {
	case Work:
		fmt.Println("doing: work")
		a.Work()
	case Leisure:
		fmt.Println("doing: leisure")
		a.Chill()
	default:
		log.Fatalln("doing: well...")
	}
}

func FindSomethingToDo() interface{} {
	if time.Now().Weekday() == time.Sunday {
		return Leisure{}
	}
	return Work{}
}

Refactoring

You can see that in the structure, and in the usage, Work and Leisure are very close. In fact, it’s easy to find a common interface. I propose this one:

type Activity interface {
	Title() string
	Do()
}

Title() will be easier to use and to test instead of a literal. You can also implement String() but I prefer a separated function with a special meaning. Sometimes, String() is only implemented to have a nice display in the debugger, so you are uncertain if it is ok to use it like that for a domain related function.
Do() is really close to the Activity name and a good generalization of Work() and Chill().

main() is then refactored to:

func main() {
	activity := FindSomethingToDo()
    fmt.Printf("doing: %s\n", activity.Title())
	activity.Do()
}

Cleaner, no? And when it is clean, there is less smell. 😉

Some can say that we lose the log.Fatalln() part, and then, a part of the domain definition and a safety net.
I disagree.
We gain safety cause now we know that we cannot receive an “unknown” type. If you respect the Liskov Substitution Principle, nothing can go wrong.
Well, a lot can go wrong in a software, but you know what I mean.

And to stay in the SOLID principle, it is okay to add some new implementation, but beware to keep this interface small and cohesive.

Here is the complete code:

package main

import (
	"fmt"
	"time"
)

type Activity interface {
	Title() string
	Do()
}

type Work struct{}

func (w Work) Title() string {
	return "work"
}

func (w Work) Do() {}

type Leisure struct{}

func (l Leisure) Title() string {
	return "leisure"
}

func (l Leisure) Do() {}

func main() {
	activity := FindSomethingToDo()
	fmt.Printf("doing: %s\n", activity.Title())
	activity.Do()
}

func FindSomethingToDo() Activity {
	if time.Now().Weekday() == time.Sunday {
		return Leisure{}
	}
	return Work{}
}

Do not stay too long admiring your clean code, you need to deliver.

Edit 2022-01-11: Thanks to the Reddit readers to point that I need to talk about reflection and not type checking.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *