I have the following type:
type entity struct {
components []Component
}
func NewEntity(componentsLength ComponentType) Entity {
return &entity{
components: make([]Component, componentsLength),
}
}
When coding, I noticed the following repeated pattern:
func (e *entity) HasComponent(components ...Component) bool {
for _, c := range components {
if e.components[c.Type()] == nil {
return false
}
}
return true
}
func (e *entity) HasAnyComponent(components ...Component) bool {
for _, c := range components {
if e.components[c.Type()] != nil {
return true
}
}
return false
}
Go seems okay with a lot of duplicated code when you're writing similar functions for different types. However, in this case the types are the same. I'm still having trouble refactoring the code to generalize the use of the for
loop and if
statement.
How can I refactor these functions so that the list handling is common, but the boolean operators and values are abstracted out? And should I?
Yes, I think you should refactor this code, not merely to make it more compact, but to better express the set-theoretical nature of the operations you're performing. Both functions are asking a question about the intersection of e.components
and components
.
What you're essentially saying with HasComponent
—which should probably be called HasAllComponents
—is that the intersection of e.components
and components
is equal to components
. In other words, the size of the intersection is the same as the size of components
.
As for HasAnyComponent
, what you're saying is that the size of the intersection is at least one.
Both functions can be expressed succinctly by testing the size of the intersection, which you can obtain with, say, a function that has this signature:
func (e *entity) CountComponents(components ...Component) int
However, your original code has short-circuit logic that stops looking at further components once it knows the answer. You can incorporate such a short-circuited loop into a function with this signature:
func (e *entity) HasMinimumComponents(minimum int, components ...Component) bool
In the loop, increment a count
variable whenever e.components[c.Type()] != nil
. Return true
as soon as count >= minimum
.
Now you can efficiently implement HasAllComponents
with a one-line body:
return entity.HasMinimumComponents(len(components), components...)
And the body of HasAnyComponent
becomes:
return entity.HasMinimumComponents(1, components...)
Below is a program that implements this idea with slightly different data types. I defined a more abstract kind of Entity
that contains a map[int]bool
. You should have no trouble adapting the idea to your own program.
package main
import (
"fmt"
)
type Entity struct {
ValueMap map[int]bool
}
func MakeEntity(values ...int) *Entity {
entity := Entity{map[int]bool{}}
for _, value := range values {
entity.ValueMap[value] = true
}
return &entity
}
func (entity *Entity) HasMinimumValues(minimum int, values ...int) bool {
count := 0
if minimum == 0 {
return true
}
for _, value := range values {
if entity.ValueMap[value] {
count++
if count >= minimum {
return true
}
}
}
return false
}
func (entity *Entity) HasAllValues(values ...int) bool {
return entity.HasMinimumValues(len(values), values...)
}
func (entity *Entity) HasAnyValue(values ...int) bool {
return entity.HasMinimumValues(1, values...)
}
func main() {
entity := MakeEntity(1, 3, 5, 7, 9)
fmt.Printf("%t
", entity.HasAllValues(3, 9))
fmt.Printf("%t
", entity.HasAllValues(3, 9, 12))
fmt.Printf("%t
", entity.HasAnyValue(9, 12, 15))
fmt.Printf("%t
", entity.HasAnyValue(12, 15))
}