I created this in the playground: https://play.golang.org/p/Jj4UhA8Yn7
I'll paste the code below as well.
The question revolves around whether my approach on composability is something I should consider as viable, good Go code, or if I'm thinking about it incorrectly and should consider something more in line with idiomatic Go.
My goal is to use this pattern to create "logic" tiers that decorate the underlying layer with additional logic that the wrapped layer should not need to know about.
As a cursory example I might have these "layers"
I hope I've made myself somewhat clear and that the example code below will help illustrate it better than my words above.
package main
import (
"errors"
"fmt"
)
//An interface
type Weird interface {
Name() string
SetName(name string) error
Age() int
SetAge(age int) error
}
//Simple struct to hold data
type SimpleWeird struct {
name string
age int
}
func (s *SimpleWeird) Name() string {
return s.name
}
func (s *SimpleWeird) SetName(name string) error {
s.name = name
return nil
}
func (s *SimpleWeird) Age() int {
return s.age
}
func (s *SimpleWeird) SetAge(age int) error {
s.age = age
return nil
}
//RegularWeird encapsulates some "business" logic within it's methods
//and would be considered normal logic flow
type RegularWeird struct {
Weird
}
func (r *RegularWeird) SetName(name string) error {
if len(name) > 5 {
return errors.New("Regulars can't set a name longer than 5 characters long")
}
return r.Weird.SetName(name)
}
func (r *RegularWeird) SetAge(age int) error {
if age > 80 {
return errors.New("Regulars can't set an age above 80")
}
return r.Weird.SetAge(age)
}
//AdminWeird encapsulates some admin "business" logic within it's methods
//It would be considered admin logic flow/rules
type AdminWeird struct {
Weird
}
//AdminWeirds don't have their own SetName. If they
//Wrap a SimpleWeird then any name size is allowed (desired behavior)
//If the wrap a regular weird then the regular weird's logic is enforced
func (a *AdminWeird) SetAge(age int) error {
if age > 100 {
return errors.New("Admins can't set an age above 100")
}
return nil
}
func NewAdminWeird() Weird {
return &AdminWeird{Weird: &SimpleWeird{}}
}
func NewRegularWeird() Weird {
return &RegularWeird{Weird: &SimpleWeird{}}
}
//This one doesn't make sense for this example but I wanted to show
//the composability aspect of this. I would be creating chainable
//interfaces that each handle different unrelated logic
func NewAdminRegularWeird() Weird {
return &AdminWeird{Weird: NewRegularWeird()}
}
func checkErr(err error) {
if err != nil {
fmt.Println(err)
}
}
func main() {
var err error
r := NewRegularWeird()
a := NewAdminWeird()
ar := NewAdminRegularWeird()
fmt.Println("Regular output:")
err = r.SetName("test")
checkErr(err)
err = r.SetAge(5)
checkErr(err)
err = r.SetName("something-longer")
checkErr(err)
err = r.SetAge(90)
checkErr(err)
fmt.Println("Admin output:")
err = a.SetName("test")
checkErr(err)
err = a.SetAge(5)
checkErr(err)
err = a.SetName("something-longer")
checkErr(err)
err = a.SetAge(101)
checkErr(err)
fmt.Println("AdminRegular output:")
err = ar.SetName("test")
checkErr(err)
err = ar.SetAge(5)
checkErr(err)
err = ar.SetName("something-longer")
checkErr(err)
err = ar.SetAge(90)
checkErr(err)
}
I'm not sure about idiomatic, but I think you've effectively used interfaces, structures and constructor methods to achieve your goal. Here are a few things you might change if it suits you.
1) Let the implementations deal with the setters.
//An interface
type Weird interface {
Name() string
Age() int
}
Implementers could choose to implement a SetName or SetAge, but they could just as well initialize with values or assign directly to the structure. For good or bad, the business rules about min/max age, etc. are not enforceable by the interface, so the interface methods seems like extra code to me.
2) Create an Admin interface, though you haven't really defined any unique behavior of Admins in your example:
type Admin interface {
Weird
SomeAdminMethod() string
}
3) Your use of the interface as the return value for all of the New* functions is a bit confusing to me. It seems like that would be more suited to a single NewWeird function like:
func NewWeird(flavor string) (Weird, error) {
switch flavor {
case "regular":
return &RegularWeird{Weird: &SimpleWeird{}}, nil
case "admin":
return &AdminWeird{Weird: &SimpleWeird{}}, nil
case "regularadmin":
return &RegularWeird{Weird: &NewWeird(regular)}, nil
case default:
return nil, errors.Error("unknown weird type")
}
}
The use of the interface as the return is handy if you want to initialize a bunch of heterogeneous Weirds and stuff them into a []Weird
or something like that.
Hope it helps...