DTO vs. Assembly(转载)
DTO vs. Assembly
We probably need to make a strong statement about data transfer objects. Do we like or hate them? Well, it depends. DTOs are an excellent tool for explaining to kids the difference between theory and practice—that is, if kids can understand DTOs.
DTOs are a must-have in theory; in practice, they represent signifi cant overhead. However, experience teaches that there’s no overhead that can’t be worked in if that is the only way to go.
In a service-oriented world, a DTO-only solution is close to perfection. In a developer-oriented world, a DTO-only solution sounds extreme and is sometimes impractical. As usual, it is a matter of fi nding the tradeoff that works for you in the project that you are engaged on. If you were to abandon the DTO-only principle to some extent, what else could you do? Essentially, you could use domain objects in a library (an assembly in .NET) and share that library across the layers.
Let’s review some of the factors in this tradeoff.
Examining Some DTO Facts
In a multitier system, the service layer is responsible for decoupling the presentation layer from the rest of the system. To get this behavior, you need to keep in mind that the key guideline is to share data contracts rather than classes. A data contract is essentially a neutral representation of the data that interacting components exchange. The data contract describes the data a component receives, but it is not a system-specifi c class, such as a domain object. Ultimately, a data contract is a class, but it is an extra class specifi cally created for a particular service method.
This extra class is referred to as a data transfer object. Having a layer of DTOs isolates the domain model from the presentation layer, resulting in both loose coupling and optimized data transfer.
The adoption of data contracts adds fl exibility to the schema of data. For example, with DTOs in place, a change in the user interface that forces you to move a different amount of data doesn’t have any impact on the domain. Instead, the impact is limited to the DTO adapter layer. Likewise, you can enter changes to the domain without affecting the client.
With DTOs, you also work around the problem of circular references. When you create the domain model, you fi nd it extremely useful to double-link entities, such as Customer-to-Orders and Order-to-Customer. As we’ve mentioned, classes with circular references aren’t serializable. With method-specifi c DTOs, you brilliantly solve this issue also.
Note The real problem with circular references is not the lack of support from serializers. This is a false problem. On one hand, we welcome and love circular references between parents and children because this gives us unmatched power when it comes to querying and modeling real problem domains. On the other hand, a complex web of domain objects can’t just be handed to an automatic tool for serialization. Even if you could rely on serializers that handle circular references, would you really use them? Would you really serialize a large share of a domain model? With doubly-linked parent and children, when you load an order, you likely also load all details of the order and product, category, customer, and employee information. In this regard, a DTO is a much safer and smarter approach. In a WCF world, the NonSerializedAttribute is your best friend because it lets you break the cycle of references at some precise point.
DTO in Action: Load an Order
Let’s see how to address a couple of typical scenarios that involve actions on the service layer. Let’s fi rst consider a form where the user can pick up an ID to see the details of an order. As a result, the presentation layer invokes a method on the service layer with a similar prototype:
- ??? FindByID(int orderID);
The input is an integer, which should be good most of the time. What about the return type? Should it be a real representation of the Order entity you have in the system? This can be an Order domain model object, an Order class generated by LINQ-to-SQL, or even a typed DataTable. In any case, we return the entire graph of the order. Looking at the Northwind database, the graph could be as large as the one shown in Figure 5-8.
The user interface might need to show some data coming from linked objects, but there’s probably no need to transfer the entire graph. An adapter layer can create a DTO that fulfi lls the data contract established between the service and presentation layers. Here’s a better signature:
- OrderServiceFindByIDResponse FindByID(OrderServiceFindByIDRequest request);
The naming convention is arbitrary; we opted for <Service><Method><ContractType>. Ideally, you have a different DTO for input data and output data. Here’s a possible signature for the DTOs:
- public class OrderServiceFindByIDRequest
- {
- public int OrderID { get; set; };
- }
- public class OrderServiceFindByIDResponse
- {
- public int OrderID {get; set; };
- public DateTime OrderDate { get; set; };
- public DateTime RequiredDate { get; set; };
- public bool Shipped { get; set; };
- public DateTime ShippedDate { get; set; };
- public string FullAddress { get; set; };
- public string CompanyName { get; set; };
- public string ShipperCompany { get; set; };
- public List<OrderItem> Details { get; set; };
- }
- public class OrderItem
- {
- public int OrderDetailID { get; set; };
- public int Quantity { get; set; };
- public string Description { get; set; };
- public decimal UnitPrice { get; set; };
- public decimal Discount { get; set; };
- public decimal TotalPrice { get; set; };
- }
Internally, the service method queries the domain model and gets the graph of the order. The order is identifi ed by its ID. Using a wrapper object for the order ID keeps you on the safe side in case there are additional parameters such as a range of dates.
Here’s a possible implementation for the service method:
- public OrderServiceFindByIDResponse FindByID(OrderServiceFindByIDRequest request)
- {
- // Load the graph for the order using the data access layer
- // (You can directly use an O/RM here or perhaps a repository or your data mappers)
- Order order = LoadOrderFromPersistence(request.OrderID);
- // Prepare the response using an adapter
- OrderServiceFindByIDAdapter adapter = new OrderServiceFindByIDAdapter(order);
- OrderServiceFindByIDResponse response = adapter.Fill();
- return response;
- }
- internal class OrderServiceFindByIDAdapter
- {
- private Order _order;
- public OrderServiceFindByIDAdapter(Order order)
- {
- _order = order;
- }
- public OrderServiceFindByIDResponse Fill()
- {
- OrderServiceFindByIDResponse response = new OrderServiceFindByIDResponse();
- response.OrderID = order.OrderID;
- response.OrderDate = order.OrderDate;
- response.FullAddress = String.Format("{0}, {1}, {2}",
- order.Address, order.City, order.Country);
- response.CompanyName = order.Customer.CompanyName;
- response.ShipperCompany = order.Shipper.CompanyName;
- foreach(OrderDetail detail in order.OrderDetails)
- {
- OrderItem item = new OrderItem();
- item.OrderDetailID = detail.OrderDetailID;
- item.Quantity = detail.Quantity;
- item.Discount = detail.Discount;
- item.Description = detail.Product.Description;
- item.UnitPrice = detail.UnitPrice;
- item.TotalPrice = detail.Quantity * detail.UnitPrice * detail.Discount;
- response .Details.Add(item);
- }
- return response;
- }
- }
As you can see, the adapter fl attens the Order’s graph as appropriate to suit the presentation layer. The presentation layer, in turn, receives only the data it needs and in the format that it prefers. The presentation layer doesn’t know anything about the Order object in the underlying domain model.
DTO in Action: Update an Order
Let’s consider another example: the user navigates to a screen where she is allowed to enter changes to an existing order. How would you handle this in terms of data transfer between the presentation and service layers?
The response we get from the service method should contain a failure/success flag and additional information in case of errors. We clearly need an ad hoc class here. What about the input data for the service method? Ideally, we pass only the list of changes. Here’s a possible signature for the method:
OrderServiceUpdateOrderResponse Update(OrderServiceUpdateOrderRequest request);
The response is simple. Possible errors reported to the user interface are caused by failures in the execution of the request.
- public class OrderServiceUpdateOrderResponse
- {
- public bool Success;
- public string[] Errors;
- }
You can use a flag or perhaps an error code to indicate the success of the operation. You can also use another member to communicate additional information, such as a list of errors or suggestions for retrying.
Note If the service is for internal use only and is not publicly exposed in an SOA manner (that is, it is a vanilla class), it might be acceptable for you to make it throw any exceptions directly to the caller—the presentation layer. If the service layer might not be local, and you want to reserve the possibility to move it to a different machine, you have to code the service to swallow exceptions and return ad hoc data structures. If you want an approach that works regardless of the service layer implementation, the only option is creating ad hoc structures.
We want to let the service know about what has changed in the order. How can we formalize this information? An order update is essentially a list of changes. Each change is characterized by a type (update, insertion, or deletion) and a list of values. To specify new order values, we can reuse the same OrderItem DTO we introduced for the previous load scenario:
- public class OrderServiceUpdateOrderRequest
- {
- public int OrderID;
- public List<OrderChange> Changes;
- }
- public class OrderChange
- {
- public OrderChangeTypes TypeofChange;
- public OrderItem NewItem;
- }
Internally, the service method loops through the requested changes, validates values, attempts updates, and tracks notifi cations and errors. Here’s a general skeleton for the method:
- public OrderServiceUpdateOrderResponse Update(OrderServiceUpdateOrderRequest request)
- {
- // Count insertions (if only insertions are requested, avoid loading the graph)
- int insertCount = (from op in request.Changes
- where op.TypeOfChange == OrderChangeTypes.Insert).Count();
- if (insertCount == request.Changes.Count)
- {
- foreach(OrderChange change in request.Changes)
- {
- Order newnewOrder = new Order();
- InsertOrderIntoPersistence(newOrder);
- }
- }
- // Load the graph for the order using the data access layer
- // (You can directly use an O/RM here or perhaps a repository or your data mappers)
- Order order = LoadOrderFromPersistence(request.OrderID);
- foreach(OrderChange change in request.Changes)
- {
- switch(change.TypeOfChange)
- {
- }
- }
- // Prepare the response OrderServiceFindByIDAdapter adapter = new OrderServiceFindByI
- DAdapter(order);
- OrderServiceUpdateOrderResponse response = new OrderServiceUpdateOrderResponse();
- return response;
- }
There are some aspects of the method you can optimize. For example, you can avoid loading the order’s graph from the data access layer if only insertions are requested. For deletions and updates, you likely need to have the graph available to check conditions and detect possible confl icts. However, you do not necessarily have to load the entire graph. Lazy loading could be used to minimize the quantity of data loaded.
Note It is not unusual that you have to dynamically adjust your fetch plan to be as lazy as possible. Most tools you might want to use in this context—from LINQ-to-SQL to NHibernate—allow you to dynamically confi gure the fetch plan. For example, in LINQ-to-SQL, you get this ability through the LoadOptions class in the data context.
When You Can Do Without DTOs
If we had to preserve the aesthetics and harmony of the solution and the purity of the architecture, we would opt for DTOs all the way through the system. However, we live in an imperfect world where pragmatism is the key to fi nding a happy medium between purity and reality.
We’ve learned from our experiences in the fi eld that a DTO-only approach is impractical (even insane, we would say) in the context of large projects with hundreds of entities in the domain model. What can you use instead? There is only one alternative: using domain objects directly.
This solution, though, should not be applied with a light heart. In particular, be aware that some project conditions make it more favorable and worthwhile. Which conditions are those?
The ideal situation to partially or entirely remove the DTO constraint is when the presentation and service layers are located on the same layer and, ideally, in the same process. For example, this can be a .NET-to-.NET scenario where the same common language runtime (CLR) types can be understood on both sides. (See Figure 5-9.)
When the presentation layer is ASP.NET, services implemented as ASMX Web services or WCF services can live side by side in the same Internet Information Services (IIS) worker process. If the presentation and service layers are not hosted in the same process, you have an additional nontrivial problem to deal with: serializing CLR types over the process boundary.
As we’ve seen, in a domain model it’s common and desirable to have circular references between objects. This is not an issue if objects are referenced in-process, but out-of-process references (and it doesn’t matter whether it is the same layer or different tiers) are another story.
In a .NET-to-.NET scenario, you can opt for a .NET remoting bridge for the cross-process serialization. Unlike the XML serialization used for Web and WCF services, the binary formatter used by .NET remoting supports circular references. This is not the main point, though. Would you really want the entire graph to be serialized? If you did that, you would be back to square one. If DTOs are not a practical option, you can still use domain objects in the signature of service methods, but use smart and lazy strategies to pull data into these objects.
The presentation layer still expects to receive, say, an Order; however, the Order object it actually gets doesn’t necessarily have all of its members set to a non-null value. To make it easier for the presentation layer to fi gure out which combination of data the domain object provides, you can implement in the domain object a number of alias interfaces. In this way, the presentation layer knows that if, say, interface ILoadOrder is found, only a contracted subset of properties is available. Note that when it comes to moving domain objects between tiers, there’s no generally agreed-upon solution that works in all cases. Mostly, the solution is left to your creativity and to your specifi c knowledge of the project.
All in all, valid reasons for not using DTOs can be summarized as follows:
There are hundreds of entities in the domain model.
It is OK for the presentation layer to receive data in the format of domain objects (or, at least, an adapter layer on the presentation layer is not an issue).
Presentation and service layers are co-located in the same process.
In many of our largest projects, we opt for a mixed approach. We tend to use mostly domain objects, thus saving our teams the burden of dealing with a lot of extra classes—which are a nightmare for maintenance. At the same time, when the distance between domain model and presentation layer is signifi cant, we resort to made-to-measure and handwritten, DTOs.
Note The emphasis on the adjective handwritten is not accidental. The issue with DTOs is not the extra layer of code (whose benefi ts we largely recognize) that is needed, but with the need to write and maintain many more classes. With 400 classes in a domain model, this likely means managing more than 1000 classes. Avoid that situation if you can.
What we would really like to have is a tool that weds awareness of a domain model with the ability to generate the code for graphically designed DTO classes. We would gladly welcome a tool that was based on the Visual Studio 2008 class designer and that, for Entity Framework and LINQ-to-SQL, would provide the ability to add a DTO. Ideally, the wizard for doing this would let you select properties to import from an existing graph and add an autogenerated C# class to the project that you could further extend with custom properties.
A DTO, in fact, is not necessarily a mere a subset of a particular domain object, with some of its properties and none of its methods. A DTO can be signifi cantly different from any domain objects. You don’t need to have a one-to-one or one-to-n correlation between domain objects and DTOs. You can also have DTOs that are projections of a portion of the domain model. For this reason, we would welcome a designer tool for autogenerating code for DTOs.
What You Gain and What You Lose
If you have DTOs, your design of the system is loosely coupled and open to a variety of consumers—although they are not necessarily known at the time the system is architected.
If you are at the same time the provider and consumer of the service layer and have control over the presentation, there are clear benefi ts in sharing the domain model via an assembly.
As usual, this is not a point that can be grossly generalized. You should realize that whatever options we propose here come with a “Well, it depends” clause.
A 100 percent DTO approach has its origins in SOA, which we’ll say more about in just a few moments. The benefi ts of SOA go beyond the contract with and implementation of a service layer. SOA in a distributed application is all about ensuring a loose coupling between layers so that versioning, maintenance, and even evaluation of the threat model are simplifi ed and made more effective.
As part of an SOA, an adapter layer placed in between the domain model and public data contracts isolates the domain model from the rest of the system. This arrangement costs you something, but it is an effort that pays off by allowing changes to be made without affecting consumers.
The problem is, will the payoff be enough for you? Will the subsequent cost/benefi t ratio be positive? We all agree on the added value that an SOA brings to a distributed application. Can we afford it?
In large projects with hundreds of entities, DTOs add a remarkable level of (extra) complexity and work. Tightly coupled layers, though, might be a remedy that’s worse than the disease.
An SOA is a great thing, but its benefi ts might not be evident when used with a single application. A strict adherence to SOA principles is especially benefi cial for the information system as a whole, but that’s not always so for a single application.