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
  • 614
  • 4
  • 12
  • I think the crux of this question is the sentence - *"Data is passed to the UI via DTOs, which should not contain business logic."* - where did you find this, to which architectural context does this idea refer? What does "UI"in this sentence mean precisely (for example, in a MVP architecture, does this include the Presenter layer or not)? And why should a presenter layer not directly access business entities and their methods? What kind of architecture is this? – Doc Brown Oct 26 '19 at 08:24

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
  • 102,279
  • 24
  • 197
  • 315
  • Yes, referencing Button in the requirement is not something I do in practice. Mistake in my post on my part. I'll edit that. – HardCode Oct 24 '19 at 13:42
  • "It's the UI's job to know that there is a button. It is not the presentation layers job." This is the first time I'm seeing the UI referenced as something separate from the Presentation Layer. I've always thought the UI **is** the presentation layer. Can you explain further or have a reference i can read? I'd appreciate that. – HardCode Oct 24 '19 at 13:43
  • @HardCode look at an OSI stack. Presentation and UI are not the same. A model knows what number you have. A presentation knows to express it as 3.14159 or π. A UI knows to express it with a text box, or a console, or an audio speaker. You don't have to know all these things in the same place. – candied_orange Oct 24 '19 at 15:53
  • @candied_orange, it seems to me a somewhat artificial distinction. The presenter in this case really appears to be doing very little, and I couldn't imagine a presenter that truly worked unchanged whether the UI was a console or a GUI, because different UI technologies have different idioms and would integrate differently. – Steve Oct 24 '19 at 16:48
  • @Steve all distinctions are artificial. You don't have to do any of this. If you separate UI and presentation code you make that separation really valuable if you do it with a design that allows you to substitute one UI for another. But that value doesn't come for free. You have to do design work to create an API that works for all the ones you wish to support. It's not impossible. It's just work. – candied_orange Oct 24 '19 at 17:42
  • @candied_orange, have you ever worked on a project that actually replaced one UI with another - especially something as radical as console to GUI, or visual to aural - without any rework? How would an presentation API even be conceived and tested, to work with UI technologies that either don't exist yet or just aren't being used for the project in question? These sorts of concerns seem to me to be a form of architecture astronautics. – Steve Oct 24 '19 at 18:50
  • @Steve no matter how sound an idea is there will always exist a context that you can put it in to make it absurd. I've put this ideal to good use on real projects without getting so carried away that I'm spending days without getting useful work done. The secret is doing just that. Don't let chasing these ideals turn into an excuse to disappear into a problem. Keep getting useful work done. – candied_orange Oct 24 '19 at 19:52
  • @candied_orange, agreed, I'm sure this approach has value, but it is neither to allow replacement of the UI technology, nor to separate "requirements" from implementation. Implementation often determines requirements, it's impossible to establish the requirements of a UI technology you don't work with or test against. The arrangement of code in the three-layer approach is as much the *product* of the typical case of a relational database, an object-oriented application language, and a GUI which has some common principles across applications. – Steve Oct 25 '19 at 07:09
  • @Steve that's only true if you refuse to use abstraction at all. – candied_orange Oct 26 '19 at 04:18
  • @candied_orange, I'm not so sure. The design of screens for console applications does not, in my experience, exactly match that of GUIs - it's not just a different skin, the representation of data and the work process may be different. You cannot abstract without potentially losing the advantages of a close integration with one paradigm, and woe betide the person who attempts to "abstract" a GUI design without any experience of using consoles! Moreover, abstraction takes time and effort and consumes resources, and I've yet to see a GUI replaced with a console. – Steve Oct 26 '19 at 13:58
  • @Steve see git. – candied_orange Oct 26 '19 at 13:59
  • @candied_orange, that is a developer tool and hardly an example typical of a business application, and it is not clear that it was written for a GUI then later converted to console use (if I had to guess, as a developer tool, it was probably written and tested against both from inception). – Steve Oct 26 '19 at 14:18
  • @Steve no. It was designed with a data model in mind. The model doesn't care about the ui. Putting the GUI at the center of your design is what locks you in. You don't have to do that. Don't use the fact that you made it hard to switch UI as evidence that you should never try to switch UIs. You're trapped in a self fulfilling prophecy. – candied_orange Oct 26 '19 at 14:57
  • @candied_orange, I'm not suggesting putting the UI at the centre of all design, but some regard has to be paid to user interface. I certainly don't see how you can write "presentation" code without some idea of how the data will be presented and some idea of the mechanics of the UI technology(/ies) that will actually be employed. I won't labour the point though - my view is simply that good software consists of good organisation and good integration, not "abstractions" where one element or layer pretends that it does not have to be concerned *at all* with any other. – Steve Oct 26 '19 at 15:27
  • Let us [continue this discussion in chat](https://chat.stackexchange.com/rooms/100306/discussion-between-candied-orange-and-steve). – candied_orange Oct 26 '19 at 15:28
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
  • 32,131
  • 2
  • 42
  • 79
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.

Robert Bräutigam
  • 11,473
  • 1
  • 17
  • 36
  • "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." Could you expand on this, with maybe a small concrete example? – HardCode Oct 25 '19 at 14:07
  • So do I understand this correctly, you are suggesting to have no distinction between a business layer dependent and an UI layer and mix them up into one? – Doc Brown Oct 26 '19 at 08:15
  • @DocBrown I don't think that's a fair characterization of what I've said. I would argue exactly the opposite is true. The distinction between UI and Business is much stronger than before. Here the UI components don't know about the internals of the `Order`. Not the properties, not how it all relates, not even what length are the individual data elements, etc. This is exactly what was asked above. In popular architectures it is exactly backwards, the UI knows everything of the business. So if "business" stuff changes, you likely have to modify both layers. – Robert Bräutigam Oct 26 '19 at 08:27
  • Ok, so you are suggesting to have a dependency from the BL to the UI layer (or layer**s**, if there is more than one of them), but not vice versa? – Doc Brown Oct 26 '19 at 08:36
  • Yes, that is what I'm suggesting. Note though: The UI in such an architecture will not look like anything we would call "UI layer" in "traditional" designs. It will mostly come from a generic library, with some additional things like Pages, some standard Widgets defined, that is it. If it is a web-ui. – Robert Bräutigam Oct 26 '19 at 09:27
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,149
  • 6
  • 8
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
  • 199,015
  • 33
  • 367
  • 565
  • I've read so much, I never kept links to cite. I think a lot of the material I've read is oversimplified, which leads to my confusion about these things. One concept I'll have to look into is Presenter, to maybe help make more sense of my core question. – HardCode Oct 28 '19 at 15:01
  • "I am not sure which kind of architecture you had in mind..." That's what I'm still trying to figure out :-) – HardCode Oct 28 '19 at 16:06
  • @HardCode: I suggest you read the article about the "Clean architecture", especially the ideas of how data crosses boundaries between the layers, and which role interfaces play. If you think this still does not answer your question, feel free to ask. – Doc Brown Oct 28 '19 at 16:16
  • That's one blog post I have read. But, I'm more of a "read the code" kind of person, so while that post of theory might be good, without concrete code examples to back up that theory, it gets lost on me. – HardCode Oct 28 '19 at 16:18