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