如何处理前任程序员留下的代码

作为软件工程师不可避免会遇到的一个场景是:我们在改变或添加一个功能到不是我们创建的、我们不熟悉的、与我们负责的系统部分无关的代码中时,会遇到麻烦。虽然这可能会是一个繁琐而艰巨的任务,但是由于使用其他开发人员编写的代码有很大的灵活性,所以我们可以从中得到大大的好处,包括增加我们的影响范围,修复软件腐烂以及学习我们以前不了解的系统部分(更何况,还可以学习其他程序员的技术和技巧)。

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

我们的自我意识:我们可能会觉得自己知道得最多,但通常事实并非如此。我们要更改的是我们知之甚少的代码——我们不知道原作者的意图、导致此代码的决策以及原作者在写代码时可用的工具和框架,等等。谦逊的品质价值千金,你值得拥有。

原作者的自我意识:我们即将接触的代码是由另一个开发人员所编写的,另一种风格、约束、期限和个人生活(消耗他或她工作之外的时间)。只有当我们开始质疑他或她做出的决定或质疑代码为什么这么不干净的时候,那人才会自我反省,不至于夜郎自大。我们应该尽一切努力让原作者帮助我们工作,而不是妨碍我们。

对未知的恐惧:很多时候,我们将要接触的代码是我们知之甚少或完全一无所知的。令人害怕的是:我们将对我们所做的任何改变负责,但是我们基本上就像是在没有光线的黑暗屋子里走动一样。其实我们不需要担心,而是应该构建一种使我们能够在大小不一的改变中感到舒适的结构,并允许我们确保没有破坏现有的功能。

