1. 认识DTO
DTO(Data Transfer Object,数据传输对象)—— 这个词是程序员圈子里出来的,尤其是在分布式应用和服务的世界里(比如一个服务通过网络调用另一个服务)。本质上,它就是个对象——大多数时候就是普通的C#类——唯一的任务就是:装一堆数据,然后在程序各层或者应用之间安全地传递。
想象一下客户端和服务器之间的聊天。每次用户发消息,客户端就会组一个带文本、时间和用户名的对象发给服务器,然后服务器再决定怎么处理。如果这个对象里没有多余的东西,只保留传递数据需要的内容,那这就是DTO类了。
public class UserDto
{
public int Id { get; set; }
public string Name { get; set; }
}
就这样!没有业务逻辑,没有复杂方法——只有属性。
只用来传数据的类
想象你在送披萨。你的快递员(DTO)不用会做披萨,不用接单,也不用修自行车。他唯一要做的事——就是把盒子从A点送到B点。DTO类也是这样:它的任务就是存简单数据。它不用自我校验,也不用知道怎么在UI里展示自己——只负责存数据。
- DTO就是“没有职业的人”或者“没有逻辑的对象”,只负责搬数据。
- 用它们是为了不让业务逻辑被数据传递的细节搞乱:箱子越小,越好搬。
2. 普通DTO类在实际中的问题
看起来好像很美好:写个带自动属性的类,生活美滋滋!但其实没那么简单。
数据可变性:会出啥问题
public class ProductDto
{
public int Id { get; set; }
public string Name { get; set; }
}
现在假设你在代码里不小心改了属性:
ProductDto dto = new ProductDto { Id = 1, Name = "牛奶" };
SomeApiProcess(dto);
dto.Name = "酸奶"; // 哎呀!
因为所有属性都有public set,很容易不小心把对象搞坏,或者更糟,在某处改了它,结果整个系统都跟着乱套。尤其是当对象被很多模块和线程用的时候,这问题就更大了。
复制和比较——无尽的体力活
比如你想复制一个ProductDto,只改名字。
var otherDto = new ProductDto { Id = dto.Id, Name = "奶酪" };
如果属性多,这就变成了超级无聊又容易出bug的手动复制每个字段的活。
要是你还得比较对象?那就得重写Equals和GetHashCode,很多人要么写得很随便,要么干脆忘了写。
不可变性——普通类做不到
大多数时候DTO应该是不可变的:数据拿到一次就不再改。但带自动属性的类根本不适合这种需求:谁都能随便改字段。
3. 在大项目里实际会怎样
来看个真实例子。
你是个大型电商的开发。你要通过网络传订单信息。于是你写了个这样的类:
public class OrderDto
{
public int Id { get; set; }
public string Customer { get; set; }
public double Total { get; set; }
}
一切都好,直到每周有几十万订单。各种微服务开始来回传DTO,有人忘了Total应该计算出来而不是手动填,有人发完后不小心改了Customer……然后你就会遇到超级难查的bug。
怎么改进?
- 只用get属性(不要set)。
- 写个专门的构造函数,只能在创建时赋值。
- 重写Equals和GetHashCode……
你明白了吗?本来应该是个“箱子”,结果变成了个四不像怪物。
4. DTO类常见问题小结
小项目里,DTO被改出bug还能“手动抓”。大项目里,这就是灾难。DTO用在授权、转账、订单处理——任何非法数据改动都可能变成钱的损失,甚至(卧槽!)刑事案件。
下面是写DTO类时几乎所有人都会遇到的“备忘单”问题:
| 问题 | 描述 |
|---|---|
| 可变性 | 数据随处都能改——bug风险大(多线程时尤其严重) |
| 克隆 | 复制对象很麻烦,经常得手写 |
| 比较 | 默认是按引用比,不是按值比 |
| 支持with复制 | 想只改部分数据复制,结果还得手动写 |
| 嵌套不直观 | 嵌套DTO要全量复制,超麻烦 |
5. 示例:我们应用里层与层之间的数据传递
比如我们有个日程本应用,可以存任务。以前我们会写这样一个类:
public class TaskDto
{
public int Id { get; set; }
public string Description { get; set; }
public DateTime DueDate { get; set; }
}
我们从文件里读TaskDto,加到集合里,再显示到屏幕上。都挺好……直到别的模块有人在序列化前不小心改了DueDate——用户就会一脸懵逼。
7. 能不能更好?
当然可以!而且不光能,还必须!为了解决这些问题,C#里有了个专门的东西——record。它几乎像开挂一样解决了DTO类的大部分头疼问题。
record能做什么:
- 默认按值比较:对象内容一样就相等,不是按引用。
- 不可变:只写get属性,值只能通过构造函数设。
- 轻松克隆:有with语法,复制时能改部分字段。
- 自动生成比较和输出方法。
public record TaskDto(int Id, string Description, DateTime DueDate);
酷吧?超级酷!不过这个我们下节课再聊。
GO TO FULL VERSION