DTO vs. Assembly(转载)

探讨了数据传输对象(DTO)在不同场景下的应用及其优缺点,分析了DTO在服务导向与开发者导向环境中面临的挑战,并提出了使用域对象作为替代方案的情况。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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:

  1. ??? FindByID(int orderID);  
  2.  

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.


FIGURE 5-8 A possible graph for an Order entity 

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:

  1. OrderServiceFindByIDResponse FindByID(OrderServiceFindByIDRequest request);  
  2.  

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:

  1. public class OrderServiceFindByIDRequest  
  2. {  
  3. public int OrderID { get; set; };  
  4. }  
  5. public class OrderServiceFindByIDResponse  
  6. {  
  7. public int OrderID {get; set; };  
  8. public DateTime OrderDate { get; set; };  
  9. public DateTime RequiredDate { get; set; };  
  10. public bool Shipped { get; set; };  
  11. public DateTime ShippedDate { get; set; };  
  12. public string FullAddress { get; set; };  
  13. public string CompanyName { get; set; };  
  14. public string ShipperCompany { get; set; };  
  15. public List<OrderItem> Details { get; set; };  
  16. }  
  17. public class OrderItem  
  18. {  
  19. public int OrderDetailID { get; set; };  
  20. public int Quantity { get; set; };  
  21. public string Description { get; set; };  
  22. public decimal UnitPrice { get; set; };  
  23. public decimal Discount { get; set; };  
  24. public decimal TotalPrice { get; set; };  
  25. }  
  26.  

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:

  1. public OrderServiceFindByIDResponse FindByID(OrderServiceFindByIDRequest request)  
  2. {  
  3. // Load the graph for the order using the data access layer  
  4. // (You can directly use an O/RM here or perhaps a repository or your data mappers)  
  5. Order order = LoadOrderFromPersistence(request.OrderID);  
  6. // Prepare the response using an adapter  
  7. OrderServiceFindByIDAdapter adapter = new OrderServiceFindByIDAdapter(order);  
  8. OrderServiceFindByIDResponse response = adapter.Fill();  
  9. return response;  
  10. }  
  11. internal class OrderServiceFindByIDAdapter  
  12. {  
  13. private Order _order;  
  14. public OrderServiceFindByIDAdapter(Order order)  
  15. {  
  16. _order = order;  
  17. }  
  18. public OrderServiceFindByIDResponse Fill()  
  19. {  
  20. OrderServiceFindByIDResponse response = new OrderServiceFindByIDResponse();  
  21. response.OrderID = order.OrderID;  
  22. response.OrderDate = order.OrderDate;  
  23. response.FullAddress = String.Format("{0}, {1}, {2}",  
  24. order.Address, order.City, order.Country);  
  25. response.CompanyName = order.Customer.CompanyName;  
  26. response.ShipperCompany = order.Shipper.CompanyName;  
  27. foreach(OrderDetail detail in order.OrderDetails)  
  28. {  
  29. OrderItem item = new OrderItem();  
  30. item.OrderDetailID = detail.OrderDetailID;  
  31. item.Quantity = detail.Quantity;  
  32. item.Discount = detail.Discount;  
  33. item.Description = detail.Product.Description;  
  34. item.UnitPrice = detail.UnitPrice;  
  35. item.TotalPrice = detail.Quantity * detail.UnitPrice * detail.Discount;  
  36. response .Details.Add(item);  
  37. }  
  38. return response;  
  39. }  
  40. }  
  41.  

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.

  1. public class OrderServiceUpdateOrderResponse  
  2. {  
  3. public bool Success;  
  4. public string[] Errors;  
  5. }  
  6.  

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:

  1. public class OrderServiceUpdateOrderRequest  
  2. {  
  3. public int OrderID;  
  4. public List<OrderChange> Changes;  
  5. }  
  6. public class OrderChange  
  7. {  
  8. public OrderChangeTypes TypeofChange;  
  9. public OrderItem NewItem;  
  10. }  
  11.  

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:

  1. public OrderServiceUpdateOrderResponse Update(OrderServiceUpdateOrderRequest request)  
  2. {  
  3. // Count insertions (if only insertions are requested, avoid loading the graph)  
  4. int insertCount = (from op in request.Changes  
  5. where op.TypeOfChange == OrderChangeTypes.Insert).Count();  
  6. if (insertCount == request.Changes.Count)  
  7. {  
  8. foreach(OrderChange change in request.Changes)  
  9. {  
  10. Order newnewOrder = new Order();  
  11. InsertOrderIntoPersistence(newOrder);  
  12. }  
  13. }  
  14. // Load the graph for the order using the data access layer  
  15. // (You can directly use an O/RM here or perhaps a repository or your data mappers)  
  16. Order order = LoadOrderFromPersistence(request.OrderID);  
  17. foreach(OrderChange change in request.Changes)  
  18.  
  19. {  
  20. switch(change.TypeOfChange)  
  21. {  
  22. }  
  23. }  
  24. // Prepare the response OrderServiceFindByIDAdapter adapter = new OrderServiceFindByI  
  25. DAdapter(order);  
  26. OrderServiceUpdateOrderResponse response = new OrderServiceUpdateOrderResponse();  
  27. return response;  
  28. }  

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.)


