如何在添加新字段时避免破解克隆/复制方法?

时间:2017-05-28 19:47:30

标签: java clone deep-copy

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

  @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;
 }

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

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

private String additionalInfo;

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

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

2 个答案:

答案 0 :(得分:2)

我使用我称之为“循环和切换”的测试:

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:
      // ...
  }
}

关于这一点的好处是你也发现了被删除的情况,因为不再生成字段编号,这意味着测试开关将不再编译。

答案 1 :(得分:0)

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

此外,复制方法不应该那么脆弱。如果您有"不应复制的特定字段"为什么他们可以看到儿童班?

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

您无法强制程序员通过编译器从具体实现中覆盖方法。代码评论应该能够解决这些错误。如果他们没有与错过它的评论者说一句话。

抽象方法的实现更容易理解,因为如果你不这样做,编译器就会呻吟。