DDD: Using Value Objects inside controllers?
Asked Answered
B

3

14

When you receive arguments in string format from the UI inside you controller, do you pass strings to application service (or to command) directly ?

Or, do you create value objects from the strings inside the controller ?

new Command(new SomeId("id"), Weight.create("80 kg"), new Date())

or

new Command("id", "80 kg", new Date())
new Command("id", "80", "kg", new Date())

Maybe it is not important, but it bothers me.

The question is, should we couple value objects from the domain to (inside) the controller ?

Imagine you don't have the web between you application layer and the presentation layer (like android activity or swing), would you push the use of value objects in the UI ?


Another thing, do you serialize/unserialize value objects into/from string like this ?

Weight weight = Weight.create("80 kg"); 
weight.getValue().equals(80.0);
weight.getUnit().equals(Unit.KILOGRAMS);
weight.toString().equals("80 kg");

In the case of passing strings into commands, I would rather pass "80 kg" instead of "80" and "kg".

Sorry if the question is not relevant or funny.

Thank you.


UPDATE

I came across that post while I was searching information about a totally different topic : Value Objects in CQRS - where to use

They seem to prefer primitives or DTOs, and keep VOs inside the domain.

I've also taken a look at the book of V. Vernon (Implementing DDD), and it talks about (exactly -_-) that in chapter 14 (p. 522)

I've noticed he's using commands without any DTOs.

someCommand.setId("id");
someCommand.setWeightValue("80");
someCommand.setWeightUnit("kg");
someCommand.setOtherWeight("80 kg");
someCommand.setDate("17/03/2015 17:28:35");
someCommand.setUserName("...");
someCommand.setUserAttribute("...");
someCommand.setUserOtherAttributePartA("...");
someCommand.setUserOtherAttributePartB("...");

It is the command object that would be mapped by the controller. Value objects initialization would appeare in the command handler method, and they would throw something in case of bad value (self validation in initialization).

I think I'm starting to be less bothered, but some other opinions would be welcomed.

Belinda answered 16/3, 2015 at 14:28 Comment(3)
What if your client is not written in the same language?Dromous
You mean "what about using DTOs" ? ^^; Indeed, I have not talked about that in my questionBelinda
I think you mean "I've noticed he's using commands without any VOs." (you say "DTOs" instead).Welsh
M
6

As an introduction, this is highly opinionated and I'm sure everyone has different ideas on how it should work. But my endeavor here is to outline a strategy with some good reasons behind it so you can make your own evaluation.

Pass Strings or Parse?

My personal preference here is to parse everything in the Controller and send down the results to the Service. There are two main phases to this approach, each of which can spit back error conditions:

1. Attempt to Parse

When a bunch of strings come in from the UI, I think it makes sense to attempt to interpret them immediately. For easy targets like ints and bools, these conversions are trivial and model binders for many web frameworks handle them automatically.

For more complex objects like custom classes, it still makes sense to handle it in this location so that all parsing occurs in the same location. If you're in a framework which provides model binding, much of this parsing is probably done automatically; if not - or you're assembling a more complex object to be sent to a service - you can do it manually in the Controller.

Failure Condition

When parsing fails ("hello" is entered in an int field or 7 is entered for a bool) it's pretty easy to send feedback to the user before you even have to call the service.

2. Validate and Commit

Even though parsing has succeeded, there's still the necessity to validate that the entry is legitimate and then commit it. I prefer to handle validation in the service level immediately prior to committing. This leaves the Controller responsible for parsing and makes it very clear in the code that validation is occurring for every piece of data that gets committed.

In doing this, we can eliminate an ancillary responsibility from the Service layer. There's no need to make it parse objects - its single purpose is to commit information.

Failure Condition

When validation fails (someone enters an address on the moon, or enters a date of birth 300 years in the past), the failure should be reported back up to the caller (Controller, in this case). While the user probably makes no distinction between failure to parse and failure to validate, it's an important difference for the software.

Push Value Objects to UI?

I would accept parsed objects as far up the stack as possible, every time. If you can have someone else's framework handle that bit of transformation, why not do it? Additionally, the closer to the UI that the objects can live, the easier it is to give good, quick feedback to the user about what they're doing.

A Note on Coupling

Overall, pushing objects up the stack does result in greater coupling. However, writing software for a particular domain does involve being tightly coupled to that domain, whatever it is. If a few more components are tightly coupled to some concepts that are ubiquitous throughout the domain - or at least to the API touchpoints of the service being called - I don't see any real reduction in architectural integrity or flexibility occurring.

Parse One Big String or Components?

In general, it tends to be easiest to just pass the entire string into the Parse() method to get sorted through. Take your example of "80 kg":

  • "80 kg" and "120 lbs" may both be valid weight inputs
  • If you're passing in strings to a Parse() method, it's probably doing some fairly heavy lifting anyway. Expecting it to split a string based on a space is not overbearing.
  • It's far easier to call Weight.create(inputString) than it is to split inputString by " ", then call Weight.create(split[0], split[1]).
  • It's easier to maintain a single-string-input Parse() function as well. If some new requirement comes in that the Weight class has to support pounds and ounces, a new valid input may be "120 lbs 6 oz". If you're splitting up the input, you now need four arguments. Whereas if it's entirely encapsulated within the Parse() logic, there's no burden to outside consumers. This makes the code more extensible and flexible.