由于开发人员,包括我们自己,是人,所以在处理其他开发人员编写的代码时,处理好很多人的天性问题是很有用的。在这篇文章中,我们将通过我们可以使用的五种技术来确保将对人性的理解成为我们的优势,从现有代码和原作者汲取尽可能多的帮助,并使得其他开发人员编写的代码最后变得比原来更优秀。虽然这里列出的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<Person> {

@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.与编写代码的人交流

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

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

这个代码片段最适合放到系统的哪里?

你有什么设计或图表吗?

我应该注意什么陷阱?

这个组件或类是做什么的?

有没有什么你想放到代码里,但当时没有做的?为什么?

始终要保持谦虚的态度,积极寻求原作者真正的答案。几乎每个开发人员都碰到过这样的场景,他或她看着别人的代码,自问自答:“为什么他/她要这样做?为什么他们不这样做?”然后花几个小时来得出本来只要原作者回答就能得到的结论。大多数开发人员都是有才华的程序员,所以即使如果我们遇到一个看似糟糕的决定,也有可能有一个很好的理由(可能没有,但研究别人的代码时最好假设他们这样做是有原因的;如果真的没有,我们可以通过重构来改变)。

沟通在软件开发中起次要副作用。1967年最初由Melvin Conway创立的康威定律规定:

设计系统的任何组织…都将不可避免地产生一种设计,该设计结构反映了组织的通信结构。

这意味着,一个庞大、紧密沟通的团队可能会生成一体化,紧密耦合的代码,但一些较小的团队可能会生成更独立、松散耦合的代码(有关此相关性的更多信息,请参阅《Demystifying Conway’s Law》)。对于我们来说,这意味着我们的通信结构不仅影响特定的代码段,也影响整个代码库。因此,与原作者密切沟通绝对是一个好办法,但我们应该自检不要太过于依赖于原作者。这不仅可能会惹恼原作者,还可能在我们的代码中产生无意识的耦合。

虽然这有助于我们深入研究代码,但这是在假设可以接触原作者的情况下。在很多时候,原作者可能已经离开了公司,或恰巧不在公司(例如正在休假)。在此种情况下我们该做什么?询问可能对代码有所了解的人。这个人不一定要曾真正工作于代码,他可以是在原作者编写代码时就在周围,也可以是认识原作者。哪怕仅是从原开发者周围的人中得到只言片语,也可能会启迪其他未知的代码片段。

3.删除所有警告

心理学中有一个众所周知的概念,称为“破窗理论”,Andrew Hunt和Dave Thomas在《 The Pragmatic Programmer 》(第4-6页)中详细描述了这个概念。这个理论最初是由James Q.Wilson和George L. Kelling提出的,描述如下:

假设有一个建筑物有几扇破了的窗户。如果窗户没有修好,那么破坏者会趋向于打破更多的窗户。最终,他们甚至可能会破门而入,如果建筑物是没人住的,那么他们可能会非法占有或者在里面点火。也可以考虑人行道的情况。如果道路上面有垃圾堆积,那么不久之后,就会有更多的垃圾累积。最终,人们甚至会开始往那里扔外卖垃圾,甚至打破汽车。

这个理论指出,如果似乎已经没人关心这个物品或事物,那么我们就会忽视对物品或事物的照顾,这是人的天性。例如,如果一栋建筑物看上去已经凌乱不堪,那么它更有可能被肆意破坏。在软件方面,这个理论意味着如果开发人员发现代码已经是一团糟,那么人的本性会让他弄坏代码。从本质上说,我们心里想的是(即使心理活动没有这么丰富),“既然最后一个人不在乎这代码,我为什么要在乎?”或“都是乱糟糟的代码,谁知道是谁写的。”

但是,这不应该成为我们的借口。只要我们接触以前属于其他人的代码,那么我们就要对这些代码负责,并且如果它不能有效工作的话,我们得担负后果。为了战胜这种人的天性行为,我们需要采取一些小措施以避免我们的代码更少地被弄脏(及时更换破掉的窗户)。

一个简单方法是删除来自我们正在使用的整个包或模块中的所有警告。至于未使用或添加注释的代码,删除它。如果我们稍后需要这部分代码,那么在存储库中,我们总是可以从先前的提交中检索它。如果存在无法直接解决的警告(例如原始类型警告),那么使用@SuppressWarnings注解注释该调用或方法。这样可以确保我们对代码进行过仔细的考虑:它们不是因为疏忽而发出的警告,而是我们明确地注意到了警告(如原始类型)。

一旦我们删除或明确地禁止所有警告,那么我们就必须确保代码保持免除警告。这有两个主要作用:

迫使我们仔细考虑我们创建的任何代码。

减少代码腐败的变化,现在的警告会导致以后的错误。

这对其他人,以及我们自己都有心理暗示作用——我们其实关心我们正在处理的代码。它不再是条单行线——我们强逼着自己更改代码,提交,然后永不回头。相反,我们认识到我们需要对这代码负责。这对之后的软件开发也是有帮助的——它向将来的开发人员展示,这不是一间窗户都破了的仓库:而是一个维护良好的代码库。

4.重构

在过去几十年中,重构已经成为了一个非常重要的术语,并且最近被当作是对当前工作代码做任何改变的代名词。虽然重构确实涉及对当前正在工作的代码的更改,但并非整个大局。Martin Fowler在他关于这个话题的重要着作——《Refactoring》一书中将重构定义为:

对软件的内部结构进行更改,使其更容易理解并且修改起来更便宜,而不改变其可观察的行为。

这个定义的关键在于它涉及的更改不会改变系统可观察的行为。这意味着当我们重构代码时,我们必须要有方法来确保代码的外部可见行为不会改变。在我们的例子中,这意味着是在我们继承或自己开发的测试套件中。为了确保我们没有改变系统的外部行为,每当我们进行改变时,都必须重新编译和执行我们的全部测试。

此外,并不是我们所做的每一个改变都被认为是重构。例如,重命名方法以更好地反映其预期用途是重构,但添加新功能不是。为了看到重构的好处,我们将重构SuccessfulFilter。执行的第一个重构是提取方法,以更好地封装个人净工资的逻辑:

public class SuccessfulFilter implements Predicate<Person> {

@Override

public boolean test(Person person) {

return person.getAge() < 30 && getNetSalary(person) > 60000;

}

private double getNetSalary(Person person) {

return (((person.getSalary() - (250 * 12)) - 1500) * 0.94);

}

}

在我们进行这种改变之后,我们重新编译并运行我们的测试套件,测试套件将继续通过。现在更容易看出,成功是通过一个人的年龄和净薪酬定义的,但是getNetSalary方法似乎并不像Person类一样属于SuccessfulFilter(指示标志就是该方法的唯一参数是Person,该方法的唯一调用是Person类的方法,因此对Person类有很强的亲和力)。 为了更好地定位这个方法,我们执行一个Move方法将其移动到Person类:

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 double getNetSalary() {

return ((getSalary() - (250 * 12)) - 1500) * 0.94;

}

}

