欢迎光临诊断 Java 代码一个隔周更新的新专栏着重讨论和您日常编程工作有关的 Java 解决方案本文为第一篇介绍了错误模式的概念一个非常有用的概念它将提高您检测和修正代码中错误的能力您会了解到一种最普遍的错误模式这将为您开始识别和避免更高级的错误模式奠定基础
错误模式和它们为什么有用
正如好的编程技能涉及很多设计模式(您可以在不同的程序上下文中组合和应用这些模式)的知识一样好的调试技能也涉及对错误模式的一定了解错误模式就是已发出的错误和程序中潜在的错误之间的重复出现的相互关系这种概念对编程来说并不新鲜医生们在诊断疾病时依靠相似类型的相互关系他们在实习期间通过和资格较老的医生共同工作来学习这些他们的教育就是集中在做这种诊断上的相反我们软件工程师的教育是集中在过程设计和算法分析上的这些技能固然重要但是人们对调试过程的教育却很少关注相反我们得自己去拾起这种技能随着极端编程的出现和它对单元测试的注重这种做法已经开始改变了但是频繁的单元测试只是解决了问题的一部分一旦发现错误就必须诊断和纠正它们幸运的是很多错误都遵循我们可以识别的几种错误模式的其中一种一旦您可以识别出这些错误模式您就可以诊断出错误的原因并且更快地纠正它了
错误模式与反模式有关反模式是一次又一次被证明是失败的公共软件设计的模式虽然反模式是设计模式错误模式却是与编程错误相关的错误的程序行为的模式这与设计根本没有关系而是与编程和调试过程有关
通过示例学习
为了说明错误模式后面的思想让我们来考虑一种基本错误模式编程新手(经常还有更高级的程序员)常常会遇到这种错误模式在后面的文章中我们会谈到更高级的错误模式对每一种模式我会讨论将有助于把该模式的错误的发生控制到最少的编程原则(并非暗示所有的错误都是不遵循编程原则的结果不管我们遵循多少原则我们都会犯错误)
为了分类起见我会使用下面的形式(从医学上借用一些术语)来概括错误模式描述
模式名称
症状
起因
治疗方法和预防措施
Rogue Tile 模式
也许它是编程新手中最普遍的错误模式起因是复制和粘贴一段代码到程序的其它部分有时复制的一小部分因为功能上需求的略微不同而作了改动不可避免地错误在一个副本中被修正了而在另一个副本中没有被修正这样在错误症状复发时就会让您很头疼尽管大多数程序员很快就熟悉了这种错误模式但他们中很少人采取适当的措施来将这种错误的出现控制到最少您很容易就会偷懒不去思考而简单地复制您认为已经可以运行的代码但是工作效率由于修正代码而丧失这是因为不加选择的复制—粘贴操作很快降低了复制代码带来的任何工作效率
我称此为 Rogue Tile 模式是因为一段代码的各个副本可以被看成是分布在程序中的tile由于不同副本中的代码出现了差异副本就变成了rogue tile
症状
这种错误的模式的最普遍症状是在您认为已经修正了问题以后程序还继续表现出错误的行为
起因
为了理解这种情况发生的原因我们来看看下面的二元树类层次结构
public abstract class Tree {
}
public class Leaf extends Tree {
public Object value;
}
public class Branch extends Tree {
public Object value;
public Tree left;
public Tree right;
}
对于这些类要注意的第一件事就是两种具体类都包含 Object 类型的 value 字段如果您决定稍后让树包含比如说Interger您也许会忘记更新其中的一个字段声明如果程序的其它部分需要这些字段是 Interger 的话程序就很可能不会编译您或许记得您改变了其中一个类的 value 字段的类型却忽略了一个事实就是您没有在其它类中作相应的改变
一些预防措施
当然这个示例所示的错误是编程新手可以很快学会通过分解出公共代码来避免的在本例中字段声明应该移到 Tree 类中它的两个子类就会继承这个字段而且对字段声明的任何改变都只需要在一个地方出现
继续看这个示例我们可能还会编写在一个 Tree 中相加和相乘所有节点的方法为了简单起见我将以递归的方式来编写这些方法
// in class Tree:
public abstract int add();
public abstract int multiply();
// in class Branch:
public int add() {
return thisvalueintValue() + leftadd() + rightadd();
}
public int multiply() {
return thisvalueintValue() * leftmultiply() + rightmultiply();
}
// in class Leaf:
public int add() { return thisvalueintValue(); }
public int multiply() { return thisvalueintValue(); }
请注意我在 multiply 方法中为 Branch 类引入的错误我没有用第三项去乘而是加了它错误发生了因为我通过复制 add 方法中的代码并作轻微(但不完全)的改动创建了 multiply 方法这种错误非常隐蔽因为调用 multiply 方法永远不会发出错误信号事实上在很多情况下它会返回一个看上去完全合理的结果
就象以前一样我们可以通过分解出公共代码来将这种错误控制到最少在这种情况下我们可以编写一个单独的方法它在 Tree 上累计一个运算符(作为一个参数传送)我们可以使用一种被称为公共模式的设计模式(不是错误模式!)在对象中封装这个运算符
public abstract class Operator {
public abstract int apply(int l int r);
}
public class Adder extends Operator {
public int apply(int l int r) {
return l + r;
}
}
public class Multiplier extends Operator {
public int apply(int l int r) {
return l * r;
}
}
然后我们就可以如下面的代码所示在我们的 Tree 类层次结构中改变这个方法
// in class Tree:
public abstract int accumulate(Operator o);
public int add() {
return thisaccumulate(new Adder());
}
public int multiply() {
return thisaccumulate(new Multiplier());
}
// in class Leaf:
public int accumulate(Operator o) {
return valueintValue();
}
in class Branch:
public int accumulate(Operator o) {
return oapply(thisvalueintValue()
oapply(leftaccumulate(o)
rightaccumulate(o)));
}
通过分解出公共代码我们消除了在 add 和 multiply 方法正文中出现复制—粘贴错误的可能性另外请注意我们不再需要为 Tree 的每一个子类编写单独的 add 和 multiply 方法了
分解出公共代码是一个很好的习惯但它并不适用于所有的情况比如说Java 类型系统的简单性经常迫使我们在精确类型检验和保持对程序的每个不同的功能性元素的单点控制(请参阅参考资料阅读我写的关于 NextGen 的文章)之间作出选择正因为这个Rogue Tile 模式是所有开发人员必须一直努力以控制到最少的一种错误类型