Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Couple of ideas #16

Open
blaggacao opened this issue Nov 22, 2020 · 1 comment
Open

Couple of ideas #16

blaggacao opened this issue Nov 22, 2020 · 1 comment

Comments

@blaggacao
Copy link
Contributor

blaggacao commented Nov 22, 2020

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
Copy link
Contributor Author

blaggacao commented Nov 22, 2020

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant