最近在读《实现领域驱动设计》这本书,对于业务模型有了很多的见解,也知道该怎么去设计一个系统,下面我通过一个例子,将我之前的代码进行一个重构操作
前言
如果你现在在使用Eclipse,当然不是说Eclipse完全是落后的,相比于IDEA,内存消耗和搜索方面是一个非常大的亮点,但是建议还是用IDEA,也就是JetBean出品的那一套,如果你是学生或者毕业不太久的学生,用你的教育邮箱就可以免费得到一个专业版的,何乐不为,至于更多IDEA的好处,可以Google一下看看
代码重构
规范化
根据以往程序员观念,包括我之前代码,都有这个毛病
- domain包名
之前关于包名,都是用com.xxx.domain来命名,觉得这个是一个领域对象,针对每一个数据库表都建立一个domain来对应,但是实际上不是这样,Domain是一个领域对象,在实现领域驱动设计中,Domain是一个贫血模型,是没有行为的,或者说是没有实现领域模型的行为。所以这些对象应该属于entity对象,而不是领域对象,应该命名为com.xxx.entity, 当然具体贫血模型和领域对象的区别最好是看看这本书。
- DTO
对于DTO对象,很多人认为只有在输入输出里面算,或者只能在上层调用对象才算DTO,但是这种说法不完全正确,对于DTO其实只要在网络中传输的对象,都可以叫DTO对象,比如RPC调用等等。
场景描述
现在有一个商品项目,我们有一个用户信息表,需要维护,里面有三个字端:username,Age,Sex
@RequestMapping("/v1/api/user")
@RestController
public class UserApi {
@Autowired
private UserService userService;
@PostMapping
public User addUser(UserInputDTO userInputDTO){
User user = new User();
user.setUsername(userInputDTO.getUsername());
user.setAge(userInputDTO.getAge());
user.setSex(userInputDTO.getSex)
return userService.addUser(user);
}
}
复制代码
相信很多人都这样写的,在Controller收到UserDTO对象,我们需要在Service层转换成BO或者Entity对象
重点就在这一步
User user = new User();
user.setUsername(userInputDTO.getUsername());
user.setAge(userInputDTO.getAge());
user.setSex(userInputDTO.getSex)
复制代码
但是就出现个问题,现在三个字端已经够繁杂了,但是如果20个字端,那代码冗余度就很高了,所以这是最不推荐的做法。
使用工具类
我们了解到,这个时候拷贝技术就用到了,直接拷贝过来是最方便最优雅的,比如org.springframework.beans.BeanUtils#copyProperties这方法,我们用这个工具类直接进行拷贝,这里注意,这个方法是一个浅拷贝方法,我们优化一下代码
这里注意,阿里手册上是不推荐使用Apache的BeanUtils,因为性能问题,但是这是Spring的工具类
@PostMapping
public User addUser(UserInputDTO userInputDTO){
User user = new User();
BeanUtils.copyProperties(userInputDTO,user);
return userService.addUser(user);
}
复制代码
这样的话,代码就精简多了,只要把user这个entity对象的字段设置和UserInputDTO对象字端一样就行了,就算再多字端也不怕了。
语义问题
上面代码看起来精简了很多,但是是存在语义问题的,因为不具备很好的可读性,所以我们最好还是专门写在一个方法里面,实现DTO的转换,详细如下
@PostMapping
public User addUser(UserInputDTO userInputDTO){
User user = convertFor(userInputDTO);
return userService.addUser(user);
}
// 专门实现一个私有方法,来对DTO实现转换
private User convertFor(UserInputDTO userInputDTO){
User user = new User();
BeanUtils.copyProperties(userInputDTO,user);
return user;
}
复制代码
这里其实也应该引起我们的注意,就是我们写代码时候,也要考虑到不要随便实现一个转化,可读性很差,而且改动也是直接在原有地方改,风险很大,例如如果转换方式变了这里,你就要修改addUser方法,下面这种方法,直接在ConvertFor改动即可。所以我们应该将相同语义的代码放到同一个层次地方,这里可以看到,我们将转换方法convertFor私有化了,在重构书里,我们把这种重构方式叫做Extract Method,如何在同一个方法中,做一组相同层次的语义操作,而不是暴露具体的实现。
抽象接口定义
在实际写代码时候,我们可能需要大量做一个这样的操作,UserDTO转换,ItemDTO转换等等,我们应该将这个共同操作给抽离出来,这样所有操作就有规则执行了,这个时候,我们知道,convertFor这个方法就不能是一个统一方法,因为入参是根据不同DTO变的,这个时候我们就需要用泛型了。我们定义一个抽象接口。
public interface DTOConvert {
T convert(S s);
}
复制代码
现在这个接口实现了,我们应该将ConvertFor实现类重新实现一遍了
public class UserInputDTOConvert implements DTOConvert {
@Override
public User convert(UserInputDTO userInputDTO) {
User user = new User();
BeanUtils.copyProperties(userInputDTO,user);
return user;
}
}
复制代码
这样,在Service层,我们就将代码规范了
@RequestMapping("/v1/api/user")
@RestController
public class UserApi {
@Autowired
private UserService userService;
@PostMapping
public User addUser(UserInputDTO userInputDTO){
User user = new UserInputDTOConvert().convert(userInputDTO);
return userService.addUser(user);
}
}
复制代码
Code Review
我们在看看这里面,这里有个小问题,在AddUser这里,直接返回User,暴露信息很多,前文我们说,既然进去是DTO,出来也是DTO,那么这里我们完全可以在规范一点,返回的也是一个DTO对象,没有必要直接返回一个完整的User对象
@PostMapping
public UserOutputDTO addUser(UserInputDTO userInputDTO){
User user = new UserInputDTOConvert().convert(userInputDTO);
User saveUserResult = userService.addUser(user);
UserOutputDTO result = new UserOutDTOConvert().convertToUser(saveUserResult);
return result;
}
复制代码
我们在这里,你会发现,new这样一个DTO转化对象是没有必要的,而且每一个转化对象都是由在遇到DTO转化的时候才会出现,那我们应该考虑一下,是否可以将这个类和DTO进行聚合呢
User user = new UserInputDTOConvert().convert(userInputDTO);
复制代码
我们用的就是这个convert方法,我们直接将其融合到UserInputDTO里面
public class UserInputDTO {
private String username;
private int age;
private String sex;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getSex(){
return sex;
}
public void setSex(String sex){
this.sex = sex;
}
public User convertToUser(){
UserInputDTOConvert userInputDTOConvert = new UserInputDTOConvert();
User convert = userInputDTOConvert.convert(this);
return convert;
}
private static class UserInputDTOConvert implements DTOConvert{
@Override
public User convert(UserInputDTO userInputDTO) {
User user = new User();
BeanUtils.copyProperties(userInputDTO,user);
return user;
}
}
}
复制代码
这样可读性也很高,我们的输入DTO提供了转换Entity方法
这样在Service中的转换
User user = userInputDTO.convertToUser();
User saveUserResult = userService.addUser(user);
复制代码
再看工具类
我们上文实现了一个工具类,通过定义一个抽象接口,我们能够实现转换,但是这样转换是不完美的,很多工具类都是有转换类的,比如GUAVA的源码中也有一个转换类,我们可以参考一下,看有什么不同。
// com.google.common.base.Convert转换
public abstract class Converter<A, B> implements Function<A, B> {
protected abstract B doForward(A a);
protected abstract A doBackward(B b);
//其他略
}
复制代码
我们看到,他是实现了两个抽象方法,doForward 和doBackward方法,也就是我们说的正向和逆向转化,我们可以仿照写一下
原来的
public class UserInputDTOConvert implements DTOConvert {
@Override
public User convert(UserInputDTO userInputDTO) {
User user = new User();
BeanUtils.copyProperties(userInputDTO,user);
return user;
}
}
复制代码
修改一下
private static class UserInputDTOConvert extends Converter<UserInputDTO, User> {
@Override
protected User doForward(UserInputDTO userInputDTO) {
User user = new User();
BeanUtils.copyProperties(userInputDTO,user);
return user;
}
@Override
protected UserInputDTO doBackward(User user) {
UserInputDTO userInputDTO = new UserInputDTO();
BeanUtils.copyProperties(user,userInputDTO);
return userInputDTO;
}
}
复制代码
可能你觉得这样写有么有必要,但是在大多数系统中,入参和形参都是一样的,这样的话,正向转换和逆向转化就很方便了
例如我们将入DTO和出DTO都综合在一起,组成一个UserDTO,可以正向转也可以逆向转
public class UserDTO {
private String username;
private int age;
private String sex;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getSex(){
return sex;
}
public void setSex(){
this.sex = sex;
}
public User convertToUser(){
UserDTOConvert userDTOConvert = new UserDTOConvert();
User convert = userDTOConvert.doForward(this);
return convert;
}
public UserDTO convertFor(User user){
UserDTOConvert userDTOConvert = new UserDTOConvert();
UserDTO convert = userDTOConvert.doBackward(user);
return convert;
}
private static class UserDTOConvert extends Converter<UserDTO, User> {
@Override
protected User doForward(UserDTO userDTO) {
User user = new User();
BeanUtils.copyProperties(userDTO,user);
return user;
}
@Override
protected UserDTO doBackward(User user) {
UserDTO userDTO = new UserDTO();
BeanUtils.copyProperties(user,userDTO);
return userDTO;
}
}
}
复制代码
这样在Service层的代码就更加精简了,因为入和出都是一样的
@PostMapping
public UserDTO addUser(UserDTO userDTO){
User user = userDTO.convertToUser();
User saveResultUser = userService.addUser(user);
UserDTO result = userDTO.convertFor(saveResultUser);
return result;
}
复制代码
在特殊情况下,入和出都不一定是一样的,所以我们需要禁用逆向
private static class UserDTOConvert extends Converter<UserDTO, User> {
@Override
protected User doForward(UserDTO userDTO) {
User user = new User();
BeanUtils.copyProperties(userDTO,user);
return user;
}
@Override
protected UserDTO doBackward(User user) {
throw new AssertionError("不支持逆向转化方法!");
}
}
复制代码
bean验证
现在我们写完了接口,但是可能也存在个问题,就是我们好像没有严重DTO,看起来好像比较完美,可能你也会存在疑问,比如关于验证,无论是前端提供限制,还是权限验证,这些不都做了嘛,后端还需要什么验证。
任何调用我api或者方法的人,比如前端验证失败了,或者某些人通过一些特殊的渠道(比如Charles进行抓包),直接将数据传入到我的api,那我仍然进行正常的业务逻辑处理,那么就有可能产生脏数据!
Jar 303验证
public class UserDTO {
@NotNull
private String username;
@NotNull
private int age;
@NotNull
private String sex;
// 余下省略
}
复制代码
api验证
@PostMapping
public UserDTO addUser(@Valid UserDTO userDTO){
User user = userDTO.convertToUser();
User saveResultUser = userService.addUser(user);
UserDTO result = userDTO.convertFor(saveResultUser);
return result;
}
复制代码
我们将这个验证传到前端,并转换为一个API异常
@PostMapping
public UserDTO addUser(@Valid UserDTO userDTO, BindingResult bindingResult){
checkDTOParams(bindingResult);
User user = userDTO.convertToUser();
User saveResultUser = userService.addUser(user);
UserDTO result = userDTO.convertFor(saveResultUser);
return result;
}
private void checkDTOParams(BindingResult bindingResult){
if(bindingResult.hasErrors()){
//throw new 带验证码的验证错误异常
}
}
复制代码
BindingResult是Spring MVC验证DTO后的一个结果集,可以参考spring 官方文档
lomlock
lomlock当初用得很早,详细很多人都在用这个工具,能够省略我们大量getter setter操作
@Setter
@Getter
public class UserDTO {
@NotNull
private String username;
@NotNull
private int age;
public User convertToUser(){
UserDTOConvert userDTOConvert = new UserDTOConvert();
User convert = userDTOConvert.convert(this);
return convert;
}
public UserDTO convertFor(User user){
UserDTOConvert userDTOConvert = new UserDTOConvert();
UserDTO convert = userDTOConvert.reverse().convert(user);
return convert;
}
private static class UserDTOConvert extends Converter{
@Override
protected User doForward(UserDTO userDTO) {
User user = new User();
BeanUtils.copyProperties(userDTO,user);
return user;
}
@Override
protected UserDTO doBackward(User user) {
throw new AssertionError("不支持逆向转化方法!");
}
}
}
复制代码
当然如果只是做这些做操作当然不足以体现lomlock的强大,具体详细查看文档
链式风格
这在大数据一些框架里面很多体现,通常一个类有大几个个方法,而且要重复调用,甚至还有顺序
例如赋值操作
User user = new User();
user.setName("fourous");
user.setPassword("12345");
复制代码
同样的,如果有20个属性,这个清单会拉很长
我们将这个类再优化一下
public class Student {
private String name;
private int age;
public String getName() {
return name;
}
public Student setName(String name) {
this.name = name;
return this;
}
public int getAge() {
return age;
}
public Student setAge(int age) {
return this;
}
}
复制代码
现在调用变成了
User user = new User();
user.setName("fourous").setPassWord("12345");
复制代码
好,我们在用lomlock优化
@Accessors(chain = true)
@Setter
@Getter
public class Student {
private String name;
private int age;
}
复制代码
使用静态构造方法
我们之前发现,每次都要new 一个对象,其实我们可以用静态构造方法来简化一部分,语义也更加优美一点
例如对于数组创建
List list = new ArrayList();
复制代码
而在GUANA中,是这样的,提供了一个Lists工具类
Listlist = Lists.newArrayList();
复制代码
Lists命名是一种约定(俗话说:约定优于配置),它是指Lists是List这个类的一个工具类,那么使用List的工具类去产生List,这样的语义是不是要比直接new一个子类来的更直接一些呢,答案是肯定
再回过头来看刚刚的Student,很多时候,我们去写Student这个bean的时候,他会有一些必输字段,比如Student中的name字段,一般处理的方式是将name字段包装成一个构造方法,只有传入name这样的构造方法,才能创建一个Student对象。
这种完全可以用lomlock来优化
@Accessors(chain = true)
@Setter
@Getter
@RequiredArgsConstructor(staticName = "of")
public class Student {
@NonNull private String name;
private int age;
}
复制代码
这样创建对象时候就是这样的
Student student = Student.of("zs");
复制代码
我们链式调用一次
Student student = Student.of("zs").setAge(24);
复制代码
这样来的话,可读性强,而且代码冗余和代码量都不大
Build模式
我们设计模式有这个模式,我们先用原生的试试
public class Student {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public static Builder builder(){
return new Builder();
}
public static class Builder{
private String name;
private int age;
public Builder name(String name){
this.name = name;
return this;
}
public Builder age(int age){
this.age = age;
return this;
}
public Student build(){
Student student = new Student();
student.setAge(age);
student.setName(name);
return student;
}
}
}
复制代码
调用方式
Student student = Student.builder().name("zs").age(24).build();
复制代码
我们lomlock优化一下
@Builder
public class Student {
private String name;
private int age;
}
复制代码
调用方式
Student student = Student.builder().name("zs").age(24).build();
复制代码
代理模式
正如我们所知的,在程序中调用rest接口是一个常见的行为动作,如果你和我一样使用过Spring 的RestTemplate
,我相信你会我和一样,对他抛出的非http状态码异常深恶痛绝。
所以我们考虑将RestTemplate
最为底层包装器进行包装器模式的设计:
public abstract class FilterRestTemplate implements RestOperations {
protected volatile RestTemplate restTemplate;
protected FilterRestTemplate(RestTemplate restTemplate){
this.restTemplate = restTemplate;
}
//实现RestOperations所有的接口
}
复制代码
然后再由扩展类对FilterRestTemplate
进行包装扩展:
public class ExtractRestTemplate extends FilterRestTemplate {
private RestTemplate restTemplate;
public ExtractRestTemplate(RestTemplate restTemplate) {
super(restTemplate);
this.restTemplate = restTemplate;
}
public RestResponseDTOpostForEntityWithNoException(String url, Object request, ClassresponseType, Object... uriVariables)
throws RestClientException{
RestResponseDTOrestResponseDTO = new RestResponseDTO();
ResponseEntitytResponseEntity;
try {
tResponseEntity = restTemplate.postForEntity(url, request, responseType, uriVariables);
restResponseDTO.setData(tResponseEntity.getBody());
restResponseDTO.setMessage(tResponseEntity.getStatusCode().name());
restResponseDTO.setStatusCode(tResponseEntity.getStatusCodeValue());
}catch (Exception e){
restResponseDTO.setStatusCode(RestResponseDTO.UNKNOWN_ERROR);
restResponseDTO.setMessage(e.getMessage());
restResponseDTO.setData(null);
}
return restResponseDTO;
}
}
复制代码
包装器ExtractRestTemplate
很完美的更改了异常抛出的行为,让程序更具有容错性。
在这里我们不考虑ExtractRestTemplate
完成的功能,让我们把焦点放在FilterRestTemplate
上,“实现RestOperations
所有的接口”,这个操作绝对不是一时半会可以写完的
public abstract class FilterRestTemplate implements RestOperations {
protected volatile RestTemplate restTemplate;
protected FilterRestTemplate(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
@Override
public T getForObject(String url, ClassresponseType, Object... uriVariables) throws RestClientException {
return restTemplate.getForObject(url,responseType,uriVariables);
}
@Override
public T getForObject(String url, ClassresponseType, MapuriVariables) throws RestClientException {
return restTemplate.getForObject(url,responseType,uriVariables);
}
@Override
public T getForObject(URI url, ClassresponseType) throws RestClientException {
return restTemplate.getForObject(url,responseType);
}
@Override
public ResponseEntitygetForEntity(String url, ClassresponseType, Object... uriVariables) throws RestClientException{
return restTemplate.getForEntity(url,responseType,uriVariables);
}
//其他实现代码略。。。
}
复制代码
我们用lomlock就很简洁
@AllArgsConstructor
public abstract class FilterRestTemplate implements RestOperations {
@Delegate
protected volatile RestTemplate restTemplate;
}
复制代码