wild-workouts-go-ddd-example icon indicating copy to clipboard operation
wild-workouts-go-ddd-example copied to clipboard

Couple of ideas

Open blaggacao opened this issue 5 years ago • 1 comments

While continuing to work on ddd-gen (example) I had a couple of ideas / made a couple of choices worth sharing here:

  • Policeable interface to enforce access policies at the application level:

  • Move Repository to application layer (single aggregate per application)
  • Implement updateFn for Add and Remove as 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

blaggacao avatar Nov 22 '20 00:11 blaggacao

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)
	}
}

blaggacao avatar Nov 22 '20 06:11 blaggacao