Previously I wrote about using struct embedding to achieve pseudo inheritance for code reuse. In this post, I want to explore an example of what I think is an anti pattern as a result of the lack of inheritance and how I think it could be better.

In the absence of inheritance in golang, it is very tempting to turn fields into functions of an interface.

As an motivating example, consider the following:

type Event struct {
    id int64
    name string
}
type BlogEvent struct { Event }
type OfflineEvent struct { Event }

We want to be able to do something to all the different type of events. For example, uploading them.

func upload(e Event) {
    // code to upload event to our database.
}

func main() {
    upload(BlogEvent{}) // does not compile because it is not true sub typing.
    upload(OfflineEvent{}) // does not compile because it is not true sub typing.
}

But it does not work because BlogEvent is not a Event. One temptation is to turn Event into an interface.

type Event interface {
    ID() int64
    Name() string
}

var _ Event = BlogEvent{}
var _ Event = OfflineEvent{}

But now it’s a bit awkward to deal with fields that only exists in one type but another another. For example, what if an OfflineEvent has a GPSLocation and a BlogEvent has a URL, and the upload code has to deal with these.

// do we add them to Event interface?
type Event interface {
    ID() int64
    Name() string
    URL() string // return empty if irrelevant
    GPSLocation() Location // only for OfflineEvent
}
func (OfflineEvent) URL() string { return "" } // awkward and ugly
func upload(e Event) {
    id := e.ID() // use this somewhere
    // search for GPS location and return human readable address or landmark
    if e.GPSLocation() != nil {
        // ... ugly
    }
}

I think the best we can do here is to create a facade and have upload redirect the call.

type UploadableEvent interface {
    Upload()
}

// let the compiler help us assert interface satisfaction
var _ UploadableEvent = OfflineEvent{}
func (e OfflineEvent) Upload() {
    id := e.id // use this somewhere like before
    // use gpsLocation without shame
}

func upload(e UploadableEvent) {
    e.Upload()
}