Java 如何避免在添加新字段时中断克隆/复制方法?

Java 如何避免在添加新字段时中断克隆/复制方法?,java,clone,deep-copy,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

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

  @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测试,在原始对象和它的副本之间进行比较,它们对现有字段很有效,但不考虑新字段

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

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协议缓冲区具有,因此您可以实际切换字段号,而不是名称:

for (FieldDescriptor fieldDesc : proto.getDescriptorForType().getFields()) {
  switch (fieldDesc.getNumber()) {
    case FIELD1_FIELD_NUMBER:
      // ...
    case FIELD2_FIELD_NUMBER:
      // ...
  }
}
这样做的好处是,您也可以了解已删除的案例,因为字段号将不再生成,这意味着测试开关将不再编译。

为此,我使用了我称之为“循环和开关”的测试:

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协议缓冲区具有,因此您可以实际切换字段号,而不是名称:

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

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

为什么代码复查没有覆盖

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

为什么继承层次如此之深?如果每个需要复制的类型都要实现一个
Copyable
接口,那么在开发过程中就很难忽略覆盖的缺失,也就不需要深入的继承层次结构。那些碰巧继承了
getCopy()
的适当基本实现的类可以通过
super调用它。
首先,那些不只是实现继承的接口方法的类

您不能强制程序员通过编译器重写具体实现中的方法。代码审查应该能够发现这样的错误。如果他们没有,那就和错过的评论员谈谈


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

为什么代码复查没有覆盖

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

为什么继承层次如此之深?如果每个需要复制的类型都要实现一个
Copyable
接口,那么在开发过程中就很难忽略覆盖的缺失,也就不需要深入的继承层次结构。那些碰巧继承了
getCopy()
的适当基本实现的类可以通过
super调用它。
首先,那些不只是实现继承的接口方法的类

您不能强制程序员通过编译器重写具体实现中的方法。代码审查应该能够发现这样的错误。如果他们没有,那就和错过的评论员谈谈


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

这种方法在面对更改时极不稳定。测试
assertEquals(复制、原件,“复制失败”)通常就足够了
@LewBloch,除了依赖于
equals
检查所有字段外。仅检查建立相等所需的字段。如果状态由其他字段确定,则应在
equals
等中对其进行说明。@LewBloch同样,使用equals检查无法处理您不打算克隆的字段,如
id
。这种方法在面对更改时极不稳定。测试
assertEquals(复制、原件,“复制失败”)通常就足够了@LewBloch,但依赖于
eq的除外