The standard library solves this problem in multiple ways:
1) Without a “Central” Registry
Example of this is the different hash algorithms. The crypto
package just defines the Hash
interface (the type and its methods). Concrete implementations are in different packages (actually subfolders but doesn’t need to be) for example crypto/md5
and crypto/sha256
.
When you need a “hasher”, you explicitly state which one you want and instantiate that one, e.g.
h1 := md5.New()
h2 := sha256.New()
This is the simplest solution and it also gives you good separation: the hash
package does not have to know or worry about implementations.
This is the preferred solution if you know or you can decide which implementation you want prior.
2) With a “Central” Registry
This is basically your proposed solution. Implementations have to register themselves in some way (usually in a package init()
function).
An example of this is the image
package. The package defines the Image
interface and several of its implementations. Different image formats are defined in different packages such as image/gif
, image/jpeg
and image/png
.
The image
package has a Decode()
function which decodes and returns an Image
from the specified io.Reader
. Often it is unknown what type of image comes from the reader and so you can’t use the decoder algorithm of a specific image format.
In this case if we want the image decoding mechanism to be extensible, a registration is unavoidable. The cleanest to do this is in package init()
functions which is triggered by specifying the blank identifier for the package name when importing.
Note that this solution also gives you the possibility to use a specific implementation to decode an image, the concrete implementations also provide the Decode()
function, for example png.Decode()
.
So the best way?
Depends on what your requirements are. If you know or you can decide which implementation you need, go with #1. If you can’t decide or you don’t know and you need extensibility, go with #2.
…Or go with #3 presented below.
3) Proposing a 3rd Solution: “Custom” Registry
You can still have the convenience of the “central” registry with interface and implementations separated with the expense of “auto-extensibility”.
The idea is that you have the interface in package pi
. You have implementations in package pa
, pb
etc.
And you create a package pf
which will have the “factory” methods you want, e.g. pf.NewClient()
. The pf
package can refer to packages pa
, pb
, pi
without creating a circular dependency.