10

I've been reading a lot of material lately about DDD (business entity objects) and other common patterns in n-tiered(layered) architecture. One thing I have issue with is, most articles, blogs, examples, etc. seem to talk to only one aspect of a system. Some may talk about creating a BLL for containing business logic. Some talk about only saving data via a DAL (ignoring reading). Some talk about only DTOs. I've not yet found anything that at a basic level talks about putting all of this together. When I attempt to put a lot of that material out together myself, it seems the information is at times is mutually exclusive.

Here is where (to me) things tend to be mutually exclusive:

  • Business logic is contained in the DDD business entity objects, which your UI doesn't create directly (it gets DTOs).
  • Data is passed to the UI via DTOs, which should not contain business logic. The DTO should only contain getters and setters.

Which leads to my Question:

How does your UI configure itself when business logic is required to analyze the data of those DTOs to make UI decisions?

For example, you are developing the UI and you have this requirement. You need to determine in code whether or not the Delete Button should be enabled:

System Requirement: An Order can only be deleted when there are no products on the Order and the Order's status is 'Open'.

Sure, you can do a check in your UI with the following:

Dim _order As OrderDTO = SomeServiceCall.ReadOrder(123)

If _order.Products.Count = 0 And _order.Status = "Open" Then 
    DeleteButton.Enabled = True
End If

But isn't this now putting business logic into the UI code? What if there are new or changed business rules to determine when an Order can be deleted? While you could add a function .CanDelete() to the OrderDTO, it's the wrong place based on everything I've been reading, because business logic belongs in the Order entity object. So how do you tell the UI to Enable or Disable that darned Delete button???

One option may be to always enable the Delete button and throw an exception when the Delete action fails validation in the Order entity object (side note: just found FluentValidation and it seems amazing!). But that's sloppy UI. What are the "good architecture" alternatives to get the UI what it needs?

The same could apply to many other UI situations, where based on different combinations of the data (i.e. business logic) contained in a DTO, you may lock out some input fields, or hide them, etc.

A lot of times it seems the write-ups about DDD, DAL, BLL, DTO ignore the practical UI requirements.

HardCode
  • 674

5 Answers5

5

System Requirement: The UI should only enable the Delete button for an Order when there are no products on the Order and the Order's status is 'Open'.

This is the classic requirement mistake of letting implementation details leak into a requirement. To the point, this requirement has absolutely no right to know that the way a user requests a delete is by using a button. The requirement isn't a UI requirement at all.

The most fundamental question in software design is, "What knows about what?"

It's the UI's job to know that there is a button. It is not the presentation layers job. The presentation layers job is to know that there is a command feature being disabled. Doesn't know if it's a button. Doesn't know if it's labeled delete, effacer, or löschen.

Isolating knowledge it the point of all this. So yes. That code shouldn't be in the UI.

But .CanDelete() is a broken strategy simply because it fails to present enabled/disabled status to the user before they issue the command (however they issue it). That's your UI requirement.

Enabling and disabling in the UI doesn't require business logic in the UI. It only requires that the UI accept status updates whenever they are sent. It doesn't need to understand why.

So unless you want to poll by infinitely looping over .CanDelete() what you should be doing is calling this business logic whenever _order.Products.Count or _order.Status could have changed state.

You want to put this all together? Start with something that works. Then spot places where too many ideas have been mixed together and tease them apart. Do that enough times and you'll start seeing what to seperate at the start and save yourself some time.

It's very important to start this way. If you only ever build systems whose concerns are perfectly separated you'll never learn how to fix systems that have them mixed together.

candied_orange
  • 119,268
4

What if there are new or changed business rules to determine when an Order can be deleted? While you could add a function .CanDelete() to the OrderDTO, it's the wrong place based on everything I've been reading, because business logic belongs in the Order entity object.

What works for me: I think of the domain model as a finite state machine. The state machine reacts to messages, where messages can be simple things like a "Time Has Passed" with a reading from a timestamp of a local clock, or something complicated like a booking agent selecting a specific itinerary for cargo. The state machine copies the information that it wants from the message and computes its next state (which could even be the current state, if there is no transition or if the transition loops back to the current state).

When the model describes its current state (aka providing a view for the use case of change), it does so by not only offering a representation of its computed state, but also a representation of the candidate commands it is prepared to receive.

In your specific example, that list of candidate commands for an empty order would include both the "add item" command and the "delete order" command, but in the case where the domain logic forbids the delete then the delete order command is not present in the candidate list.

It is then, as @candied_orange pointed out, someone else's job to figure out how to translate each of the candidate commands into the appropriate user affordances.

For example, in a Web interface, you might have a module that takes the list of candidate commands and creates HTML forms for each. It's not "domain logic", in the sense that it isn't making the decisions of the domain model, but it is translating from the domain models message to a more general purpose representation.

But you could just as easily replace the web interface with a command line interface; the list of candidate commands is unchanged, but the expression of them in the command line interface is certainly different.

