【问题标题】:How to avoid breaking clone/copy method when new fields are added?添加新字段时如何避免破坏克隆/复制方法?
【发布时间】:2017-05-28 19:47:30
【问题描述】:

我有一个对我的应用程序很重要的简单复制/克隆方法:

  @Override
  public Operation getCopy() {
    Operation copy = new Operation();
    copy.year = this.year;
    copy.stage = this.stage;
    copy.info = this.info;
    copy.user = this.user.getCopy();
    // NOT TO BE COPIED! copy.id = this.id;
    ...
    return copy;
 }

请注意,有些特定字段不应被复制。还有一些复杂的对象(比如用户)有自己的复制方法。

问题在于,随着新代码的开发,有时开发人员会创建一个应该复制的新字段,但他忘记将其添加到copy 方法中:

private String additionalInfo;

即使没有编译错误,也存在业务问题,只有稍后我们的 QA 团队甚至用户才发现。

我能做些什么来防止这种情况发生?我已经尝试过在原始对象与其副本之间进行比较的 JUnit 测试,它们适用于现有字段,但它们不考虑新字段。

【问题讨论】:

    标签: java clone deep-copy


    【解决方案1】:

    我为此使用了我所谓的“循环和切换”测试:

    for (Field field : Operation.class.getFields()) {
      switch (field.getName()) {
        case "year":
          // Test that year is copied correctly.
          // Initialize blah so that year is set.
          assertEquals(getCopy(blah).year, blah.year);
          break;
        case "stage":
          // Test that stage is copied correctly.
          // Initialize blah so that stage is set.
          assertEquals(getCopy(blah).stage, blah.stage);
          break;
        case "id":
          // We don't want to copy id.
          // Initialize blah so that id is set.
          assertNull(getCopy(blah).id);
          break;
    
        // etc.
    
        default:
          throw new AssertionError("Unhandled field: " + field.getName());
      }
    }
    

    这不是一个很有想象力的名字:你循环遍历类中的所有字段,然后直接切换,这样你就可以分别明确地处理各个字段。

    这样做的好处是,default 的情况会立即发现对新添加字段的处理不足。你得到一个很大的耳光,说你需要在测试中处理它 - 并且,通过扩展,你也需要在生产代码中处理它。

    使用普通的旧 Java 反射的缺点是它不会捕获被删除的字段。这可能是一种“不那么糟糕”的情况,因为它只是留下了未使用的代码,而不是生产代码中存在未经测试的代码路径。


    我在构建协议缓冲区-协议缓冲区转换器时开发了(或在某处读过,遗憾的是我不记得了)这个习语。 Java 协议缓冲区有generated field numbers,因此您实际上可以打开字段编号,而不是名称:

    for (FieldDescriptor fieldDesc : proto.getDescriptorForType().getFields()) {
      switch (fieldDesc.getNumber()) {
        case FIELD1_FIELD_NUMBER:
          // ...
        case FIELD2_FIELD_NUMBER:
          // ...
      }
    }
    

    这样做的好处是您也可以发现已删除的案例,因为将不再生成字段编号,这意味着测试开关将不再编译。

    【讨论】:

    • 这种方式在面对变化时极不稳定。通常测试assertEquals(copy, original, "copy failed"); 就足够了。
    • @LewBloch 除了依赖equals 检查所有字段。
    • 只有那些需要建立平等的。如果状态由其他字段确定,则它们equals等中进行说明。
    • @LewBloch 同样,使用等号检查无法处理您不打算克隆的字段,例如 id
    【解决方案2】:

    为什么代码审查错过了缺少覆盖?

    另外,复制方法不应该那么脆弱。如果您有“不应复制的特定字段”,为什么它们对子类可见?

    为什么继承层次这么深?如果每个需要复制的类型都实现Copyable 接口,那么在开发过程中错过覆盖的缺失会更加困难,并且您不需要深度继承层次结构。那些恰好继承了getCopy() 的合适基实现的类可以通过super. 调用它来开始,那些不简单地实现继承的接口方法的类。

    您不能强制程序员通过编译器从具体实现中重写方法。代码审查应该发现这样的错误。如果他们没有,那么请与错过它的审稿人交流。

    抽象方法的实现更容易捕捉,因为如果你不这样做,编译器就会抱怨。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2020-11-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多