Muscid answered 17/3, 2015 at 6:38 Comment(1)
Thank you, a lot to think about, and some answers as well.Belinda
O
1

The difference between a DTO and a VO is that a DTO has no behavior, it's a simple container designed to pass data around from component to component. Besides, you rarely need to compare two DTO's and they are generally transient.

A Value Object can have behavior. Two VO's are compared by value rather than reference, which means for instance two Address value objects with the same data but that are different object instances will be equal. This is useful because they are generally persisted in one form or another and there are more occasions to compare them.

It turns out that in a DDD application, VO's will be declared and used in your Domain layer more often than not since they belong to the domain's Ubiquitous Language and because of separation of concerns. They can sometimes be manipulated in the Application layer but typically won't be sent between the UI layer and Application. We use DTO's for that instead.

Of course, this is debatable and depends a lot on the layers you choose to build your application out of. There might be cases when crunching your layered architecture down to 2 layers will be beneficial, and when using business objects directly in the UI won't be that bad.

Orthographize answered 18/3, 2015 at 9:40 Comment(0)
C
0

I am far from a DDD expert, but I see it this way now:

I'd rather inject immutable value objects (but not mutable entities) in presentation view models, so I would spare a lot of boilerplate code doing trivial VO <-> DTO <-> VM conversion. Though it is problematic, in theory the application services should not return VOs or VMs, because they must not know much about presentation and presentation must not know much about the domain. My application layer does not do much though, either I put VM (view model) creation there or something close to it, or most of the times it is just a getting an aggregate from a repository, calling a method on it and packing, unpacking DTOs. Maybe my model is not good enough though, idk, I am still learning how to do this properly.

Currently I have a calendar example:

enter image description here

The VM I need is pretty stupid and far from optimal, still it works:

        'calendar' => array(
            'dayNames' => array('H','K','Sze','Cs','P','Szo','V'),
            'monthNames' => array(
                'január','február','március','április','május','június',
                'július','augusztus','szeptember','október', 'november','december'
            ),
            'year' => 2023,
            'month' => 3,
            'weeks' => array(
                array(null,null,1,2,3,4,5),
                array(6,7,8,9,10,11,12),
                array(13,14,15,16,17,18,19),
                array(20,21,22,23,24,25,26),
                array(27,28,29,30,31)
            ),
            'selected' => array(2,3,4,6,7,8,9,10,13,14,15,16),
            'disabled' => array(1,2,3,4,5,11,12,18,19,25,26)
        )

I'd like to inject a BusinessIntervalDTO into it which contains data like this (from a different query):

{
    id: 1,
    operatingRange: [[2023,3,6,1],[2023,3,31,5]],
    cookingDays: [
        [[2023,3,6,1],[2023,3,10,5]],
        [[2023,3,13,1],[2023,3,17,5]],
        [[2023,3,20,1],[2023,3,24,5]],
        [[2023,3,27,1],[2023,3,31,5]]
    ],
    bookingChangeDeadline: [2023,3,18,6],
    bookedDays: [
        [[2023,3,6,1],[2023,3,9,4]],
        [[2023,3,14,2],[2023,3,16,4]],
        [[2023,3,20,1],[2023,3,24,5]],
        [[2023,3,28,2],[2023,3,30,4]]
    ]
}

I have proper Date, DayOfTheWeek, DateRange, DateRangeComposition VOs in the BusinessInterval entity and the dayNames, monthNames is language dependent in the VM and I got helpers for it. So it is basicly iterating through weeks and day numbers in a month what I need. I could easily do it by injecting the VOs and the language config with constructor injection into the VM and spare a lot of boilerplate and copy-paste code with it. It does not make sense to do a two step data structure conversion especially that the VOs have features like iDate. getDayOfTheWeek, getDay, getMonth, getYear, nextDay, isSame, etc. which simple arrays don't have or I can add getWeeks into iDateRange and iDateRangeComposition. If I do the other direction I can do validation and single step conversion for the form data too. So for entities it is fine to make DTO-s, but I think for VOs it does not make much sense unless presentation is on a completely different server and injecting them is not possible.

So I think the main reason we need DTOs is scalability here and if your application is not big enough to scale horizontally, it is better to stick without DTOs and do a direct conversion into VMs and change the code later for horizontal scaling if necessary. Another reason is having multiple presentations with different VM needs which can use the same DTOs, though again, they could probably use the same VOs too. So I think unless you really need it, it is fine to return an object, which uses the VOs directly instead of doing the VO <-> DTO conversion and using the DTO, because the VOs can contain many features, which can be useful for the VM too.

I ended up adding an iBusinessIntervalCalendarViewModel to the presentation and implementing it with a BusinessIntervalTransfer in the application. So the object is a DTO while it implements the interface of the view model. Let's see how it works.

Carline answered 6/3, 2023 at 12:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.