In a sense, adding ".CanDelete" to the DTO is exactly the right sort of idea, although might not use that spelling. Your DTO is an immutable data structure with some in-memory representation of the list of candidate commands, and you can choose whatever design you like for querying it.

Dim _order As OrderDTO = SomeServiceCall.ReadOrder(123)

If _order.commands.contains("http://example.org/commands/deleteOrder") Then 
    DeleteButton.Enabled = True
End If

Remember, the basic idea of a Data Transfer Object is that it is designed to reduce the number of method calls, which is to say it has many interesting answers baked into it. So we are encoding some redundant information into the DTO, and in return we get centralizd domain logic that is easier to change.

A lot of times it seems the write-ups about DDD, DAL, BLL, DTO ignore the practical UI requirements.

Yes. DDD in particular is bad at discussing "plumbing", and the real complications that it can introduce.

VoiceOfUnreason
  • 34,589
  • 2
  • 44
  • 83
2

I feel your pain, and I've made the same observations couple of years back, that led me down the rabbit hole of re-evaluating what object-orientation supposed to be and what constitutes a maintainable design. Short answer: You can't find a lot in this subject, because it doesn't work. N-tier architectures, DDD (as practiced by most) and DTOs in particular are all sub-optimal ideas. Persistence-agnostic business-layer doesn't work. Business-agnostic UI in these designs doesn't really exist.

First: It is entirely reasonable to have a requirement to gray out some buttons based on some rules. The UI is what users see, it is completely natural for them to adopt the terminology of the UI.

How to implement: Let's think about this one step at a time. Where is the knowledge supposed to be to determine whether an Order is deletable? It seems pretty reasonable to expect this in the Order itself. It depends on the internal state and semantics of the Order, so it should be there.

So how does the information "get to the UI" from the Order? This is where all the above designs fail. All of them try to get this information out of the Order somehow. And regardless of how you do that: whether it is a new field in the DTO, whether it is status updates or events, or whatever, it always means that now you couple all those things to this simple feature.

The only way to get this in one place and maintainable is to not get this information out of the Order at all. Instead, get the behavior that needs this information into the Order, and that is to present the Order on the UI.

So it follows quite reasonably that the Order should be able to present itself, with disabled/enabled delete button and everything. In effect, the UI should not depend on the "Business", the "Business" should depend on the UI. (Or the "Business" has a UI if you will)

I hope that sounds entirely reasonable to you too, even if it runs counter to almost everything we've been told by these architecture and design patterns the last 2-3 decades.

Update pseudocode example:

class Order {
   ...state: i.e. products, whatever...

   void cancel() { ... }

   ...

   UIComponent display() {
      return Panel(
         new Table(products),
         new Button("Track", ...),
         new Button("Cancel", products.empty?ENABLED:DISABLED, this->cancel()),
         ...);
   }
}

This demonstrates what I mean. The Order knows how to present itself. This follows KISS, Object-Orientation, Law of Demeter, is Maintainable, etc. If the "Business" and "UI" is in the same application, there is no reason whatsoever to not do it this way.

Note that the decision whether the order is cancellable stays within the Order. Note also that there are no details of the presentation leaking into the Order itself. No colors, layout or things like that.

1

Given that the DTO does not need to be 1 to 1 with the data entities, I'd go with your CanDelete() in the DTO using the business rules in the BL. I'd probably repackage it as an attribute "IsDeletable" to make it more data-like.

If this is a regular pattern then you might want to embed a structure of some sort. (IsDeletable, IsUpdatable).

If you were doing an API instead of a UI (and an API is a type of UI in my opinion) it's kind of like https://en.wikipedia.org/wiki/HATEOAS in abstract.

There's a lot of dogma as well as perfectly valid practice that is out of favour. Picking that which works for you and allows you to deliver something while striking the balance between perfectionist hand wringing and hacking is a skill.

LoztInSpace
  • 1,338
1

I am not sure which kind of architecture you had in mind with the idea of using DTOs this way, but let us take Bob Martin's very popular Clean architecture and have a look how the problem is solved there.

In such an architecture, an UI object would provide an interface for

  • getting Order data and for

  • enabling and disabling buttons (but without any logic how and when to do this).

A Presenter object (which holds such an interface instance) could grab the data from the order entity, pass that data - maybe as an order DTO - to the UI through the interface, check the .CanDelete() method of the order entity and call the interface to disable or enable the delete button accordingly. The presenter can also listen to certain events in the system which may change the state of the order, and repeat the evaluation of .CanDelete() (and the update of the UI) then.

So, in short, I don't know which "articles, blogs, examples, etc." you are referring to, but the ingredient which seem to be missing there are simply interfaces and events. The "Data is passed to the UI via DTO" idea may be simply a misunderstanding or oversimplification, but since you did not gave any references I cannot actually tell you what the source where you got this from really meant.

Doc Brown
  • 218,378