身为一个软件工程师,我们不可避免的会遇到这样一些问题:不得不修改别人的代码,或者在别人的代码中添加新的功能。我们并不熟悉这些代码,它也可能在整个系统中与我们编写的部分无关。虽然这样的工作很困难,容易让人感到无奈,但是要达到足够的灵活性来也别的开发者一起编写代码,收获也蛮大的。这些收获包括提高影响力,修复烂软件,还能学到系统中以前并不了解的部分(还可以从其它程序员那里学到技术和技巧)。
在其它开发者的代码中工作时,既会感到郁闷,又会从中有益,考虑到这些因素,我们必须警惕一些极其容易出错的地方:
-
我们的自我意识:我们可能会认为自己最有能耐,但通常都不是。我们对要改变的代码知之甚少,不了解原作者的意图,也不了解多少年有哪些因素导致这些代码形成,以及作者在编写这些代码的时候使用了什么样的工具和框架。谦卑价值万金,我们应该时刻保持这种心态。
-
原作者的自我意识:我们要接触的代码来自另一个开发者,他/她有自己的网络、约束、最后期限等,当然也有他/她自己的生活(这会占用一点工作时间)。他/她也是一个人,当我们质疑他/她做出的决定,或者质问为什么代码这么糟糕的时候,他/她会自然地产生防御性心理。我们应该努力让原作者与我们合作,而不是成为我们工作的阻碍。
-
恐惧未知事物:我们很多时候会接触到只了解一点点甚至完全不了解的代码。这似乎是件可怕的事情:我们得对自己做出的改动负责,但我们就像是在一个没有光亮的黑屋子里走来走去。我们不需要害怕,而是应该建立起一个框架,可以在里面安心地进行大大小小的修改,同时确保我们不会破坏现有的功能。
所有开发人员,包括我们自己,都是人。因此在别人编写的代码上工作,会受到人性的影响。在本文中,我们会讲述五种方法,利用人性的优点,从现有代码和原作者身上取得尽可能多的收获,并改善代码既有的状态。虽然这个清单并不全面,但应用这些方法将确保我们在完成对别人代码的修改工作后,会有信心保持现有功能的工作状态,同时又能保证新功能融合在现有代码中。
1. 确保有测试
对于别的开发人员写出来的功能,它确实如预期一样工作吗?我们所做的修改是否会妨碍它按照预期工作?对此,唯一能让人产生信心完成前述问题的方式就是,用测试来支持代码。我们在阅读别人的代码时,会发现两种可能的状态:(1) 没有达到足够水平的测试,或者 (2) 有达到足够水平的测试。对于前者,我们会陷入创建测试的困境;而对于后者,我们可以使用现有的测试来确保我们所做的修改不会破解原来的代码,同时也能从测试中大量地了解到代码的意图。
创建新测试
这听起来可能很惨:我们在更改另一个开发人员的代码时,要对我们的行为负责,但我们无法保证更改是否会造成破坏。吐槽是没有用的。不管我们发现代码是什么状态,只要动了代码,就得对其负责。因此,我们应该在修改代码的时候控制自己的行为。如果不想造成破坏,那就自己写测试。
这很枯燥,但我们可以通过编写测试来了解代码,这也是它的主要优点。假如现在的代码工作良好,我们需要编写测试,使其在获得预期输入的情况下产生预期的输出。在写测试的过程中,我们会逐渐了解代码的意图和功能。比如,存在如下代码
public class Person {
private int age;
private double salary;
public Person(int age, double salary) {
this.age = age;
this.salary = salary;
}
public void setAge(int age) {
this.age = age;
}
public int getAge() {
return age;
}
public void setSalary(double salary) {
this.salary = salary;
}
public double getSalary() {
return salary;
}
}
public class SuccessfulFilter implements Predicate<Person> {
@Override
public boolean test(Person person) {
return person.getAge() < 30 &&
((((person.getSalary() - (250 * 12)) - 1500) * 0.94) > 60000);
}
}
我们对其功能和代码中使用的魔法数字[译者注:指直接的数字常量]并不了解,但我们可以创建一组测试,根据已知的输入产生已知的输出。比如,通过简单的数学运算分析成功人士的薪资。我们发现如果 30 岁以下的人每年挣大约 $68,330,就会被认为是成功的(按代码中的标准)。虽然我们不知道那些魔法数字是什么意思,但我们知道它们会减少原始薪资。这样,$68,330 这个阈值是扣除前的基本薪资。使用这些信息,我们可以创建一些简单的测试,如下:
public class SuccessfulFilterTest {
private static final double THRESHOLD_NET_SALARY = 68330.0;
@Test
public void under30AndNettingThresholdEnsureSuccessful() {
Person person = new Person(29, THRESHOLD_NET_SALARY);
Assert.assertTrue(new SuccessfulFilter().test(person));
}
@Test
public void exactly30AndNettingThresholdEnsureUnsuccessful() {
Person person = new Person(30, THRESHOLD_NET_SALARY);
Assert.assertFalse(new SuccessfulFilter().test(person));
}
@Test
public void under30AndNettingLessThanThresholdEnsureSuccessful() {
Person person = new Person(29, THRESHOLD_NET_SALARY - 1);
Assert.assertFalse(new SuccessfulFilter().test(person));
}
}
通过这三个测试,我们已经对当前代码的工作方式了有大致了解:如果一个人不到 30 岁,每年能挣 $68,300,他就被认为是成功的。我们可以创建更多测试来确保功能在边缘情况(比如没有年龄或薪资)下的正确性。而且建成一套自动化测试之后,它可以用以确保我们对现有代码的修改不会破坏现有的功能。
使用现存测试
在现有代码中存在足够测试的情况下,我们也可以从测试中了解不少东西。就像我们创建测试一样,我们可以通过阅读测试从功能级别来了解代码是如何工作的。另外,我们也可以了解到原作者所理解的代码功能。就算测试不是原作者,而是其他人(在我们之前)写的,它仍然可以向我们提供其他人对代码意图的理解。
即使现在的测试很有帮助,我们仍然要保持谨慎。我们很难判断测试是否和代码的变化保持一致。如果一致,我们就拥有理解代码的坚实基础;如果不一致,我们就必须小心不要被误导。比如,如果原薪资阈值是每年 $75,000,后来改为我们知道的 $68,330,那么这个过时的测试可能会把我们引入歧途:
@Test
public void under30AndNettingThresholdEnsureSuccessful() {
Person person = new Person(29, 75000.0);
Assert.assertTrue(new SuccessfulFilter().test(person));
}
这个测试仍然会通过,但不是预期的效果。它能通过不是因为正确的阈值,而是因为它超过了阈值。如果这个测试集中包括一个测试用例,其薪资只比阈值少 $1 时返回 false,那么第二个测试会失败,这表示阈值是错误的。如果套件没有这样的测试,那么旧的数据很容易对我们了解代码的实际意图产生误导。当存在疑问的时候,请相信代码:正如我们前端所展示的,解决阈值的问题表明测试并未针对实际的阈值。
此外,参考代码库日志(比如 Git 日志)来了解代码和测试用例:如果最后更新代码的时间比最后更新测试的时间要新得多(并且代码中存在重大的代码,比如修改阈值),那么测试可能已经过时,需要谨慎对待。注意,不要完全忽略它们,因为它们还可能为我们提供一些原作者(或最近编写测试的开发者)的资料,不它们可能包含过时或错误的数据。
2. 和编写代码的人谈谈
在任何涉及多个人的工作中,沟通都至关重要。无论是在公司中、越野旅行中或是在项目中,缺少沟通都极易产生严重后果。尽管我们在创建新代码的时候进行沟通,但当我们接触既存代码时,风险还是会增加。因为我们对既存代码的了解有限,我们所了解的东西有可能受到了误导,也有可能过于片面,因此,为了真正理解现有的代码,我们需要与编写它的人交谈。
在问问题的时候,我们要确保问题是有针对性的,能达到我们理解代码的目的。比如:
-
这段代码对应于系统蓝图的哪个部分?
-
你有没有相关的设计方案或图表?
-
有我需要注意的坑吗?
-
某个组件或类是做什么用的?
-
有没有你本想写进代码,当时却没有写的东西?为什么?