Restful中传输对象DTO的研究

文章目录

  • DataTransferObject
  • ValueObject vs DTO
    • getter的名字和返回值
    • int vs Integer
    • boolean vs Boolean
    • Stream vs List
    • enum vs (String or DTO)
    • 总结
  • DTO vs PO
  • ViewObject vs DTO
  • AllArgumentConstructor
  • extends

DataTransferObject

根据Restful风格,对于资源操作后需要返回当前资源(即使是DELETE操作),也就意味着对于一个资源的多种业务操作都需要返回一个统一的Response格式
另外在分布式环境下为了解除耦合,常常使用发消息来完成通信,由于业务操作的多种多样,对于Listener而言(特别是QueryService接收CommandService的消息)而言,也需要一个统一的Message格式
为了方便起见,把上面两种情况使用同一个结构,定义为DataTransferObject,这是一个实体类,主要是完成两个服务间通信.在这种情况下,DTO暗含了一种承诺,那就是其中的任意一个字段发生变动时就要发送一条消息从而更新QueryService的数据
在实际代码中会出现在以下几种情况

  • 对前端的返回
@RestController
class XxxController{
	@PutMapping("/api/xxx/{id}")
	public XxxDTO doSome(@PathVariable("id")String id,@RequestBody SomeCommand command){
		//业务逻辑
		return new XxxDTO(...);
	}
}
  • 对后端的返回
    代码类似于前面
  • 消息接收
    包括CommandService接收另一个CommandService的事件和BFF中QueryService接收来自CommandService的消息
class XxxListener{
	public void onMessage(XxxDTO dto){
		//响应事件
	}
}
  • 同步消息
    另一种情况其实是上面的情况变种,代码类似只是不再接收来自MessageQueue的消息而是同步调用.

ValueObject vs DTO

VO:ValueObject 相对于DomainObject而言,只有getter,是外界访问DomainObject的窗口
VO vs DTO:他俩最大的差别在于感知的不同,对于VO而言是OO的一种延续,它的返回应该也是VO,它似乎是在一个单体应用中.而DTO正是由于分布式环境而产生的,所以它必然能感知到服务与服务间的边界.
体现在代码上他们会有以下差别

getter的名字和返回值

interface DepartmentValueObject{
	
}
interface StaffValueObject{
	DepartmentValueObject getDepartment(){
		//...
	}
}

上面的VO的getter返回的仍然是一个VO对象.这里VO可以是class也可以是interface,关于这个的探讨我在别的文章在做说明

class StaffDTO{
	private String departmentId;
	String getDepartmentId(){
		return departmentId;
	}
	void setDepartmentId(String departmentId){
		this.departmentId=departmentId;
	}
}
class BaseDTO{
	private String id;
	//getter & setter
}
class StaffDTO{
	private BaseDTO department;
	BaseDTO getDepartment(){
		return department;
	}
	void setDepartment(BaseDTO department){
		this.department=department;
	}
}

在上面的两个例子中,区别是DTO的结构是否努力与VO保持一致(getter是否同名).后面的方法任然保留了OO的痕迹,对于前者则彻底放弃OO,从而更加方便.

int vs Integer

对于一些原始类型(特别是int,其他的也许有专有对象,例如Money),如果必然非空,VO可以使用int,而DTO需要使用Integer

boolean vs Boolean

对于一个业务上必然非空的布尔字段而言,VO可以返回boolean格式从而解除调用方担心null的忧虑,而DTO按照规范需要返回的是Boolean来确保不会篡改另一个服务的原始数据.
另外这种差别也导致了他们的getter方法的名字的差别从而影响字段名的差别

interface AccountValueObject{
	boolean isActive();
}
class AccountDTO{
	private Boolean active;
	public Boolean getActive(){
		return active;
	}
	public void setActive(Boolean active){
		this.active=active;
	}
}
class AccountDTO{
	private Boolean isActive;
	public Boolean getIsActive(){
		return isActive;
	}
	public void setIsActive(Boolean isActive){
		this.isActive=isActive;
	}
}

上面又有两种DTO的写法,差别在于是否有is

Stream vs List

一般而言,为了声明不能修改的特性,VO的返回值可以是Stream,从而可以更方便的实现懒加载.而DTO考虑的json格式的转换,需要是List格式,并且一般不适用?格式的泛型

interface DepartmentValueObject{
	Stream<? extends StaffValueObject> getStaffs();
}
class DepartmentDO implements DepartmentValueObject{
	private List<String> staffIds;
	private final StaffRepository staffRepository;
	@Override
	public Stream<? extends StaffValueObject> getStaffs(){
		return staffIds.stream().map(staffRepository::lazyLoad);
	}
}

enum vs (String or DTO)

对于ValueObject而言,返回值可以是enum格式,但是对于DTO则不能这样做.因为当业务变动时enum需要增加一项时VO可以考虑增加,而DTO由于是产生关联的两个服务间共享的,另一个服务无法同时增加(甚至永远无法增加),为了保持兼容性,DTO不能使用enum.此时有两种做法,简单点就是使用String,但是这样仅仅解决报错的问题,对于QueryService而言,最好把name一并返回.
结合上面的,如果VO也被共享,那么使用enum也是有风险的.

总结

以上这些返回值的差别直接导致了VO和DTO之间不能有任何class extends class或class implements interface或interface extends interface的关系

