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"
- interface layer - a set of interfaces that define the "model"
- simple struct layer - just holds data from the database meets the interfaces above
- validation layer - wraps around an interface from the interface layer and validates incoming data against some validation rules before it then forwards the method calls to the wrapped layer below. It also meets the same interface as the layer it is wrapping.
- translation layer - same as number 3, translates any text being accessed by first calling the layer it is wrapping, then translates the text, and returns the translated text. it will meet the same interface of the object it wraps. Any methods unrelated to getting info will be forwarded to the underlying layer transparently.
I hope I've made myself somewhat clear and that the example code below will help illustrate it better than my words above.
The example code from the playgroud
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)
}