Domain Primitive - 图1

传统案例问题分析

  1. public class User {
  2. Long userId;
  3. String name;
  4. String phone;
  5. String address;
  6. Long repId;
  7. }
  8. public class RegistrationServiceImpl implements RegistrationService {
  9. private SalesRepRepository salesRepRepo;
  10. private UserRepository userRepo;
  11. public User register(String name, String phone, String address)
  12. throws ValidationException {
  13. // 校验逻辑
  14. if (name == null || name.length() == 0) {
  15. throw new ValidationException("name");
  16. }
  17. if (phone == null || !isValidPhoneNumber(phone)) {
  18. throw new ValidationException("phone");
  19. }
  20. // 此处省略address的校验逻辑
  21. // 取电话号里的区号,然后通过区号找到区域内的SalesRep
  22. String areaCode = null;
  23. String[] areas = new String[]{"0571", "021", "010"};
  24. for (int i = 0; i < phone.length(); i++) {
  25. String prefix = phone.substring(0, i);
  26. if (Arrays.asList(areas).contains(prefix)) {
  27. areaCode = prefix;
  28. break;
  29. }
  30. }
  31. SalesRep rep = salesRepRepo.findRep(areaCode);
  32. // 最后创建用户,落盘,然后返回
  33. User user = new User();
  34. user.name = name;
  35. user.phone = phone;
  36. user.address = address;
  37. if (rep != null) {
  38. user.repId = rep.repId;
  39. }
  40. return userRepo.save(user);
  41. }
  42. private boolean isValidPhoneNumber(String phone) {
  43. String pattern = "^0[1-9]{2,3}-?\\d{8}$";
  44. return phone.matches(pattern);
  45. }
  46. }

问题1:接口的清晰度

Java代码中,对一个方法来说所有的参数名会在编译时丢失,最后只会留下一个参数类型的列表。所以其实在运行时仅仅是:

  1. User register(String, String, String);

这种情况下在编译器是不会出错的,一般很难通过代码就能发现bug,普通的code review很难发现这种问题,只有在代码运行时才会报错,可能就会导致代码上限之后才会暴露出问题。

  1. service.register("殷浩", "浙江省杭州市余杭区文三西路969号", "0571-12345678");

类似的代码情况还有:

  1. // 不得不在方法名上面加上ByXXX来区分
  2. User findByName(String name);
  3. // 不得不在方法名上面加上ByXXX来区分
  4. User findByPhone(String phone);
  5. // 没办法保证参数传递的顺序,如果传递的是 phone, name,编译时也不会出错,在运行时才会报错
  6. User findByNameAndPhone(String name, String phone);

所以,需要有一种办法,让方法入参一目了然,避免入参错误导致的bug。
**

问题2:数据验证和错误处理

传统的做法是直接与业务逻辑代码耦合在一块,就会出现扩展性低,维护困难等问题,违背DRY原则。

  1. if (phone == null || !isValidPhoneNumber(phone) || !isValidCellNumber(phone)) {
  2. throw new ValidationException("phone");
  3. }
  4. # 如果有很多地方用到了phone这个入参,就需要在每个方法里面都加上上面这个验证逻辑,后期调整上也可能会因为某个地方忘记修改而造成bug
  5. # 如果有个新的需求,需要把入参错误原因返回,那么代码就会变得更复杂
  6. if (phone == null) {
  7. throw new ValidationException("phone不能为空");
  8. } else if (!isValidPhoneNumber(phone)) {
  9. throw new ValidationException("phone格式错误");
  10. }
  11. # 如果有大量这样的代码充斥在项目里面的话,维护成本就很高。
  12. # 最后在这个业务方法里面,会抛出ValidationException,所以需要外部调用try/catch,而业务逻辑异常和数据校验异常被混在一起,是极不合理的。

DRY原则:

在传统的架构里有几个方法能够解决一部分问题,常见的BeanValidation注解或ValidationUtils类,比如:

  1. // Use Bean Validation
  2. User registerWithBeanValidation(
  3. @NotNull @NotBlank String name,
  4. @NotNull @Pattern(regexp = "^0?[1-9]{2,3}-?\\d{8}$") String phone,
  5. @NotNull String address
  6. );
  7. // Use ValidationUtils:
  8. public User registerWithUtils(String name, String phone, String address) {
  9. ValidationUtils.validateName(name); // throws ValidationException
  10. ValidationUtils.validatePhone(phone);
  11. ValidationUtils.validateAddress(address);
  12. ...
  13. }
  14. # 但是传统的方法同样有问题
  15. BeanValidation
  16. - 通常只能解决简单的校验逻辑,复杂的校验逻辑一样要写代码实现定制校验器。
  17. - 在添加了新校验逻辑时,同样会出现某些地方忘记添加注解的情况,DRY原则还是违背了。
  18. ValidationUtil
  19. - 当大量的校验逻辑集中在一个类里之后,未被了单一职责的原则,导致代码混乱和不可维护。
  20. - 业务异常和校验异常还是会混杂。

问题3:业务代码的清晰度

在这段代码里:

  1. String areaCode = null;
  2. String[] areas = new String[]{"0571", "021", "010"};
  3. for (int i = 0; i < phone.length(); i++) {
  4. String prefix = phone.substring(0, i);
  5. if (Arrays.asList(areas).contains(prefix)) {
  6. areaCode = prefix;
  7. break;
  8. }
  9. }
  10. SalesRep rep = salesRepRepo.findRep(areaCode);
  11. # 这段代码里,出现一种情况,就是从一些入参里抽取一部分数据,比如上面的phone,然后调用一个外部依赖获取更多的数据,然后通常从新的数据中在抽取部分数据用作其他的作用,比如上面的areaCode 这种代码叫做:胶水代码。
  12. # 胶水代码:本质是由 外部依赖的服务的入参(areaCode) 并不符合我们 原始的入参(phone)导致的。
  13. # 所以,一个常见的方法是将这段代码抽离出来,编程独立的一个或多个方法
  14. private static String findAreaCode(String phone) {
  15. for (int i = 0; i < phone.length(); i++) {
  16. String prefix = phone.substring(0, i);
  17. if (isAreaCode(prefix)) {
  18. return prefix;
  19. }
  20. }
  21. return null;
  22. }
  23. private static boolean isAreaCode(String prefix) {
  24. String[] areas = new String[]{"0571", "021"};
  25. return Arrays.asList(areas).contains(prefix);
  26. }
  27. # 然后原始代码就变成:
  28. String areaCode = findAreaCode(phone);
  29. SalesRep rep = salesRepRepo.findRep(areaCode);
  30. # 而为了复用以上的方法,可能会抽离出一个静态工具类PhoneUtils。但是需要思考,静态工具类是否是最好的实现方式呢?当你的项目里充斥着大量的静态工具类,业务代码散在多个文件中的时,是否能够找到核心的业务逻辑呢?

问题4:可测试性