DTO vs PO

PO:PersistenceObject 是Repository所关心的东西.一般来说RMDB中的每个表对应的Entity和NoSQL中的存储的对象都算是PO.在CQRS设计中,PO可以完全遵循设计范式而不用在乎冗余字段,而DTO的目的就是为了体现出这些容易字段.他们之间有类似关系
P e r s i s t e n c t O b j e c t ⇒ D o m a i n O b j e c t → V a l u e O b j e c t D a t a T r a n s f e r O b j e c t PersistenctObject \xRightarrow{DomainObject\rightarrow ValueObject} DataTransferObject PersistenctObjectDomainObjectValueObject DataTransferObject
这样看上去DTO和PO可以实现extends的关系.但其实不然.有以下两个原因

  • 数据类型不一致:其典型就是时间日期相关的,由于json中其实是使用字符串表示,并且一般是yyyy-MM-dd的格式,其与程序中的标准格式并不一致.
  • PO可以改动,而DTO作为对外的窗口,其格式一般不能变化.这一点在涉及数组相关更加明显
class LessonPO{
	Map<String,StudentRecordPO> studentRecords;
}
class LessonDTO{
	List<StudentRecordDTO> studentRecords;
}

由于难以确定唯一性,所以DTO最好使用List而不是Map,另外Domain使用Map可以完成更方便的查找,而QueryService接收DTO时一般是进行遍历操作,所以list更方便.另外上面还需要注意的一点是StudentRecordPO里可以没有studentId,因为这是entry的key,而StudentRecordDTO中必须有studentId
另外在PO中使用enum是完全可以的,因为不会被共享.另外PO也无法完全OO而需要考虑各种格式问题,所以PO相对于VO甚至和DTO的格式更加接近.另外DTO适合引入VO(否则客户端在import时也需要import),所以向DTO的转换就会出现toDTO来从Domain直接获得DTO.

ViewObject vs DTO

由于上面承诺的限制,DTO不能包含太多的字段,另外由于ViewObject的会有多个并且频繁变动,所以DTO和ViewObject应该完全切割没有联系.QueryService需要自行完成ViewObject的组装.

AllArgumentConstructor

为了避免增加字段后的遗漏,代码中使用全参构造来创建对象,并且这个全参构造方法需要有lombok自动产生.另外为了满足框架的序列化要求,还需要保留无参构造方法,所以字段也不能定义为final的,虽然逻辑上来说应该是final的
同样由于全参构造,对于内部类而言需要声明为static的,毕竟在创建Outter对象的时候需要把创建好的Inner对象作为参数传入.

extends

如果业务中出现了extends的时候(这个时候DomainObject,至少是BusinessObject和ValueObject应该出现继承),DTO是否也需要继承呢?
我们假设把Department分为两类,School和Office.本身这二者是使用了type字段作为区分.现在的问题是School需要加额外的操作,这个时候也许可以不增加DTO,但是当School需要增加新的字段,特别是School和Office各自需要增加各自的字段的时候,可能就需要SchoolDTO和OfficeDTO了.为了向前兼容从而减少客户端的改动仍然需要保留DepartmentDTO,更重要的是作为一个业务概念也许后面还会出现新的子类(例如Company),那么为了向后兼容也需要保留DepartmentDTO.那么现在的问题是SchoolDTO需要继承Office么?
我们先从一个细节着手,那就是SchoolDTO是否需要DepartmentDTO的所有字段?其他的都差不多,那我们回到type字段,对于SchoolDTO而言肯定是SCHOOL,那么这个字段似乎多次一举.如果SchoolDTO没有type字段那么自然不能继承DepartmentDTO.但是这里的问题是type只会有这两种么,以后会不会增加,特备是School是否会有两种type.考虑到这种可能type就是需要的了.但这个时候对于客户端就不能做出type只有两种的假设,更加不能做出SchoolDTO.type==SCHOOL的假设.那这个时候如何确定是不是SCHOOL呢,也许要加上isSchool这个方法了.而在这个时候SchoolDTO.isSchool==true.结合上面对于enum的讨论,type可以是一个DTO从而直接将name返回.
在上面的问题解决后我们再来看是否需要extends呢?如果实现了extends的,一个好处随之而来

class DepartmentValueObject{
	DepartmentDTO toDTO();
}
class SchoolDTO extends DepartmentDTO{
}
class SchoolValueObject{
	@Override
	SchoolDTO toDTO();
}

那就是toDTO可以被Override.但是让我们抵制住这种诱惑来看看两个DTO
让我们考虑下面这种复杂的数据结构

@Data
@AllArgumentConstrator
@NoArguamentConstrator
class ParentDTO{
	class InnerDTO{
	}
	private InnerDTO inner;
}

@Data
@AllArgumentConstrator
@NoArgumentConstrator
class ChildDTO extends ParentDTO{
	class InnerDTO extends ParentDTO.InnerDTO{
	}
	private InnerDTO inner;
}

上面的咋一看没有问题,但其实隐含了一个很大的问题,那就是子类的setInner未能正确的重写父类的方法.那么在反序列化时可能会出现问题.由于上面所说的全参构造的问题,即使如果让父类把inner定义成protected的也并不能解决问题

你可能感兴趣的:(SoftwareDesign)