我原本不想用“錯誤”二字。因為錯誤顯得太嚴重了,並且,軟體編碼本身就沒有對錯,只要你把功能實現了,剩下的就是思想流派的分歧。但這裡仍舊想用“錯誤”兩個字,因為本篇診斷所涉及的那些問題基本已經屬於當前軟體開發規則中的普適需要避免的。
注意,被診斷的學員並不是學的差的學員,相反,他有可能是學的很好的那一個,今天要診斷的劉同學,就是這樣的一位同學。劉同學來到最課程學習已經50天了,以下是他的學習記錄:
看到沒有,分數基本都在99分左右,然後到了大作業這裡……,80分。我當時跟劉同學是這麼說的:基本上,物件導向編碼過程中所犯的那些錯誤,你都犯了。當然,再次說明,劉同學到目前為止的學習都是非常棒的,不信你看他過去這幾周發的博文,我相信,已經畢業的同學中都沒有幾個能達到他這樣的理解力度:
《Java中的正規表示式之(?=X)和(?<=X)的講解和簡單應用(注:X在這裡是表示式)》
《關於Java多執行緒的執行緒同步和執行緒通訊的一些小問題(順便分享幾篇高質量的博文)》
《關於對Java中異常處理的try catch和throw的理解(淺顯理解)》
《安利一款強大的學習軟體XMind(順便放上這幾天製作的JavaSE的思維導圖day1-day4)》
現在,讓我們言歸正傳,看看劉同學在物件導向的作業(作業地址:最課程階段大作業02:實現自己的利息計算器)中,都犯了那些錯誤。
1. 獲取原始碼
我們的第一個作業,就要求學員自己搭建原始碼伺服器(SVN),所以第一件事情,既然我們要批改作業,那就得從原始碼伺服器上去簽出程式碼。過程如下:
首先,new->other,
其次,選擇project from svn,
緊接著,輸入SVN地址,以及使用者名稱和密碼,同時注意勾選Save authentication,
直接點選finish,
再次點選finish,
選擇簽出為Java Project,
輸入本地專案地址,注意:跟伺服器上保持一致,
以下就是我們順利簽出的專案。先欣賞一下程式碼吧,是不是第一感覺還不錯:
2. 原始碼分析
劉同學的原始碼,一共分為3個檔案,分別為:
InterestCalculator.java:利息計算主類,相關業務邏輯在此處;
InterestCalculatorTester.java:測試類,main函式入口;
Utility.java:工具類,主要用於處理鍵盤輸入;
這一眼看上去,很不錯,每個類各司其職。同時,執行結果也不錯,如下:
但是,我看到這份原始碼,首先第一個就去找判斷利息計算方式的程式碼,結果發現瞭如下的程式碼:
於是,我問劉同學的第一個問題就來:“假設我此刻需要增加一種利息計算方式,該怎麼辦?”。
劉同學的回答是:再增加一個case!
3. 違反開閉原則(Open Closed Principle,OCP)
咳咳,“再增加一個case”,恰恰就犯了第一個錯誤:違反開閉原則。
所謂“開閉原則”,就是“對擴充套件開放,對修改關閉”。這要怎麼講?初次接觸此概念的同學,很可能一臉懵逼,
二臉還是懵逼……
我們先來說:對修改關閉。什麼是對修改關閉?就是,對於利息計算來說,當一種計算方式被髮明出來後,它的演算法就再也沒有變過。所以,一種演算法對應一個Java類,那麼,演算法寫完成後,這個類就不應該需要再修改了。
那什麼是:對擴充套件開放?就是,你的程式當前支援3種利息計算方式,但隨著時代跟進,也許增加了一種計算方式,那程式碼必須得很方便的擴充套件。那什麼是方便的擴充套件呢?其中一種方式就是:增加一個子類。
所以,想明白沒有?當前的這個switch可以重構為一個抽象工廠,同時,演算法本身應該有繼承體系,大致如下:
package com.zuikc.intesters; import java.util.LinkedList; public abstract class InterestCalculator { // 本金 protected double principal; // 期限(月數) protected int numberOfPeriods; // 年利率 protected double interestRate; // 總利息 protected double totalInterests; // 本息合計 protected double totalToPay; // 用於儲存計算結果,即還款計劃 protected LinkedList<PayPlan> results = new LinkedList<>(); public InterestCalculator(double principal, int numberOfPeriods, double interestRate){ this.principal = principal; this.numberOfPeriods = numberOfPeriods; this.interestRate = interestRate; } // 獲取輸入並且計算 public void inputAndCalculate(){ getData(); calculate(); } // 利息計算,由子類去實現 abstract void calculate(); private void getData(){ // 獲取鍵盤輸入,這裡也可以提煉一個工具類; // 從鍵盤輸入的值儲存到本金,期限,年利率 } }
注意,三個演算法就是三個子類。
在上文中,還有一個PayPlan,它是一個實體類,裡面有一些屬性,用來表示每個月的還款資訊。
4. 違反單一職責原則(Single Responsibility Priciple,SRP)
在物件導向的開發中,我們還有另外一個原則,叫做:單一職責原則(Single Responsibility Priciple,SRP),簡單來說,就是一個類只完成一件事情。
而在劉同學的程式碼中,我們可以看到InterestCalculator幹了非常多的事情,
有負責獲取輸入的,有負責進行結果匯出的,有負責利息計算的(還包好了三種計算),甚至有對結果進行加密解密的(雖然部分實現是在Utility中)。可是這樣就會導致:一眼望去,就是妻妾太多,遲早要出事。
匯入匯出重構出來一個類;
加解密重構出來一個類;
負責利息計算的,更不要說了;
5. 單個方法行數太多
一種觀點是,單個方法不要超過30行。為什麼會有30行這個數字呢?那是因為早期的顯示器,如果你的程式碼超過30行,就超過一屏的顯示了,而我們閱讀程式碼,最好是一螢幕內顯示完畢。
如果一個方法在一屏內顯示不完,在大多數情況下,這意味著你需要將當前方法重構為兩個方法。
6. 不應將輸出固定格式
我們在作業之中要求結果是輸出到控制檯的,於是劉同學將整個結果儲存到了一個字串中,如下:
那麼問題又來了:假設我們現在要輸出為HTML格式怎麼辦?
我們會發現,整個計算的過程必須全部修改。
正確的做法是,我們將計算結果儲存到物件或者物件列表中,就像上文我重構的InterestCalculator一樣。這樣做的一個好處是,當我們需要將結果以不同的格式進行輸出的話,我們只需要提取物件的屬性就可以。
7. 其它問題
其它問題不多了。如果其它問題過多的話,我估計劉同學要鬱悶的。但是,不過,當然,確實還存在另一個比較嚴重的問題:對稱加密。
本次作業,幾乎所有的同學都自己實現了一個對稱加密的演算法。但這是有問題的。由於這個議題相對來說比較獨立,所以,讓我們在下一篇中詳細指出。
8. 提交修改
我們在劉同學的程式碼上進行了一些修改,讓我們進行簽入吧。
寫上comment,
好了,診斷結束,讓我們給出劉同學的本次作業的會診單吧。
最課程學員會診單
華麗分割線
===========================================================
最課程JavaEE+網際網路分散式新技術開班進行中,來http://www.zuikc.com看看吧。你想參加不一樣的培訓班,並且一畢業就NB,那就來加入我們吧;
更多技術文章和開班資訊請加入,
QQ群: