在验证 类 中使用哪种设计模式来避免 if/else?

Which design pattern to use to avoid if/else in validation classes?

我目前正在使用 HibernateConstraintValidator 来实现我的验证。但是我的审阅者不同意在代码中使用 if/else 或!运营商。我可以使用哪种设计模式来删除验证逻辑中的 if/else?

public class SomeValidatorX implements ConstraintValidator<SomeAnnotation, UUID> {

      @Autowired
      SomeRepository someRepository;
   
      @Override
      public boolean isValid(UUID uuid, ConstraintValidationContext context) {

             return !(uuid!=null && someRepository.existsById(uuid)); //The reviewer doesn't want this negation operator
      }
}

在下面的代码中,他不想要 if/else

public class SomeValidatorY implements ConstraintValidator<SomeAnnotation, SomeClass> {

      @Autowired
      SomeRepository someRepository;
   
      @Override
      public boolean isValid(SomeClass someObject, ConstraintValidationContext context) {
           if(someObject.getFieldA() != null) { //He doesn't want this if statement
                //do some operations
                List<Something> someList = someRepository.findByAAndB(someObject.getFieldA(),B);
                return !someList.isEmpty(); //He doesn't want this ! operator
           }
           return false; // He was not fine with else statement in here as well
      }
}

旁注:我们必须使用领域驱动设计(如果有帮助的话)

很久很久以前,在时间的开始。有一条准则说方法应该只有一个出口点。为此,开发人员必须跟踪本地状态并使用 if/else 才能到达方法的结尾。

今天我们知道得更多了。通过尽早退出方法,在阅读代码时更容易将整个流程牢记在心。更简单的代码意味着更少的错误。更少的错误等于更少的错误。

在我看来,这就是审阅者不喜欢代码的原因。它并不像它应该的那样容易阅读。

举第一个例子:

 public boolean isValid(UUID uuid, ConstraintValidationContext context) {

         return !(uuid!=null && someRepository.existsById(uuid)); //The reviewer doesn't want this negation operator
  }

代码说的是“不是这个:(uuid不应该为空并且必须存在)”。这容易理解吗?我觉得不是。

备选方案:“如果 uuid 不存在也可以,但如果存在,该项目可能不存在”。

或者在代码中:

if (uuid == null) return true;
return !someRepository.existsById(uuid);

更容易阅读,对吧? (我希望我的意图是正确的;))

第二个例子

      if(someObject.getFieldA() != null) { //He doesn't want this if statement
            //do some operations
            List<Something> someList = someRepository.findByAAndB(someObject.getFieldA(),B);
            return !someList.isEmpty(); //He doesn't want this ! operator
       }
       return false; // He was not fine with else statement in here as well

好的。你在这里说:

  • 如果字段 A 不为空:
    • 构建一个包含 A 和 b 的列表
    • 如果该列表不为空则失败,否则成功。
  • 否则失败

一个更容易得出结论的方法就是简单地说:

  • 不指定字段A也可以
  • 如果指定字段 A,则它必须与 B 组合存在。

翻译成代码:

if (someObject.getFieldA() == null) 
    return true;

return !someRepository.findByAAndB(someObject.getFieldA(),B).isEmpty();

在 C# 中,我们有 Any(),它与 isEmpty 相反,在这种情况下我更喜欢后者,因为它消除了否定。

有时需要否定。在存储库中编写新方法来避免它是没有意义的。但是,如果 findByAAndB 仅被此使用,我会将其重命名为 ensureCombination(a,b) 以便它可以 return 为有效案例。

试着边说边写代码,这样可以更容易地在脑海中创建代码的画面。你不是在说“我没吃饱,我们去吃午饭吧”,是吗? ;)

您可以检查空对象模式。 一般模式是从您的代码中完全禁止 null 。这消除了丑陋的 null 检查。在这一点上,我同意您的代码审查者的意见。

遵循以下建议将导致:

public boolean isValid(SomeClass someObject, ConstraintValidationContext context) {
  return someRepository.containsAAndB(someObject.getFieldA(), B);
}

避免空检查

在引入空对象模式之前,只需应用该模式或约定来强制初始化所有引用。这样您就可以确保整个代码中没有 null 引用。
因此,当您遇到 NullPointerException 时,您不会通过引入 null 检查来解决问题,而是通过初始化引用(在构造时),例如,通过使用默认值、空集合或空对象。

大多数现代语言都支持通过注释进行代码分析,例如 @NonNull,它会检查参数等引用,并在参数 null/未初始化时抛出异常。 javax.annotation例如提供这样的注释。

public void operation(@NonNull Object param) {
    param.toString(); // Guaranteed to be not null
}

使用此类注释可以防止库代码出现空参数。

空对象模式

不是 null 个引用,而是用有意义的值或专用的空对象初始化每个引用:

定义空对象契约(不需要):

interface NullObject {
  public boolean getIsNull();
}

定义基本类型:

abstract class Account {
  private double value;
  private List<Owner> owners;

  // Getters/setters    
}

定义空对象:

class NullAccount extends Account implements NullObject {

  // Initialize ALL attributes with meaningful and *neutral* values
  public NullAccount() {
    setValue(0); // 
    setOwners(new ArrayList<Owner>())

  @Override
  public boolean getIsNull() {
    return true;
  }
}

定义默认实现:

class AccountImpl extends Account implements NullObject {

  @Override
  public boolean getIsNull() {
    return true;
  }    
}

使用 NullAccount class:

初始化所有 Account 引用
class Employee  {
  private Account Account;

  public Employee() {
    setAccount(new NullAccount());
  }
}

或使用 NullAccount 到 return 失败状态实例(或默认)而不是 returning null:

public Account findAccountOf(Owner owner) {
  if (notFound) {
    return new NullAccount();
  }
}

public void testNullAccount() {
  Account result = findAccountOf(null); // Returns a NullAccount
  
  // The Null-object is neutral. We can use it without null checking.
  // result.getOwners() always returns 
  // an empty collection (NullAccount) => no iteration => neutral behavior
  for (Owner owner : result.getOwners()) {
    double total += result.getvalue(); // No side effect.
  }
}
  

尝试模式

您可以使用的另一种模式是 Try-Do 模式。您无需测试操作的结果,而只需测试操作本身。该操作对return操作是否成功负责。

在文本中搜索字符串时,return 是否找到结果的布尔值比 return 空字符串更方便,甚至更糟 null :

public boolean tryFindInText(String source, String searchKey, SearchResult result) {
  int matchIndex = source.indexOf(searchKey);
  result.setMatchIndex(matchIndex);
  return matchIndex > 0;
}

public void useTryDo() {
  SearchResult result = new Searchresult();
  if (tryFindInText("Example text", "ample", result) {
    int index = result.getMatchIndex();
  }
}

在您的特殊情况下,您可以将 findByAAndB() 替换为 containsAAndB() : boolean 实现。


组合模式

最终解决方案实现了Null-Object模式并重构了find方法。原来的findByAAndB()的结果之前被丢弃了,因为你想测试AB的存在。另一种方法 public boolean contains() 将改进您的代码。
重构后的实现如下所示:

abstract class FieldA {

}

class NullFieldA {

}

class FieldAImpl {

}

class SomeClass {

  public SomeClass() {
    setFieldA(new NullFieldA());
  }
}

改进的验证:

public boolean isValid(SomeClass someObject, ConstraintValidationContext context) {
  return someRepository.containsAAndB(someObject.getFieldA(), B);
}