For what it's worth (I'm developing in C# .net - but the principles should hopefully still be helpful to you) I've defined a bunch of types (DTO's) which I use to exchange data between the business and data tiers; and I have more than one type per domain object / entity. These are defined as simple classes.
Each of these is built with a specific task in mind, for example:
- I have a light-weight type designed to populate list views (lots of "rows" but not many "columns"). I also have a corresponding collection type that holds any number of these.
- I have a "big" get copy which usually has all the properties of the entity in question - but for only one instance of it. I may have more than one of these depending on the size and complexity of the entity vs the cases of use; and also depending on whether you want to return all the data associated with an instance of the entity straight away, or lazy-load some on later requests.
- I also usually have separate "save" (new) and "update" types for the entity.
Each type is designed to hold only the information relevant for a given task.
For example the "big" will return the date last modified, but I don't expect that in my save and update types because I populate those in the data access layer.
Also, for my app, these types exist in a common assembly - so they can be re-used between any tier, not just between the business and data tiers.
Architectural Fit
There's nothing particular special about this approach, it has it's own pros and cons; exactly what those are and how they affect you will depend on a lot of things - I guess your mileage will vary - but it's certainly served me well for a number of years now.
People often make a fuss over "separation of concerns" - and that's a really wise move; this relates to DTO's in that they are exchanged between layers (and services, components, etc) so there can always some ambiguity over where exactly to draw the line.
I take the approach that if a bit of information is fit to be exchanged between to tiers it's probably fit to be exchanged between any number of tiers - so why not make it accessible to all? It also saves have to re-cast information if you're just passing it through.
As far as complexity goes - there are two ways of handling that:
- Use a verbose / human readable naming convention for all; the types so you know what things are; it doesn't matter how many there are - that's what intelli-sense (& docs) are for. The more intuitive the better.
- KISS - keep things simple if you can; you'll have to balance sensible reuse and the Single Responsibility Principle (SRP).
Would you create a DTO of a complex property of a main entity?
I've found myself making DTO's for one of 2 reasons:
- There's data I know I need to expose (push), and the design of the DTO is a no-brainer: it's driven by the data I want to expose.
- Pull: the consumer know's what it wants, and the DTO is designed to meet those needs.
Because they are all defined in a common assembly no one component "owns" it, it helps force you to think from a 'domain' perspective rather than a component centric one; to an extent this will influence the design of the DTO's (balancing reuse vs SRP).
In both cases the DTO's made can be quiet specific to a particular need, or generic; e.g, A DTO that has only a int and a string is not uncommon, it's the sort of thing you'd use for sending to dropdownlists.
Most of the DTO collections I send back (from DAL to BL) are specific to a concept - not generic. I enforce very very basic rules on these via the constructors I offer: every arg is required. I'm not sure if this answers your question "How do you manage the assembly and validation".