FIGURE 5-9 Avoiding DTOs in a .NET-to-.NET scenario 

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.

<think>我们正在讨论MyBatis的动态SQL中,判断一个集合(例如dto.skuCodes)的大小时,应该使用`dto.skuCodes.size==1`还是`dto.skuCodes.size()==1`。在MyBatis的动态SQL中,我们通常使用OGNL表达式来访问对象的属性和方法。OGNL(Object-GraphNavigationLanguage)是一种表达式语言,用于访问Java对象树中的属性和方法。对于集合的大小,有两种方式:1.使用属性:`size`,这是Java集合框架中`Collection`接口的一个属性(实际上是通过方法`size()`方法实现的,但OGNL将其视为属性)。2.使用方法:`size()`,直接调用集合的`size()`方法。在OGNL中,访问属性时不需要括号,而调用方法时需要括号。但是,对于集合的`size`属性,OGNL实际上会调用`size()`方法。因此,两种方式在功能上是等价的。然而,在MyBatis的动态SQL中,更常见的是使用属性访问方式,即`dto.skuCodes.size`,因为它更简洁,并且符合OGNL的属性访问规范。因此,在MyBatis中,推荐使用:`dto.skuCodes.size==1`注意:在OGNL中,属性访问会映射到对应的getter方法。所以,`dto.skuCodes.size`实际上会调用`dto.getSkuCodes().size()`。但是,如果集合为null,那么访问`dto.skuCodes.size`会抛出空指针异常。因此,在动态SQL中,我们通常需要先判断集合是否不为null,然后再判断大小。例如:<iftest="dto.skuCodes!=nullanddto.skuCodes.size==1">...</if>或者,如果使用的是MyBatis3.4.0及以上版本,可以使用更安全的null比较方式(避免null属性导致异常):<iftest="dto.skuCodes?.size==1">...</if>其中,`?.`是OGNL的安全导航运算符,如果`dto.skuCodes`为null,则整个表达式返回null,而不会抛出异常。在条件判断中,null会被视为false。结论:在MyBatis中,`dto.skuCodes.size==1`是更常见的写法,但要注意空指针问题。如果集合可能为null,则使用安全导航运算符或先判断非空。所以,回答用户的问题:两种写法在OGNL中都是有效的,但推荐使用`dto.skuCodes.size==1`,因为它更简洁,并且是OGNL的标准属性访问方式。同时,注意处理null的情况。注意:在XML中,`<`和`>`是特殊字符,如果写在动态SQL中,需要使用转义字符或者CDATA块。但是,在比较表达式中,我们可以使用`==`,而大小比较则使用`<`和`>`。不过,在这个问题中,我们只涉及相等比较,所以直接使用`==`即可。下面是一个示例:<selectid="selectProduct"resultType="...">SELECT*FROMproduct<where><iftest="dto.skuCodes!=nullanddto.skuCodes.size==1">ANDsku_code=#{dto.skuCodes[0]}</if><iftest="dto.skuCodes!=nullanddto.skuCodes.size>1">ANDsku_codeIN<foreachcollection="dto.skuCodes"item="skuCode"open="("separator=","close=")">#{skuCode}</foreach></if></where></select>在这个示例中,我们根据集合的大小分别处理单个元素和多个元素的情况。因此,用户应该使用`dto.skuCodes.size==1`。相关问题:1.在MyBatis动态SQL中如何安全地处理可能为null的集合?2.MyBatis动态SQL中常用的OGNL表达式有哪些?3.如何在MyBatis中遍历集合进行IN查询?4.什么是OGNL的安全导航运算符?它在MyBatis中如何使用?</think>在 MyBatis 的动态 SQL 中,正确写法是 **`dto.skuCodes.size() == 1`**。以下是详细解析: ### 原因分析 1. **OGNL 表达式规范**: - MyBatis 动态 SQL 使用 OGNL(Object-Graph Navigation Language)解析表达式 - OGNL **要求调用方法时必须带括号**,访问属性时才省略括号 - `size()` 是 `java.util.Collection` 接口的方法,必须使用括号调用 2. **语法差异**: - ✅ 正确写法:`dto.skuCodes.size() == 1` (调用 `List.size()` 方法) - ❌ 错误写法:`dto.skuCodes.size == 1` (尝试访问不存在的 `size` 属性,会抛出异常) 3. **空指针防护**: - 如果 `dto.skuCodes` 可能为 `null`,需使用安全导航: ```xml <if test="dto.skuCodes != null and dto.skuCodes.size() == 1"> ``` - MyBatis 3.4.0+ 支持安全导航运算符: ```xml <if test="dto.skuCodes?.size() == 1"> ``` ### 验证示例 ```xml <select id="selectProducts" resultMap="productMap"> SELECT * FROM products <where> <!-- 正确写法 --> <if test="dto.skuCodes != null and dto.skuCodes.size() == 1"> AND sku_code = #{dto.skuCodes[0]} </if> <!-- 错误写法(会抛出异常) --> <!-- <if test="dto.skuCodes.size == 1"> ... </if> --> </where> </select> ``` ### 技术原理 - MyBatis 将 `test` 表达式编译为 Java 代码时: ```java // 正确解析 if (dto.getSkuCodes().size() == 1) { ... } // 错误解析(实际不存在size属性) if (dto.getSkuCodes().getSize() == 1) { ... } // 抛出NoSuchMethodError ``` - 集合类型方法必须遵循 Java 标准语法[^1] ### 最佳实践 1. **始终对方法调用加 `()`** 集合操作:`list.size()`, `map.keySet()`, `array.length` 2. **属性访问不加 `()`** 如 `dto.name`, `product.price` 3. **防御性空检查**: ```xml <if test="dto.skuCodes != null && !dto.skuCodes.isEmpty()"> ``` > **关键结论**:在 MyBatis 动态 SQL 中,方法调用必须使用括号。因此 `dto.skuCodes.size() == 1` 是唯一正确写法[^2]。 --- **相关问题**: 1. MyBatis 动态 SQL 中如何安全遍历可能为空的集合? 2. OGNL 表达式支持哪些集合操作方法? 3. 如何在 MyBatis 中处理嵌套 DTO 的属性判断? 4. MyBatis 动态 SQL 的空指针防护最佳实践有哪些? [^1]: MyBatis 官方文档对 OGNL 表达式的说明:方法调用需显式使用括号 [^2]: OGNL 语言规范:访问 Java 方法必须使用标准方法调用语法
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值