我有一个对我的应用程序很重要的简单复制/克隆方法:
@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测试,在原始对象和它的副本之间进行比较,它们适用于现有字段,但它们不考虑新字段。
答案 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.
调用它来开始,那些不会简单地实现继承接口方法的类。
您无法强制程序员通过编译器从具体实现中覆盖方法。代码评论应该能够解决这些错误。如果他们没有与错过它的评论者说一句话。
抽象方法的实现更容易理解,因为如果你不这样做,编译器就会呻吟。