public class SuccessfulFilter implements Predicate<Person> {

@Override

public boolean test(Person person) {

return person.getAge() < 30 && person.getNetSalary() > 60000;

}

}

为了进一步清理此代码,我们对每个magic number执行符号常量替换magic number行为。为了知道这些值的含义,我们可能得和原作者交流,或者向具有足够领域知识的人请教,以引领正确的方向。我们还将执行更多的提取方法重构,以确保现有的方法尽可能简单。

public class Person {

private static final int MONTHLY_BONUS = 250;

private static final int YEARLY_BONUS = MONTHLY_BONUS * 12;

private static final int YEARLY_BENEFITS_DEDUCTIONS = 1500;

private static final double YEARLY_401K_CONTRIBUTION_PERCENT = 0.06;

private static final double YEARLY_401K_CONTRIBUTION_MUTLIPLIER = 1 - YEARLY_401K_CONTRIBUTION_PERCENT;

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 double getNetSalary() {

return getPostDeductionSalary();

}

private double getPostDeductionSalary() {

return getPostBenefitsSalary() * YEARLY_401K_CONTRIBUTION_MUTLIPLIER;

}

private double getPostBenefitsSalary() {

return getSalary() - YEARLY_BONUS - YEARLY_BENEFITS_DEDUCTIONS;

}

}

public class SuccessfulFilter implements Predicate<Person> {

private static final int THRESHOLD_AGE = 30;

private static final double THRESHOLD_SALARY = 60000.0;

@Override

public boolean test(Person person) {

return person.getAge() < THRESHOLD_AGE && person.getNetSalary() > THRESHOLD_SALARY;

}

}

重新编译和测试,发现系统仍然按照预期的方式工作:我们没有改变外部行为,但是我们改进了代码的可靠性和内部结构。有关更复杂的重构和重构过程,请参阅Martin Fowler的Refactoring Guru网站。

5.当你离开的时候,代码比你发现它的时候更好

最后这个技术在概念上非常简单,但在实践中很困难:让代码比你发现它的时候更好。当我们梳理代码,特别是别人的代码时,我们大多会添加功能,测试它,然后前行,不关心我们会不会贡献软件腐烂,也不在乎我们添加到类的新方法会不会导致额外的混乱。因此,本文的全部内容可总结为以下规则:

每当我们修改代码时,请确保当你离开的时候,代码比你发现它的时候更好。

前面提到过,我们需要对类造成的损坏和对改变的代码负责,如果它不能工作,那么修复是我们的职责。为了战胜伴随软件生产而出现的熵,我们必须强制自己做到离开时的代码比我们发现它的时候更佳。为了不逃避这个问题,我们必须偿还技术债务,确保下一个接触代码的人不需要再付出代价。说不定,将来可能是我们自己感谢自己这个时候的坚持呢。

http://www.kmjdad.com/
http://www.jnsjzyy.com/
http://www.czhkwl.com/
http://www.express-o2o.com/
http://www.gzjindao.com/
http://www.chumingchuanmeiyishu.com/
http://www.thcxb.com/
http://www.xingguangkeji.com/
http://www.gdrhsy.com/
http://www.clhuiji.com/
http://www.nxjianye.com/
http://www.tjmingsheng.com/
http://www.gangguan022.com/
http://www.zyjbp.com/
http://www.qianhangmy.com/
http://www.tzminbell.com/
http://www.wfhuamei.com/

时间: 2024-10-11 19:18:09

如何处理前任程序员留下的代码的相关文章

分针网——每日分享:10个程序员常用的代码简写技术

