导读 作为软件工程师不可避免会遇到的一个场景是:我们在改变或添加一个功能到不是我们创建的、我们不熟悉的、与我们负责的系统部分无关的代码中时,会遇到麻烦。

虽然这可能会是一个繁琐而艰巨的任务,但是由于使用其他开发人员编写的代码有很大的灵活性,所以我们可以从中得到大大的好处,包括增加我们的影响范围,修复软件腐烂以及学习我们以前不了解的系统部分(更何况,还可以学习其他程序员的技术和技巧)。

考虑到使用其他开发人员编写的代码既有其厌烦之处,又有其优势所在,所以我们必须小心不要犯一些严重的错误:

  • 我们的自我意识:我们可能会觉得自己知道得最多,但通常事实并非如此。我们要更改的是我们知之甚少的代码——我们不知道原作者的意图、导致此代码的决策以及原作者在写代码时可用的工具和框架,等等。谦逊的品质价值千金,你值得拥有。
  • 原作者的自我意识:我们即将接触的代码是由另一个开发人员所编写的,另一种风格、约束、期限和个人生活(消耗他或她工作之外的时间)。只有当我们开始质疑他或她做出的决定或质疑代码为什么这么不干净的时候,那人才会自我反省,不至于夜郎自大。我们应该尽一切努力让原作者帮助我们工作,而不是妨碍我们。
  • 对未知的恐惧:很多时候,我们将要接触的代码是我们知之甚少或完全一无所知的。令人害怕的是:我们将对我们所做的任何改变负责,但是我们基本上就像是在没有光线的黑暗屋子里走动一样。其实我们不需要担心,而是应该构建一种使我们能够在大小不一的改变中感到舒适的结构,并允许我们确保没有破坏现有的功能。

由于开发人员,包括我们自己,是人,所以在处理其他开发人员编写的代码时,处理好很多人的天性问题是很有用的。在这篇文章中,我们将通过我们可以使用的五种技术来确保将对人性的理解成为我们的优势,从现有代码和原作者汲取尽可能多的帮助,并使得其他开发人员编写的代码最后变得比原来更优秀。虽然这里列出的5个方法并不全面,但是使用下面的技术将确保在结束改动其他开发人员编写的代码时,我们有信心保持现有功能的工作状态,同时确保我们的新功能与现有的代码库协调一致。

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 {
    @Override
    public boolean test(Person person) {
        return person.getAge() < 30 && ((((person.getSalary() - (250 * 12)) - 1500) * 0.94) > 60000);
    }
}

我们对代码的意图以及为什么在代码中使用Magic number知道得并不多,但是我们可以创建一组测试,已知输入产生已知输出。例如,通过做一些简单的数学和解决构成成功的阈值薪水问题,我们发现如果一个人的年龄在30岁以下,且每年大概赚68,330美元,那么他被认为是成功的(按照本规范的标准)。虽然我们不知道那些magic number是什么,但是我们知道它们确实减少了初始的薪水值。因此,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.与编写代码的人交流

在涉及多个人的任何工作中,沟通至关重要。无论是企业,越野旅行还是软件项目,缺乏沟通是损害任务最有效的手段之一。即使我们在创建新代码时进行沟通,但是当我们接触现有的代码时,风险会增加。因为此时我们对现有的代码并不太了解,因此我们所了解的内容可能是被误导的,或只代表了其中的一小部分。为了真正了解现有的代码,我们需要和编写它的人交流。

当开始提出问题时,我们需要确定问题是具体的,并且旨在实现我们理解代码的目标。例如:

  • 这个代码片段最适合放到系统的哪里?
  • 你有什么设计或图表吗?
  • 我应该注意什么陷阱?
  • 这个组件或类是做什么的?
  • 有没有什么你想放到代码里,但当时没有做的?为什么?

始终要保持谦虚的态度,积极寻求原作者真正的答案。几乎每个开发人员都碰到过这样的场景,他或她看着别