samedi 18 août 2018

Golang Transactional API design

I'm trying to follow Clean Architecture using Go. The application is a simple image management application.

I'm wondering how to best design the interfaces for my Repository layer. I don't want to combine all repository methods into one single big interface, like some examples I found do, I think in Go small interfaces are usually preferred. I don't think the usecase code concerning managing images needs to know that the repository also stores users. So I would like to have UserReader, UserWriter and ImageReader and ImageWriter. The complication is that the code needs to be transactional. There is some debate where transaction management belongs in Clean Architecture, but I think the usecase-layer needs to be able to control transactions. What belongs in a single transaction, I think, is a business rule and not a technical detail.

Now the question is, how to structure the interfaces?

Functional approach

So in this approach, I open a transaction, run the provided function and commit if there are no errors.

type UserRepository interface {
    func ReadTransaction(txFn func (UserReader) error) error
    func WriteTransaction(txFn func (UserWriter) error) error
}

type ImageRepository interface {
    func ReadTransaction(txFn func (ImageReader) error) error
    func WriteTransaction(txFn func (ImageWriter) error) error
}

Problems: No I can't easily write a user and an image in a single transaction, I would have to create an extra UserImageRepository interface for that and also provide a separate implementation.

Transaction as repository

type ImageRepository interface {
    func Writer() ImageReadWriter
    func Reader() ImageReader
}

I think this would be rather similar to the functional approach. It wouldn't solve the problem of combined use of multiple repositories, but at least would make it possible by writing a simple wrapper.

An implementation could look like this:

type BoltDBRepository struct {}
type BoltDBTransaction struct { *bolt.Tx }
func (tx *BoltDBTransaction) WriteImage(i usecase.Image) error
func (tx *BoltDBTransaction) WriteUser(i usecase.User) error
....

Unfortunately, If I implement the transaction methods like this:

func (r *BoltDBRepository) Writer() *BoltDBTransaction
func (r *BoltDBRepository) Reader() *BoltDBTransaction

because this does not implement the ImageRepository interface, so I'd need a simple wrapper

type ImageRepository struct { *BoltDBRepository }
func (ir *ImageRepository) Writer() usecase.ImageReadWriter
func (ir *ImageRepository) Reader() usecase.ImageReader

Transaction as a value

type ImageReader interface {
    func WriteImage(tx Transaction, i Image) error
}

type Transaction interface { 
    func Commit() error
}

type Repository interface {
    func BeginTransaction() (Transaction, error)
}

and a repository implementation would look something like this

type BoltDBRepository struct {}
type BoltDBTransaction struct { *bolt.Tx }

// implement ImageWriter
func (repo *BoltDBRepository) WriteImage(tx usecase.Transaction, img usecase.Image) error {
  boltTx := tx.(*BoltDBTransaction)
  ...
}

Problems: While this would work, I have to type assert at the beginning of each repository method which seems a bit tedious.

So these are the approaches I could come up with. Which is the most suitable, or is there a better solution?

Aucun commentaire:

Enregistrer un commentaire