Home >System Tutorial >LINUX >How to deal with code left behind by a previous programmer
While this can be a tedious and daunting task, due to the flexibility of using code written by other developers, we can gain significant benefits from it, including increasing our reach and fixing software rot. As well as learning parts of the system that we didn't know before (not to mention, learning the techniques and techniques of other programmers).
Given that using code written by other developers has its annoyances as well as its advantages, we must be careful not to make some serious mistakes:
Since developers, ourselves included, are human, it is useful to address many of the human nature issues when working with code written by other developers. In this article, we'll go through five techniques we can use to ensure that understanding human nature is used to our advantage, draw as much help as possible from existing code and original authors, and enable code written by other developers In the end, it becomes better than before. While the 5 methods listed here are not comprehensive, using the techniques below will ensure that when we end up changing code written by other developers, we are confident that we will keep existing features working while ensuring that our new features are compatible with existing ones. The code base is harmonized.
1. Ensure the existence of testsThe only real source of confidence is ensuring that existing functionality that exists in code written by other developers actually works as expected, and that any changes we make to it will not affect the functionality. The only solution is to support the code with tests. When we encounter code written by another developer, the code can be in two states: (1) there is not enough testing level, or (2) there is enough testing level. In the former case, we are responsible for creating the tests, while in the latter case we can use existing tests to ensure that any changes we make don't break the code and get as much out of the tests as possible Understand the intent of the code.
Create new testThis is a sad example: when we change other developers' code, we are responsible for the results of the change, but we have no way to guarantee that we don't break anything when we make the change. There is no use complaining. No matter what condition we find the code in, we have to touch the code, so if the code breaks, it is our responsibility. So when we change the code, we must control our own behavior. The only way to be sure you won't break the code is to write the tests yourself.
Although this is tedious, it allows us to learn by writing tests, which is its main advantage. Assume that the code now works correctly, and we need to write tests so that expected inputs lead to expected outputs. As we work through this test, we gradually learn about the intent and functionality of the code. For example, given the following code
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() 60000); } }
We don’t know much about the intent of the code and why magic numbers are used in the code, but we can create a set of tests with known inputs that produce known outputs. For example, by doing some simple math and solving the threshold salary problem that constitutes success, we find that a person is considered successful (by the standards of this specification) if he is under the age of 30 and earns approximately $68,330 per year. . Although we don't know what those magic numbers are, we do know that they do reduce the initial salary value. Therefore, the $68,330 threshold is base salary before deductions. By using this information we can create some simple tests such as:
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创立的康威定律规定:
设计系统的任何组织…都将不可避免地产生一种设计,该设计结构反映了组织的通信结构。
This means that a large, tightly communicated team may produce integrated, tightly coupled code, but some smaller teams may produce more independent, loosely coupled code (more on this correlation , see "Demystifying Conway's Law"). For us, this means that our communication structure affects not just specific pieces of code, but the entire codebase. Therefore, close communication with the original author is definitely a good way, but we should check ourselves not to rely too much on the original author. Not only is this likely to annoy the original author, it may also create unintentional coupling in our code.
While this helps us delve deeper into the code, this assumes access to the original author. In many cases, the original author may have left the company, or happens to be away from the company (e.g. on vacation). What should we do in this situation? Ask anyone who might know something about the code. This person does not have to have actually worked on the code; he could have been around when the original author wrote the code, or he could know the original author. Even just a few words from people around the original developer may enlighten other unknown code snippets.
3. Remove all warningsThere is a well-known concept in psychology called the "broken windows theory," which is described in detail by Andrew Hunt and Dave Thomas in "The Pragmatic Programmer" (pages 4-6). This theory was originally proposed by James Q. Wilson and George L. Kelling and is described as follows:
Suppose there is a building with several broken windows. If windows are not repaired, vandals will tend to break more windows. Eventually, they may even break into the building, occupy it illegally, or start a fire inside if the building is unoccupied. Consider the condition of sidewalks as well. If trash accumulates on the road, more trash will accumulate soon. Eventually, people will even start throwing takeout trash there and even breaking cars.
This theory states that if no one seems to care about the item or thing, then we will ignore the care of the item or thing, which is human nature. For example, if a building already looks untidy, it's more likely to be vandalized. In terms of software, this theory means that if a developer discovers that the code is a mess, it is human nature to break it. Essentially, what we are thinking (even if the mental activity is not so rich) is, "If the last person doesn't care about this code, why should I care?" or "It's just messed up code, who knows who wrote it."
However, this should not be our excuse. Whenever we touch code that previously belonged to someone else, we are responsible for that code and suffer the consequences if it doesn't work effectively. To defeat this human behavior, we need to take small steps to avoid our code getting dirty less often (replacing broken windows promptly).
A simple way is to remove all warnings from the entire package or module we are using. As for unused or commented code, delete it. If we need this part of the code later, in the repository we can always retrieve it from a previous commit. If there are warnings that cannot be resolved directly (such as primitive type warnings), then annotate the call or method with the @SuppressWarnings annotation. This ensures that we've thought carefully about our code: they're not warnings due to oversight, but rather warnings that we explicitly noticed (like primitive types).
Once we remove or explicitly suppress all warnings, then we must ensure that the code remains free of warnings. This has two main effects:
This has a psychological implication for other people, as well as ourselves - that we actually care about the code we are working on. It's no longer a one-way street - we force ourselves to change the code, commit it, and never look back. Instead, we recognize that we need to take responsibility for the code. This is also helpful for future software development - it shows future developers that this is not a warehouse with broken windows: it is a well-maintained code base.
4.ReconstructionOver the past few decades, refactoring has become a very important term, and has recently been used as a synonym for making any changes to currently working code. While refactoring does involve changes to the code you're currently working on, it's not the whole big picture. Martin Fowler, in his important book on the topic, "Refactoring", defines refactoring as:
Make changes to the internal structure of software to make it easier to understand and cheaper to modify, without changing its observable behavior.
这个定义的关键在于它涉及的更改不会改变系统可观察的行为。这意味着当我们重构代码时,我们必须要有方法来确保代码的外部可见行为不会改变。在我们的例子中,这意味着是在我们继承或自己开发的测试套件中。为了确保我们没有改变系统的外部行为,每当我们进行改变时,都必须重新编译和执行我们的全部测试。
此外,并不是我们所做的每一个改变都被认为是重构。例如,重命名方法以更好地反映其预期用途是重构,但添加新功能不是。为了看到重构的好处,我们将重构SuccessfulFilter。执行的第一个重构是提取方法,以更好地封装个人净工资的逻辑:
public class SuccessfulFilter implements Predicate { @Override public boolean test(Person person) { return person.getAge() 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 { @Override public boolean test(Person person) { return person.getAge() 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 { 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_SALARY; } }
重新编译和测试,发现系统仍然按照预期的方式工作:我们没有改变外部行为,但是我们改进了代码的可靠性和内部结构。有关更复杂的重构和重构过程,请参阅Martin Fowler的Refactoring Guru网站。
5.当你离开的时候,代码比你发现它的时候更好最后这个技术在概念上非常简单,但在实践中很困难:让代码比你发现它的时候更好。当我们梳理代码,特别是别人的代码时,我们大多会添加功能,测试它,然后前行,不关心我们会不会贡献软件腐烂,也不在乎我们添加到类的新方法会不会导致额外的混乱。因此,本文的全部内容可总结为以下规则:
每当我们修改代码时,请确保当你离开的时候,代码比你发现它的时候更好。
前面提到过,我们需要对类造成的损坏和对改变的代码负责,如果它不能工作,那么修复是我们的职责。为了战胜伴随软件生产而出现的熵,我们必须强制自己做到离开时的代码比我们发现它的时候更佳。为了不逃避这个问题,我们必须偿还技术债务,确保下一个接触代码的人不需要再付出代价。说不定,将来可能是我们自己感谢自己这个时候的坚持呢。
The above is the detailed content of How to deal with code left behind by a previous programmer. For more information, please follow other related articles on the PHP Chinese website!