5

Let's say we have a Book that has a List of Authors. Order of Authors is important.

We then have the update page, where user can 1) add new author, 2) remove existing author or 3) change the order. What I have as the input is the list of new authors.

I find this problem often - there is often a lot of models with this kind of relationship. Sometimes the order is not important (so we use Set) but more-less is like this.

In my business method, I need to:

  • detect new Authors, so I can insert them
  • detect removed Authors, so I can remove them
  • assume everything else is updated.

I can not simply reapply authors (delete and then insert) since there is additional data related to a book and author.

Doing this manually every time is error-prone. Is there a better way to track changes in related collections?

Should I track changes on UI instead (as this is where they happens) and then provide more input then just one list of all users: for example, list of changes, list of removals? In that case my business method would take more parameters, i.e. lists for each type of change (removal, etc).

Finally, I was thinking into storing set of events per update, so I can repeat them on model easily.

Should I handle this margining in model or in my service method?

Any wisdom on this?

  • 1
    I think the question needs more information about the design. It's hard to see why adding or removing Authors would be error-prone and why the business method needs to detect new or removed Authors. Does your Author have an identity? What is the "additional data related to a book and author"? – COME FROM Dec 2 '14 at 7:58
4

I will try to answer with the info you gave me.

You wrote

I need to:

  • detect new Authors, so I can insert them
  • detect removed Authors, so I can remove them
  • assume everything else is updated.

So I think that your approach is too CRUD for DDD and aggregate roots. My 2 cents your business method is something like Update(Book) and the parameter is the modified Book (its data, isbn or title for example, and/or its author collection) and that is why you have to "detect" changes in the aggregate root and why you are in trouble about how to do that.

I would implement a command pattern and segregate every user-case into a command.

  1. Edit book info
  2. Add author
  3. Remove author
  4. Edit author
  5. Change author order

This will need a good task-based UI design.

Then, you can choose the most suitable strategy:

  1. Execute every command instantly and save into persistence. Get Book aggregate root, modify book data or author collection (depends of the command) and update persistence. No need of tracking changes.

  2. Execute every command instantly in the aggregate root in memory and wait user push "Save" button. Then go to persistence. If you store the commands executed in the aggregate until persistence is updated you already know what update in persistence. Changes tracked by the commands executed in the aggregate.

  3. Store a list of generated commands by the user and execute it in a batch when user push "Save"; use (1) or (2) strategy to update aggregate and persistence.

Last words and tips:

Even UI design affects the architecture and patterns you can use. Think in everything as a whole.

Every strategy listed above has its uses. Depends of your domain, context, environment, technologies you use, etc.

You can use several strategies. No need to stick just in one in every bounded context.

Your domain has to model user-case actions. Don't do just Update(Book). Model every action (every command execution) in its own function and apply its relevant domain rules and invariants.

3

If authors are Value Objects, you can easily re-create the whole list each time because only their value matters, you don't have to relate them to identified objects. This might be at the cost of losing business intent though, since you don't capture what the user precisely wants to do.

If they are Entities, I suggest sending granular commands to Book -- AddAuthor, RemoveAuthor, ChangeAuthorOrder. The latter will have to include distinct order indexes for all authors, otherwise the Book can get in an inconsistent state.

2

The way I usually do this is based on a full update using a structured representation of the record. Some would call this a Data Transfer Object (DTO). Something like:

{ # book
  'id': 123,
  'title': 'The Art of Computer Programming',
  'authors': [
    {
       'id': 456,
       'name': 'Donald Knuth'
    }
  ]
}

In the list of authors, if the author record already exists, then it gets an id. If there's no id, it's a new record. Deleted authors simply do not appear in the list.

Depending on the application front-end, this is generated in different ways:

  • In a traditional HTML forms application, the server decodes the posted form data
  • In an Ajax application, JSON is generated on the client and sent to the server
  • Mobile and fat clients apps work somewhat like an Ajax app

I always use libraries so I don't generate this by hand: the library generates it based on a structured definition of the form.

The next step is to validate the DTO, and apply it to your domain objects. There are some subtleties to doing it right. In particular, where an author already has an ID, you need to check the ID is already associated with that book. If not, you risk parameter tampering attacks.

Again, I always use libraries to do this. ToscaWidgets has great support for this, again doing all the validation based on a structured definition of the form.

The final step is to persist your domain objects. Most ORMs do this automatically.

This pattern lets your perform complex updates reliably. I've always like the style of large forms with sub-forms - rather than forcing users to click through multiple pages. I use this pattern a lot for highly complex forms, including multiple levels of nesting, and it works great.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.