go – Where should interfaces be defined when using the factory pattern?-ThrowExceptions

Exception or error:

I am using a factory object FooFactory to create instances of a type Foo which has some private data members. I use the factory so that an object Bar which creates instances of Foo doesn’t need to provide (or even know about) these private data members: I first configure the factory with the necessary private stuff and then give Bar this configured factory.

I want Bar to use these Foo objects via an interface so I can mock them using gomock and test that Bar uses them correctly. From what I’ve read about interfaces in go, the best practise is to define the interfaces where they’re used rather than where the underlying types are defined, so I have a Fooer interface in the same package as my Bar object, and Bar uses this Fooer interface wherever it expects a Foo (or later, a MockFoo).

I also want Bar to use the FooFactory via interface for the same reason, so I can mock it and assert that it creates Foo objects when I expect it to. And again, I define an interface in Bars package FooBuilder, which the underlying FooFactory implicitly implements.

Now the issue is that the FooFactory returns concrete Foo objects since they are both in the same Foo package and shouldn’t know about Bar‘s locally defined interfaces. However, I want the FooBuilder to build objects of type Fooer rather than objects of type Foo because it shouldn’t need to know about the underlying types. Go does not allow this since the return types differ and it says that the FooFactory does not implement FooBuilder.

Here a minimal reproduction without the package structure described above:

type Foo struct{}

func (f *Foo) FooMethod() {}

type FooFactory struct{}

func (ff *FooFactory) Build() *Foo {
    return &Foo{}
}

type Fooer interface {
    FooMethod()
}

type FooBuilder interface {
    Build() Fooer
}

func main() {
    f := &Foo{}
    ff := &FooFactory{}

    var fooer Fooer
    var fooBuilder FooBuilder

    fooer = f
    fooBuilder = ff     // << ERROR
}

Go complains:

cannot use ff (type *FooFactory) as type FooBuilder in assignment:
    *FooFactory does not implement FooBuilder (wrong type for Build method)
        have Build() *Foo
        want Build() Fooer

My question is essentially, am I doing this right? After a background in professional C++ and Java, I’m trying to embrace go’s notion of implicit interface satisfaction but this just feels weird. The fact that this isn’t easy to do makes me think I’m doing something wrong, but everything I read online about go’s interfaces says to define interfaces close to where they’re used, which is what I’m really trying to do.

How to solve:

Conceptually

You’re correct in your method. Though if the setup of Foo isn’t complex, I’d drop the factory.

Foo and Fooer may or may not be in different packages. I’d start by putting them in the same package until it doesn’t make sense anymore.

The way I normally set these situations up is:

package foo

func NewFoo() *Foo {
    return &Foo{}
}

type Foo struct{}

func (f *Foo) FooMethod() {}

type Fooer interface {
    FooMethod()
}

And a consumer with:

package bar

func FooUser(f Fooer) {
    //Do something
}

And putting it together with:

package main

import (
    "foo"
    "bar"
)

func main() {
    f := foo.NewFoo()
    bar.FooUser(f)
}

This way you can test the foo package as a unit and test the bar package with any struct that implements Fooer.

You’ll find that quite a few core packages are built this way as well.

Compiler error

The bit you’re are missing is that FooFactory isn’t actually implementing the FooBuilder interface.

FooBuilder interface declares a method that returns Fooer but the method signature for FooFactory returns a pointer to Foo.

func (ff *FooFactory) Build() *Foo

So you’re declaring method Build but you’re returning the wrong type, so it isn’t recognized as an implementation.

To fix the error, change the signature of Build‘s implementation to:

func (ff *FooFactory) Build() Fooer

Leave a Reply

Your email address will not be published. Required fields are marked *