An anti-pattern variant of Data Transfer Objects (DTOs) where the objects are forcefully deprived of all possible functionality, even for matters that are directly relevant such as construction, serialization, and (first stage) validation.
Some believe that data transfer objects must only contain data and that there must be no behaviour declared on the type at all. Their understanding of the conceptual model is that DTOs are entirely and only about the data they contain.
In my experience, the purpose of a data transfer object is to encapsulate part of the external boundary of a system, taking explicit control of the way information (data) is transferred across that boundary.
For a data transfer object to fully meet this requirement, it has to also contain behaviour.
We can explore what this means from multiple perspectives.
Outbound Data
For outbound information, a DTO presents a well-designed structure to the outside world, one that intentionally shapes the information being made available. Ideally, this structure should be invariant over time, even as the system providing the data evolves and adapts.
I’ve found it useful for a DTO to have a formal constructor that initializes all mandatory properties, thus ensuring that the system never generates a DTO with key information missing. Such a constructor will often accept data in native formats, providing translation into serializable forms.
Inbound Data
For inbound data, a DTO needs to accept whatever is sent by clients - which makes it the first point of contact for untrustworthy data. The first responsibility here is for early detection of errors (missing or over-length properties, invalid formats, out of range values etc), while the second is for transformation (conversion of strings into timestamps, etc).
Adding straightforward validation code to a DTO provides an easy way to prevent invalid and/or malicious information from getting past the outer layers of our application.
For example, if you have a FullName
field you can check that it contains only printable characters, isn’t too long, and so on. The goal here is to reject the obviously malicious.
There’s a trap here - we need to avoid making things too restrictive. Single character names are common in some places, and 100 characters aren’t enough for some real people. (See Falsehoods Programmers Believe About Names for some good advice.)
A good rule of thumb is to include only validation on the DTO that can be done in isolation, without reference to outside sources. For example, you can check the format of an ISO 4217 currency code by rejecting any value longer than three characters, as well as any value not made up of letters. You shouldn’t worry about whether a well-formed code is known to your system - leave that to your business domain layer.
Type Conversion
We often require a DTO to transform the presentation of information from the forms used internally into forms suitable for publication. For example, converting a .NET DateTimeOffset
structure into an ISO8601 format string, or an internal enumeration into a string.
This becomes particularly powerful when we start using semantic types in our application domain. We can expose a simple string representing a part number to the outside world, and convert that into a PartNumber
instance for internal use.
Conclusion
Constraining your system to only use dumb transfer objects just makes your application more complicated - you still need all of the same behaviours, but now you need to find them new places to live. Put the behaviour where it belongs - on your data transfer objects, right on the external boundary of your application.
Comments
blog comments powered by Disqus