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 golang-nuts+unsubscr...@googlegroups.com.
To view this discussion on the web visit 
https://groups.google.com/d/msgid/golang-nuts/b859c089-ff6d-4f56-a444-9d477eb8d721n%40googlegroups.com.

Reply via email to