雖然這可能會是一個繁瑣而艱鉅的任務,但是由於使用其他開發人員編寫的程式碼有很大的靈活性,所以我們可以從中得到大大的好處,包括增加我們的影響範圍,修復軟體腐爛以及學習我們以前不了解的系統部分(更何況,還可以學習其他程式設計師的技術和技巧)。
考慮到使用其他開發人員編寫的程式碼既有其厭煩之處,又有其優勢所在,所以我們必須小心不要犯一些嚴重的錯誤:
由於開發人員,包括我們自己,是人,所以在處理其他開發人員寫的程式碼時,處理好很多人的天性問題是很有用的。在這篇文章中,我們將透過我們可以使用的五種技術來確保將對人性的理解成為我們的優勢,從現有程式碼和原作者汲取盡可能多的幫助,並使得其他開發人員編寫的程式碼最後變得比原來更優秀。雖然這裡列出的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() 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 { @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.当你离开的时候,代码比你发现它的时候更好最后这个技术在概念上非常简单,但在实践中很困难:让代码比你发现它的时候更好。当我们梳理代码,特别是别人的代码时,我们大多会添加功能,测试它,然后前行,不关心我们会不会贡献软件腐烂,也不在乎我们添加到类的新方法会不会导致额外的混乱。因此,本文的全部内容可总结为以下规则:
每当我们修改代码时,请确保当你离开的时候,代码比你发现它的时候更好。
前面提到过,我们需要对类造成的损坏和对改变的代码负责,如果它不能工作,那么修复是我们的职责。为了战胜伴随软件生产而出现的熵,我们必须强制自己做到离开时的代码比我们发现它的时候更佳。为了不逃避这个问题,我们必须偿还技术债务,确保下一个接触代码的人不需要再付出代价。说不定,将来可能是我们自己感谢自己这个时候的坚持呢。
以上是如何處理前任程式設計師留下的程式碼的詳細內容。更多資訊請關注PHP中文網其他相關文章!