最近在读《实现领域驱动设计》这本书,对于业务模型有了不少的看法,也知道该怎么去设计一个系统,下面我经过一个例子,将我以前的代码进行一个重构操做前端
若是你如今在使用Eclipse,固然不是说Eclipse彻底是落后的,相比于IDEA,内存消耗和搜索方面是一个很是大的亮点,可是建议仍是用IDEA,也就是JetBean出品的那一套,若是你是学生或者毕业不过久的学生,用你的教育邮箱就能够免费获得一个专业版的,何乐不为,至于更多IDEA的好处,能够Google一下看看java
根据以往程序员观念,包括我以前代码,都有这个毛病程序员
以前关于包名,都是用com.xxx.domain来命名,以为这个是一个领域对象,针对每个数据库表都创建一个domain来对应,可是实际上不是这样,Domain是一个领域对象,在实现领域驱动设计中,Domain是一个贫血模型,是没有行为的,或者说是没有实现领域模型的行为。因此这些对象应该属于entity对象,而不是领域对象,应该命名为com.xxx.entity, 固然具体贫血模型和领域对象的区别最好是看看这本书。spring
对于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对象设计模式
重点就在这一步api
User user = new User();
user.setUsername(userInputDTO.getUsername());
user.setAge(userInputDTO.getAge());
user.setSex(userInputDTO.getSex)
复制代码
可是就出现个问题,如今三个字端已经够繁杂了,可是若是20个字端,那代码冗余度就很高了,因此这是最不推荐的作法。数组
咱们了解到,这个时候拷贝技术就用到了,直接拷贝过来是最方便最优雅的,好比org.springframework.beans.BeanUtils#copyProperties这方法,咱们用这个工具类直接进行拷贝,这里注意,这个方法是一个浅拷贝方法,咱们优化一下代码bash
这里注意,阿里手册上是不推荐使用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);
}
}
复制代码
咱们在看看这里面,这里有个小问题,在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("不支持逆向转化方法!");
}
}
复制代码
如今咱们写完了接口,可是可能也存在个问题,就是咱们好像没有严重DTO,看起来好像比较完美,可能你也会存在疑问,好比关于验证,不管是前端提供限制,仍是权限验证,这些不都作了嘛,后端还须要什么验证。
任何调用我api或者方法的人,好比前端验证失败了,或者某些人经过一些特殊的渠道(好比Charles进行抓包),直接将数据传入到个人api,那我仍然进行正常的业务逻辑处理,那么就有可能产生脏数据!
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当初用得很早,详细不少人都在用这个工具,可以省略咱们大量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);
复制代码
这样来的话,可读性强,并且代码冗余和代码量都不大
咱们设计模式有这个模式,咱们先用原生的试试
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;
}
复制代码