You might want to extract common functionality of those interface methods to separate classes/functions and only implement handlers for logic specific to your components.
A slightly simplified example:
type Component interface {
OnRender(ctx RenderContext)
GetComponents() []Component
AddComponent(Component)
}
Approach #0: Inherit methods that are implemented the same everywhere using anonymous fields
type BaseComponent struct {
components []Component
}
func(c BaseComponent) GetComponents() []Component {
return c.components
}
func(c *BaseComponent) AddComponent(a Component) {
c.components = append(c.components, a)
}
Approach #1: Extract the common logic to a free function
func Render(c Component, ctx RenderContext) {
// implement common render logic here
// call component specific handler
c.OnRender(ctx)
// update children
for _, child := range c.GetComponents() {
Render(child, ctx)
}
}
Approach #2: Extract the common logic to a dedicated "class" (struct)
type ComponentRenderer struct {
// store common information required for rendering
}
func(cr *ComponentRenderer) Render(c Component, ctx RenderContext) {
// implement common render logic here
// call component specific handler
c.OnRender(ctx)
// update children
for _, child := range c.GetComponents() {
cr.Render(child, ctx)
}
}
Example for a final component:
type Button struct {
BaseComponent
// button specific fields
}
func(b *Button) OnRender(ctx RenderContext) {
// implement button specific rendering code here
}
Addendum for Approach #2: If you want, you can declare ComponentRenderer
as an interface and implement specific renderers for different platforms (e.g. one for Windows, one for Linux, one for a web page, one for an ingame GUI, ...).
You can mix and match these different approaches for different purposes.
Note: You could try to implement a Render
method on BaseComponent
- but be aware that there is no virtual dispatch and no method overriding on structs in Go (so the Render
method needs to somehow get the information about which handler to call from somewhere else). So I would only recommend Approach #0 for cases where every implementation uses the exact same logic.