Idiomatic go interfaces

774 views
Skip to first unread message

Travis Keep

unread,
Feb 25, 2013, 11:33:55 AM2/25/13
to golan...@googlegroups.com
I am working on a multi tier web app designed to run on top of multiple database backends.

I have:

type DataStore interface {
  UserById(...) error
  UserByName(...) error
  EntryById(...) error
  EntriesByUser(...) error
  ...
}

Of course this DataStore interface is relatively large as it contains all the operations that my webapp can do on a database backend.  All the documentation is in this interface. Various implementations have a single New() method that returns an instance of this DataStore interface.  Passing DataStore interfaces around seemed like a good idea until I had to write a test.  I wrote a UnsupportedDataStore which returns UnsupportedOperationError for each method to help with writing tests but that just doesn't seem very go like.  

So here is my question:  Should I have each package define its own DataStore interface containing only the needed methods for that package?  For instance, a package that only needs to fetch a user by Id would define only UserById() in its DataStore interface.  Should I get rid of the master DataStore interface which contains all the methods?  If so, where should the documentation go?


Jeremy Wall

unread,
Feb 25, 2013, 11:48:40 AM2/25/13
to Travis Keep, golang-nuts
I would split it up into smaller interfaces. This has a few benefits. You can if you choose use a different datastore implementation for each piece. The objects or functions that consume these interfaces don't claim to have larger api surface area than they need which might help prevent leaky abstractions. You can always combine multiple interfaces into larger interfaces using interface embedding if you need it later.

The interactions between your interface implentations and their consumers is more clearly documented. The downside is that you will probably not be returning an interface type from your constructors anymore.

Another option is to see if there is a smaller more generic api that supports your larger set of operations. Then you can have a concrete wrapper api that uses a more low level datastore api.




--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
For more options, visit https://groups.google.com/groups/opt_out.
 
 

Kamil Kisiel

unread,
Feb 25, 2013, 12:25:35 PM2/25/13
to golan...@googlegroups.com
I use similar patterns in some projects. As Jeremy mentioned, you could split it up in to smaller interfaces if possible, but that gets complicated when you have functions which need a combination of functionality. 

For one project I've written an in-memory implementation of the datastore that simply keeps the data in a bunch of maps and slices. It's not particularly efficient, but it implements the interface. I test it with the same functions as I test my database-backed datastore to ensure it acts the same way. Then for testing any functions which need a datastore I can pass the in-memory version around instead if I don't want to use a database.

Travis Keep

unread,
Feb 25, 2013, 12:49:37 PM2/25/13
to golan...@googlegroups.com
Thanks for the ideas everyone. I don't like large interfaces, but I think splitting my interface up will be complicated. I like the idea of an in-memory implementation. I already have a sqlite package for my interface, so I can set the file to ":memory:" to get an in-memory implementation.  

Kyle Lemons

unread,
Feb 25, 2013, 1:05:45 PM2/25/13
to Travis Keep, golang-nuts
You could try to get a higher-level view of your requirements and try to distill that into a more coherent interface.  Perhaps:

type DataStore interface {
// Get returns the corresponding `what` to the `value` in the `given` series.
Get(what, given, value string)
}

which would allow you to then implement a top-level function
UserByID(ds DataStore, id string) (user string) {
  return ds.Get("user", "id", id)
}


David DENG

unread,
Feb 25, 2013, 2:11:28 PM2/25/13
to golan...@googlegroups.com
I think embedding an UnsupportedDataStore into implemetation is just fine. It can be an empty struct (without any fields), and the embedding has no extra memory at all.

David

Shane Hansen

unread,
Feb 26, 2013, 3:25:11 AM2/26/13
to David DENG, golan...@googlegroups.com
imho this isn't a question of idiomatic go so much as it is a question of good design practices for data access.

I'm pretty opinionated, but basically every really good ORM that I've worked with has the following properties:
1) Each object is  a POO (plain old object) or interface. You have a user struct that has a list of entries, and an Entry struct these could
be interfaces if you want to make the implementation a bit more dynamic/lazy.

2) Some object that represents a database session is responsible for loading these objects by id and persisting them to the database.


I'd suggest you look at the docs for sqlalchemy or (if you have the time and ignore the xml) hibernate.


Travis Keep

unread,
Feb 27, 2013, 9:45:05 AM2/27/13
to golan...@googlegroups.com
Looking at my Handlers, I discovered that about half of my handlers use just 1 method of my datastore while the vast majority of the remaining handlers use 2 methods of my datastore.  So I defined my Store interface with sub-interfaces where each sub-interface has only one method.

type Store interface {
  EntryByIdGetter
  EntryUpdater
  UserByIdGetter
  UserByNameGetter
  UserByNameUpdater
  EntriesByUserGetter
  .....
}

type EntryByIdGetter interface {
  EntryById(...) error
}

...

Then each implementation package can have a single exported New method like so.

func New(...) db.Store

Then my Handlers can specify exactly what they need like so.

type ViewEntryHandler struct {
  Store db.EntryByIdGetter
  ...
}

For Handlers that use two methods, I can define an interface in that handler like so.

type Store interface {
  db.EntryByIdGetter
  db.EntryUpdater
  ...
}

type EditEntryHandler struct {
  Store Store
  ...
}

This makes testing super easy and my documentation doesn't get cluttered with something like a UnsupportedOperationStore and its self documenting because it is easy to tell exactly what each handler does.  I think this is where go really shines.



On Monday, February 25, 2013 8:33:55 AM UTC-8, Travis Keep wrote:
Reply all
Reply to author
Forward
0 new messages