更多文章:www.f-z.cn 今天小编我给大家整理了一份10个程序员常用的代码简写技术,看懂一种是入门,全懂就是大神,你能知道几个呢? 1.三元操作符 当想写if...else语句时,使用三元操作符来代替. const x = 20;let answer;if (x > 10) { 简写: const answer = x > 10 ? 'is greater' : 'is lesser'; 也可以嵌套if语句: const big = x > 10 ? " greater

当程序员说“这代码写的可真烂”,他们的意思是“这烂代码不是我写的”。而当他们说这段代码有些“小问题”时,很可能这代码是他们自己写的

英文原文:What Programmers Say vs. What They Mean 你是否听到过同事说“这段代码不言自明”?你的同事的这句话的实际意思是这段代码不需要写注释. 你也许注意到了,很多时候,程序员所说的话的字面意思和其真实的意思是完全不同的.不用惊异,下面你将很快知道这些暧昧的短语和其深层次的意思都是什么. 最近 Imgur 上出现了一张图片,里面列举的程序员的一些专业术语和其含义,它能很好的帮助你理解这些话的真实意思.这里是对其中的精华进行的总结. 典型的程序员之间的对话 当

汇道科技:如果以后程序员写不动代码了怎么办?

最近汇道科技办公室关于"程序员写不动代码了怎么办?"引发了很多人的讨论,一开始讨论的对象只是"当事人"程序员们,后面到各行各业,同时大家讨论的几个点也引人深思: 1.35岁写不动代码了怎么办?  你不得不承认,对于新事物的兴趣在下降,就如同不再有见漂亮姑娘时的小兔乱撞,就如同不再有见到梦想时的热血跌宕.就是如此尴尬的一个年龄,偏偏又生在互联网,这个到处都是常青藤生产线的艺术品,不比资历只比朝气的行业. 首先小编认为35岁并不是一个很可怕的年纪,三十而立,三十五岁正当

【健康生活】讨论程序员一天代码量

讨论程序员一天代码量. 虽然可能有一些符号.注释等一些比较占行. 那么一名合格的程序员,一天代码量应该在多少行. 200? 500? 1000? 欢迎大家来讨论. [健康生活]讨论程序员一天代码量

程序员写的代码bug超过十个bug就辞退?

某大型软件公司-- 领导:现在咱们研发部门有好几个项目组了,有上百名开发,需要评定大家的绩效. HR:对,我觉得公司天天最闲散的就是他们,天天迟到,来了就对着电脑敲啊敲,别人不懂技术也不知道他们做的怎么样. 领导:就是就是,你看咱们就没有一个项目没延期过,要加强绩效考核! HR:从今天开始,我们以大家的代码行数为评定绩效的标准. 程序员们:代码行数,这好办啊,咱多来点换行,空行.能写成两行的绝不写成一行. 过了一个月-- 领导:研发部门上个月的绩效怎么样? HR:他们上个月绩效都很好,平均行数上

互联网假大牛横飞:程序员最烂代码排行榜!!!

读完此榜,希望不要影响到大家伙的代码观! -- 从今天起,我的代码信条: 1.没有最烂,只有更烂! 2.只要能跑起来的代码,就是好代码! 第十名:巧妙避开起名的烦恼 <p class = 'c1'></p> <p class = 'c2'></p> <p class = 'c3'></p> ... *** ... <p class = 'c99'></p> <p class = 'c100'><

当程序员写完代码后,看完感同身受……

如果你是程序员,相信看完后,你会默默点个! 从此循环,估计可能到头了-- 微信关注"蜗牛新视界",带你玩不同.  

程序员除了写代码还可以做点啥

最近除了写代码,想搞点其他事情,不想生活除了写代码还是写代码,担心死的快,因为个人喜欢看电影,所以就想建个电影站,心动不如行动. 马上去买了个香港的免备案空间,并注册了个域名,下个了电影网站的cms,苹果cms,然后就上线了,发现自带的模板比较丑,就下个了模板,比较高端大气的!自己点了几下,发现模板有一些问题,花了点时间去修复了下模板,然后去采集了资源,发现如果全部采集,资源太多了,怕扛不住,就采集了部分,只想更新最新的一些资源! 上线后问题就来了,过了一两天发现百度都没收录,更可气的时好像会被

Java程序员看C++代码

从事了几年的android应用开发,这几年一直都是用Java开发,今年找了一家OEM公司,说白了就是Android系统源码定制,在这家公司学习了Python, C++.因为接触JNI会比较多一点,所以下面想和大家谈谈我看C++代码的一些理解. 我主要是从看代码和维护代码的角度来看,不涉及更深的技术层面.也欢迎大家来一起讨论下各自的理解. 首先我个人非常喜欢C++的头文件.C++的头文件类似于Java1.8的接口,可以在这里面定义好所有的接口方法/虚函数, 我觉得好的头文件应该是数据集+行为规范集