wild-workouts-go-ddd-example
wild-workouts-go-ddd-example copied to clipboard
Couple of ideas
While continuing to work on ddd-gen (example) I had a couple of ideas / made a couple of choices worth sharing here:
- Move Repository to application layer (single aggregate per application)
- Implement updateFn for
AddandRemoveas well, so that the domain does not need to know about the behaviour of the repository interface (that only works on an aggregate) - Pass
Identifiables to the repository so that the auto generated code does not need to know too much about the commands data structures, while still verifying identifiability of the object (domain aggregate) on which the command should be executed. → Repo Interface
- Avoid handling errors within the updateFn callback — so the repository does not need to have to bother with error handling
I'm playing with the idea of first (above the line) vs second (blow the line)
// Repository knows how to persist the service's aggregate
type Repository interface {
// Add knows how to create an initialized instance of an aggregate
// it expects an initialized instance be return from an add funcion or nil to bail out
Add(ctx context.Context,f func() (*account.Account)) (uuid.UUID, error)
// Add knows how to remove an identifiable instance of an aggregate
// it also returns a copy to a remove funtion in order to bail out
Rem(ctx context.Context, i Identifiable, f func(a account.Account) bool) error
// Update knows how to update an identifiable instance of an aggregate
Update(ctx context.Context, i Identifiable, f func(a *account.Account) bool) error
}
// Identifiable can be identified by the Repository
type Identifiable interface {
// Identifer knows how to identify an object
Identifier() uuid.UUID
}
// Store knows how to read and write an entity
type Store interface {
// Load knows how to load an entity
Load(ctx context.Context, uuid uuid.UUID) (a *account.Account, err error)
// Save knows how to save an entity
Save(ctx context.Context, a *account.Account) error
}
// Identifiable can be identified so that the store can load it
type Identifiable interface {
// Identifer knows how to identify an object
Identifier() uuid.UUID
}
The latter approach has several benefits in my eyes:
- simple interface, universal semantics → similar to file system interfaces
- the applications becomes more concise when reacting to storage errors ("not found" & "version conflict" will be returned by their respective appropriate method, so error checking becomes cleaner).
// cave: pseudo-code ...
func(h *DoSomethingHandler) Handle(ctx context.Context, ds DoSomething) error {
old, err := Load(ctx, ds.Identifier())
new := old.Copy()
...
ok := h.pol.Can(ctx, ds, "DoSomething", new) // check policy
if err = h.handle(ctx, new); err != nil { // handle domain logic
return errwrap.Wrap(ErrInDomain, err)
}
if err = Save(ctx, new); err != nil { // nil to delete
return errwrap.Wrap(ErrDuringSave, err)
}
...
err = Publish(ctx, &SomethingDone{...})
if err != nil {
Save(ctx, old)
}
}