I am writing this to see what everyone thinks about a solution I have for
the composite pattern.
The composite pattern is when 0 or more instances of some interface X can
act together as a single instance of interface X. For instance you may have
a Filter interface that filters instances of class Foo like so.
type Filter interface {
IsIncluded(ptr *Foo) bool
}
Filter can follow the composite pattern like so:
type sliceFilter []Filter
func (s sliceFilter) IsIncluded(ptr *Foo) bool {
for _, f := range s {
if !f.IsIncluded(ptr) {
return false
}
}
return true
}
The sliceFilter returns true for a Foo instance if and only if all the
Filters in the slice filter return true for that Foo instance.
Notice that sliceFilter lets a collection of Filters act as a single Filter
instance.
You can also have a Filter instance the represents 0 filters like this.
type nilFilter struct {
}
func (n nilFilter) IsIncluded(ptr *Foo) bool {
return true
}
When designing an API around Filters you may include a method called
Compose that creates a Filter instance out of a bunch of existing Filter
instances. A naive implementation might look like this
func Compose(filters ...Filter) Filter {
return sliceFilter(filters)
}
While this works, it is not great because if the caller passes a Filter
slice to Compose and later changes that slice, they unwittingly change the
returned composite Filter as a side effect. A better implementation may
look like this.
func Compose(filter ...Filter) Filter {
result := make(sliceFilter, len(filter))
copy(result, filter)
return result
}
This is better because it makes a defensive copy of the slice. If the
caller passes a []Filter to Compose and changes it, the returned composite
Filter works as expected. But this solution isn't optimal either because it
always allocates and returns a slice no matter what the caller passes to
it. If the caller passes a single Filter to Compose, Compose should return
that Filter as is, not a slice. If the caller passes no arguments to
Compose, Compose should return the nilFilter instance whose IsIncluded
method always returns true. If the caller passes a bunch of nil Filters to
Compose, Compose should return the nil Filter instance. If the caller
passes a bunch of nilFilters and one non nil Filter to Compose, Compose
should return the one non nil Filter. The only time Compose should
allocate and return a slice is if it is passed 2 or more non nil Filters.
If caller passes 2 or more Filters that are slices, Compose should flatten
those out into a single slice rather than returning a slice of slices.
common.Join in github.com/keep94/common handles all these edge cases
automatically. In addition to a slice type, common.Join requires a nil
instance which represents 0 of some interface X.
Compose can be written like this
func Compose(filter ...Filter) Filter {
return common.Join(filter, sliceFilter(nil), nilFilter{}).(Filter)
}
When written like this, Compose will always do the right thing. It will
only return a newly allocated slice if 2 or more arguments passed to it are
non nil Filters. If it gets just one non-nil Filter, it simply returns
that filter unchanged. If it gets 0 filters, it just returns the
nilFilter{} instance.
In conclusion, there are some edge cases to consider when implementing the
composite pattern. common.Join can address all these edge cases.
--
You received this message because you are subscribed to the Google Groups
"golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
To view this discussion on the web visit
https://groups.google.com/d/msgid/golang-nuts/b859c089-ff6d-4f56-a444-9d477eb8d721n%40googlegroups.com.