寫在前面的話
寫代碼的時間也不算太多,但是我覺得對於一個程序而言,除了時刻保持好的學習能力之外,還要對於代碼的編寫由好的習慣,養成好的代碼編寫習慣,可以提高代碼的開發效率
下面講一下好的代碼的基本素養
對於命名:
1、有意義的命名
包名:按照域名全部使用小寫
類名:首字母大寫
方法名:采用駝峰命名法則
變量名:采用駝峰命名法則
常量:字符大寫加下划線方式命名
對於名稱要求:
1、名稱代表實際意義
2、名稱不能隨便命名,名稱意義與實際操作比匹配
對於方法名應當是動詞或動詞的短語
3、在系統的開發中,對同一概念用同一個詞:
例如:在一堆代碼中,有controller,manager,還有driver,就會令人困惑,DeviceManager和Protocol_Controller之間有何根本的區別?為什么不全用controller或manager?他們都是Drivers嗎?這種名稱,讓人覺得這兩個對象是不同的類型,也分屬不同的類。
4、別用雙關語
避免將同一單詞用於不同的目的。同一術語用於不同的概念,基本上就是雙關語了。如果遵守“一詞一義”規則,可能在好多個類里面都會有add方法。只要這些add方法的參數列表和返回值在語義上等價,就一切順利。
5、添加有意義的語境
設想你的名稱firstName,lastName,street,houseNumber,city,state和ZipCode的變量。可以通過添加前綴的方式提供語境,例如addrFirstName,addrLastName,addrState等等
但不要添加沒有用的語境:
設若有一個名為“加油站豪華版”(Gas Station Deluxe)的應用,在其中給每個類添加GSD前綴就不是什么好的點子。
只要短名稱足夠清楚,就要比長長名稱好
函數
1、短小
常說函數不該長於一屏。
函數的代碼塊和縮進
if語句,else語句,while語句等,其中的代碼塊應該只有一行。該行大抵應該是一個函數的調用語句。這樣不但能保持函數短小,而且,因為塊內調用的函數擁有較具說明的名稱,從而增加了文檔上的價值。
同時在一個方法內部,不應該大到足以容納嵌套結構。所以,函數的縮進層級不該多於一層或兩層。
在函數中,不應該編寫足夠多的嵌套語句,這樣的代碼會增加的代碼的閱讀和修改成本。
2、方法只做一件事
函數應該做一件事,並且做好這件事,只做這一件事。
3、每個函數一個抽象層級
這了抽象層級一般是指if語句,else 語句,或者while、for語句中的嵌套迭代的層級,按照前文的講述,一般對於這種程序控制流語句,一般不宜有太多的嵌套,保持的最多兩層為佳,如果多於三層,這需要考慮重構出一個新的方法。
我們想要讓代碼擁有自頂詳細的閱讀順序。我們想要讓每個函數后面都跟着位於下一個抽象層級的函數,這樣一來,在查看函數列表時,就能循抽象層級向下閱讀了。
4、switch語句
寫出短小的switch語句很難,即便只有兩種條件的switch語句也要比我想要的單個代碼或函數大的多。
public Money calculatePay(Employee e) throws InvalidEmployeedType{ switch (e.type){ case COMMISSIONED: return calculateCommissionedPay(e); case HOURLY: return calculateHourlyPay(e); case SALARIED:return calculateSalariedPay(e); default: throw new InvalidEmployeeType(e.type); } }
該函數有好幾個問題,首先,它太長,當出現新的雇員類型時,還會更長。其次,它明顯做了不止一件事情。第三,它違反了單一權責原則,因為有好幾個修改它的原因。第四,它違反了開放閉合的原則。
不過,該函數最麻煩的可能是到處皆有類似的結構函數。例如,可能會有
isPayday(Employee e,Date date)
或
deliverDay(Employee e,Money pay)
該問題的解決方案如下:
通過構建工廠方式來解決問題
1 public abstract class Employee{ 2 public abstract boolean isPayday(); 3 public abstract Money calculatePay(); 4 public abstract void deliverPay(Money pay); 5 } 6 _______________________________________________________ 7 public interface EmployeeFactory{ 8 public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType; 9 } 10 _______________________________________________________ 11 public class EmployeeFactory implements EmployeeFactory{ 12 public Employee makeEmployee(EmployeeRecord r)throws InvalidEmployeeType{ 13 switch(r.type){ 14 case COMMISSIONED:return new CommissionedEmployee();
case HOURLY: return new HourlyEmployee();
case SALARIED: return new SalariedEmployee();
default:
throw new InvalidEmployeeType(e.type);
15 }
16 }
17 }
5、對於函數名稱,需要使用描述性的名稱
別害怕吃的長名字。長而具有描述性的名稱,要比短而令人費解的名稱好。長而具有描述性的名稱,要比描述性的注釋好。
6、函數的參數
最理想的參數數量是零(零參數函數),其次是一(單參數函數),再次是二(雙參數函數),應盡量避免三(三參數函數)
像函數傳遞單個參數有兩種極普遍的理由
1、操作該參數,將其轉換為其他什么東西,在輸出之
2、事件。在這種形式中,有輸入參數而無輸出參數。程序將函數看作是一個事件,使用該參數修改系統狀態。
在實際的編程中,盡量避免編寫不遵循這些形式的一元函數。如果函數要對輸入參數參數進行轉換操作,轉換的結果就該體現為返回值。對於轉換,使用輸出參數而非返回值令人迷惑。如果函數要對輸入參數進行轉換操作,轉換的結果就該體現為返回值。這樣體現了函數的完整性。
對於參數,應該盡量避免傳遞boolean值。這樣做,方法簽名立刻變得復雜起來,大聲宣布函數不止做一件事。如果標識為true將這樣做,標識為false,則那樣做。
實際應該將函數且分為兩個函數。
對於標識參數還有一種做法及時將標識參數傳遞給類的字段,然后派生類繼承這個字段,並實現這個抽象類的方法,這樣方式達到一根方法只做一件事
對於函數的參數的格式盡量不要超過三個,超過三個,一般考慮將參數封裝成對象。
對於函數的可變函數可能是一元,二元甚至是三元的。超過這個數量就可能要犯錯誤了。
void monad(Integer...args);
void dyad(String name,Integer...args);
void triad(String name,int count,Integer...args);
3、對於方法名稱的取名,需要取一個動詞加名詞的方式
7、無副作用
副作用是一種謊言。函數承諾只做一件事,但還是會做其他的被藏起來的事。
例如:
1 public class UserValidator{ 2 private Cryptographer cryptographer; 3 public boolean checkPassword(String userName,String password){ 4 if(user!=User.NULL){ 5 String coderPhrase=user.getPhraseEncoderByPassword(); 6 String phrase=cryptographer.decrypt(codePhrase,password); 7 if("Valid Password".equals(phrase)){ 8 ---------------------------------------- 9 Session.initialze(); 10 ---------------------------------------- 11 return true; 12 } 13 } 14 return false; 15 } 16 }
輸出參數:
當有函數調用時: 你一定會被數用作輸出而非輸入的參數迷惑過。對於函數的調用,例如:
appendFooter(s);
要明白這個函數的意義,你一定會花時間去看函數的簽名。
public void appendFooter(StringBuffer report)
這時候你才 清楚函數是做什么的,但是付出了檢查函數聲明的代價。你被迫檢查函數簽名,就得花上一點時間,應該避免這種中斷思路的事。
在面向對象語言的輸出參數的大部分需求已經消失了,因為this也有輸出函數的意味在內。換言之,最好的調用如下:
report.appendFooter();
普遍而言,應避免使用輸出參數,如果函數必須要修改某種狀態,就修改所屬對象的狀態。
8、分隔指令與詢問
其實函數的主要功能要么做什么事,要么回答什么事,但二者不能兼得
看例子:
public boolean set(String attributes,String values);
該函數設置某個指定屬性,如果成功就返回true,如果不存在那個屬性則返回false,這樣就導致以下的語句:
if(set("username”,"unclebob")....
其實站在讀者的角度,這是什么意思呢?它是在為username屬性值是否之前已設置為unclebob嗎?或者它是在為username屬性值是否成功設置為unclebob呢?從這行調用很難判斷其含義,因為set是動詞還是形容詞並不清楚。
解決辦法是把指令和詢問分隔開來,防止發生混淆:
if(attributeExists("username"){
setAttribute("username","unclebob");
...
}
9、使用異常替代返回錯誤碼
從指令式函數返回錯誤碼輕微違反了指令與詢問分隔的規則。它鼓勵了在if語句判斷中把指令當做表達是使用
if(deletePage(page)==E_OK)
這不會引起動詞/形容詞混淆,但卻導致更深層次的嵌套結構
if(deletePage(page)==E_OK) {
if(registry.deleteReference(page.name)==E_OK){
if(configKeys.deleteKey(page.name.makeKey())==E_OK){
logger.log("page deleted")
另一方面,如果使用異常替代返回錯誤碼,錯誤處理代碼就能從主路徑代碼中分離出來,得到簡化:
try{
deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKey());
}catch(Exception e){
logger.log(e.getMessage());
}
}
抽離try/catch代碼塊
Try/Catch代碼塊丑陋不堪,它們搞亂了代碼結構,把錯誤處理與正常流程混為一談,最好把try和catch代碼塊的主體部分抽離出來,另外形成函數
例子:
public void delete(Page page){
try{
deletePageAndAllReferences(page):
}catch(Exception e){
logError(e):
}
}
private void deletePageAndAllReference(Page page) throws Exception{
deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKey());
}
private void logError(Exception e){
logger.log(e.getMessage());
}
錯誤處理就是一件事
函數應該只做一件事,錯誤處理就是一件事。因此,處理錯誤的函數不該做其他的事,這意味着,如果關鍵字try在某個函數中存在,它就該是這個函數的第一個單詞,而且在catch/finally代碼塊后面也不該有其他的內容。
Error.java依賴磁鐵
在實際的編寫代碼中,返回錯誤碼通常暗示某處有個類或是枚舉,如下:
public enum Error{
OK,
INVALID,
NO_SUCH,
LOCKED,
OUT_OF_RESOURCES,
WAITING_FOR_EVENT;
}
隨着錯誤的增加,還有錯誤類型的修改,你會不停的維護這樣一份錯誤編碼,並且Error枚舉修改時,所有這些其他的類都需要重新編譯和部署。
使用異常替代錯誤碼,新異常就可以從異常類派生出來,無需重新編譯或重新部署。
10、別重復自己
在編程實際中,很多原則與實踐規則都是為控制與消除重復而創建。
11、結構化編程
結構化編程認為,每個函數,函數中的每個代碼都應該只有一個入口,一個出口。遵循這些規則,意味着每個函數中只該有一個return語句,循環中不能有break或contiue語句,
但是在實際編程中,只要函數保持短小,偶爾出現return,break或continue語句沒有什么壞處,甚至比單入單出原則更具有表達力
在實際的編寫中,也是一開始都代碼冗長而復雜,有太多的縮進和嵌套循環,有過長的參數列表,名稱隨意取。然后打磨這些代碼,分解函數,修改名稱,消除重復。縮短和重新安置方法。
注釋
其實在實際的編寫程序中,注釋必不可少,但是好的注釋也很有必要
注釋一般包括:
1、法律信息
下面試我們在FitNesse項目每個源文件開頭放置的標准注釋。我們可以很開心的說,IDE自動卷起這些注釋,這樣就不會顯得凌亂了
//Copyright (C) 2003,2004,2005 by Object Mentor,Inc.All right reserved. //Released under the terms of the GNU General Public License versoin 2 or Later
2、提供信息注釋
有時,用注釋來提供基本信息也有有用處。例如,以下注釋解釋了某個抽象方法的返回值:
//returns an instance of the responder being tested protected abstract Responder responderInstance();
這類注釋有時管用,但更好的方式是盡量利用函數名稱傳達信息。比如,在本例中,只要把函數重新命名為responderBeingTested,注釋就是多余的了。
// format matched kk:mm:ss EEE MM dd .yyy pattern timeMathcher=patern.complie("\\d*:\\d*:\\d \\w*,\\w* \\d*,\\d*");
3、對意圖的解釋
有時,注釋不僅提供了有關實現的有用信息,而且還提供了某個決定后面的意圖 通俗的來講就是通過注釋,告訴程序員某段代碼的意圖
4、闡釋
有時,注釋把某些晦澀難懂的參數或返回值的意義翻譯為某種可讀的形式,也是有用的
public void testCompareTo() throws Exception { WikiPagePath a=PathParser.parse("PageA"); WikiPagePath ab=PathParser.parse("PageA.PageB"); WikiPagePath b=PathParser.parse("PageB"); WikiPagePath aa=PathParser.parse("PageA.PageA"); WikiPagePath bb=PathParser.parse("PageB.PageB"); WikiPagePath ba=PathParser.parse("PageB.PageA"); assertTrue(a.compareTo(a)==0) //a==a assertTrue(a.compareTo(b)!=0) //a!=a assertTrue(ab.compareTo(ab)==0) //ab==ab assertTrue(aa.compareTo(ab)==0) //aa==ab }
5、警示
有時,用於警告其他程序員會出現某種后果的注釋也是有用的。
//Don't run unless you have some time to kill public void _testWithReallyBigFile(){ writeLinessToFile(1000000); response.setBody(testFile); response.readyToSend(this); String responseString=output.toString(); }
6、 TODO注釋
有時,有理由用//TODO形式在源代碼中放置要做的工作列表。
//TODO-MdM these are not needed //We except this to go away when we do the checkout model protected VersionInfo makeVersion() throws Exception{ return null; }
7、放大
注釋可以用來放大某種看來不合理之物的重要性
String listItemContent =match.group(3).trim(); //the trim is real important,It removes the starting //spaces that could cause the item to be recongnized //as anoter list new ListItemWidget(this,listItemContent,this.level+1); return buildList(text.substring(match.end()));
8、公共API中的doc
沒有什么比被良好的描述的公共API更有用和令人滿意的了,標准的Java庫中的Javadoc就是一例。
如果你在編寫公共的API,就該為它編寫良好的javadoc。
格式
概念間垂直方向上的區隔
在封包聲明,導入聲明和每個函數之間,都有空白行隔開。
垂直方向上的靠近
緊密相關的代碼應該互相靠近
垂直距離
關系密切的概念應該互相靠近。
變量聲明盡可能靠近其使用的位置。
偶爾,在較長的函數中,變量也可能在某個代碼塊的頂部,或在循環之前聲明。
實體變量應該在類的聲明的頂部。
相關函數。若某個函數地調用了另外一個函數,就應該把它們放到一起,而且調用者應該盡可能放在被調用者上面。
概念相關。概念相關的代碼應該放到一起。相關性越強,彼此之間的距離就該越短。
橫向格式
一般一行代碼保持在80-120個字符之間,且無需拖動滾動條。
如果一條語句太長可以考慮換行,原則是看代碼是無需拖動滾動條。
至於代碼的格式,采用IDEA或者eclipse默認的格式即可
對象和數據結構
數據、對象的反對稱性
過程式的形狀的代碼
public class Square{ public Point topLeft; public double side; } public class Rectangle{ public Point toLeft; public double height; public double width; } public class Circle{ public Point center; public double radius; } public class Geometry{ public final double PI=3.141592653589793; public double area(Object shape) thros NosuchShapeException{ if(shape instanceof Square){ Square s=(Square) shape; return s.side*s.side; } else if(shape instanceof Rectangle){ Rectangle r=(Rectangle) shape; return r.height*r.width; } else if(shape instanceof Circle){ Circle c=(Circle) shape; return PI*c.radius*c.radius; } throw new NoSuchShapeExceptino(); } }
多態式的形狀
public interface Shape { double area(); } public class Square implements Shape { private Point topLeft; private double side; public double area() { return side * side; } } public class Rectangle implements Shape { private Point topLeft; private double height; private double width; public double area() { return height * width; } } public class Circle implements Shape { private Point center; private double radius; public final double PI = 3.141592653589793; public double area() { return PI * radius * radius; } }
上述代碼說明:
過程式的代碼(使用數據結構代碼)便於在不改動現有的數據結構的前提下,添加新的數據結構。面向對象的代碼便於在不改動現有函數的前提下添加新的類。
反過來講是:
過程式代碼難以添加新數據結構,因為必須修改所有函數。面向對象代碼難以添加新的函數,因為必須修改所有類。
得墨忒耳律
模塊不應了解它所操作對象的內部情況。如上節所見,對象隱藏數據,暴露操作。這意味着對象不應通過存取器暴露其內部結構,因為這樣更像是暴露而非隱藏其內部結構。
更准確的說,得墨忒耳認為,類C的方法f只應該地調用以下的對象的方法:
- C(包括靜態方法和非靜態方法)
- 由f創建對象
- 作為參數傳遞給f的對象
- 由C的實體變量持有的對象
方法不應調用由任何函數返回的對象的方法。換言之,只跟朋友談話,不與陌生人談話。
以下方法的調用違反了得墨忒耳定律,因為它調用了getOption()返回值的getScratchDir()函數,又調用了getScratchDir()返回值的getAbsolutePath()方法。
數據傳輸對象
最為精練的數據結構,是一個只有公共變量,沒有函數的類。這種數據有時被稱為數據傳送對象,或DTO(data Transfer Objects)。在應用程序代碼里一系列將原始數據轉換為數據庫的,它們往往是排頭兵。
public class Address{ private String street; private String streetExtra; private String city; private String state; private String zip; public Address(String street,String streetExtra,String city,String state,String zip){ this.street=street; this.streetExtra=streetExtra; this.city=city; this.state=state; this.zip=zip; } public String getStreet() { return street; } public void setStreet(String street) { this.street = street; } public String getStreetExtra() { return streetExtra; } public void setStreetExtra(String streetExtra) { this.streetExtra = streetExtra; } public String getCity() { return city; } public void setCity(String city) { this.city = city; } public String getState() { return state; } public void setState(String state) { this.state = state; } public String getZip() { return zip; } public void setZip(String zip) { this.zip = zip; } }
其本質上而言,是MVC架構,將DAO層操作Java Bean對象
錯誤的處理
在實際代碼實現中try...catch的創建要和實際的業務代碼分開。將try...catch和實際的業務代碼綁定在一起會搞亂了代碼邏輯。
1、在實際的開發中,使用異常而不是標記非異常值標記碼。
2、先寫入try...catch代碼塊
在你調用函數中,最小范圍內的,使用try...catch代碼塊,而不是在增大try...catch代碼塊,將一大堆業務代碼框起來,這樣不便於排錯
3、使用不可控異常
可控異常代價就是違反開放/閉合原則。如果你在方法中拋出可控異常,而catch語句在三個層級之上,你就得在Catch語句和拋出異常處之間的每個方法簽名中聲明該異常。這意味着對軟件中較低層的修改,都將波及較高層的簽名。修改好的模塊必須重新構建、發布,即便它們自身所關注的任何東西都不每改動過。
4、給出異常發生的環境說明
在拋出異常地方應該說明,異常產生的位置包名,方法名,產生異常的變量及變量的值等。這樣方便后續錯誤的排查
5、依調用者需要定義異常類
ACMEPort port=new ACMEPort(12); try{ port.open(); }catch(DeviceResponseException e){ logger.warn("Device response exception",e); reportPortError(e); } catch(ATM1212UnlockedException e){ reportPortError(e); logger.log("Unlock exception",e) }catch(GMXError e){ reportPortError(e); logger.log("Device response exception"); }finally{ ... }
通過重構為:
LocalPort port=new LocalPort(12); try{ port.open(); }catch(PortDeviceFailure e){ reportError(e); logger.log(e.getMessage(),e) }finally{ ... } public class LocalPort{ private ACMEPort innerPort; public LocalPort(int portNumber){ innerPort=new ACMEPort(portNumber); } public void open(){ try{ innerPort.open(); }catch(DeviceResponseException e){ throw new PortDeviceFailure(e); } catch(ATM1212UnlockedException e){ throw new PortDeviceFailure(e); }catch(GMXError e){ throw new PortDeviceFailure(e); } } }
通過封裝一個自定義的異常來使上級目錄在方法聲明throws不在添加更多的異常的拋出。這樣達到一定的程度的解耦。
6、定義常規流程
例子
try{ MealExpenses expenses=expenseReportDAO.getMeals(employee.getTD()); m_total+=expense.getTotal(); }catch(MealExpenseNotFound e){ m_total+=getMealPerDiem(); }
業務邏輯是,如果消耗了餐食,則計入總額中,如果沒有消耗,則員工得到當日餐食補貼。異常打斷了業務邏輯。如果不去處理特殊情況會不會好一些?那樣的話代碼會看起來會更簡潔。就像這樣:
MealExpense expense=expenseReportDAO.getMeals(employee.getID());
m_total+=expense.getTotal();
能把代碼寫的那樣簡潔嗎?能,可以修改一下ExpenseReportDAO,使其總是返回MealExpense對象。如果沒有餐食消耗,就返回一個餐食補貼的MealExpense對象。
public class PerDiemMealExpenses implements MealExpenses{ public int getTotal(){ } }
這種手法叫做特例模式,創建一個類或者配置一個對象,用來處理特例。
7.別返回null值
如果打算在方法中返回null值,不如拋出異常,或是返回特例對象(例如一個長度為空的列表)
8.別傳遞null值
在大多數編程語言中,沒有良好的方法能對付由調用者意外傳入的null值。事已至此,恰當的做法就是禁止傳入null值。這樣,你在編碼的時候,就會時時記住參數列表中的null值意味着出問題了,從而大量避免這種無心之災。
邊界
邊界主要是自己系統中對於別人庫的調用關系,在邊界中要盡可能的消除邊界的兼容性,比如不使用泛型,還有可以使用adapter模式將我們的接口轉換為第三方提供的接口。
另外,在實際的開發當中,要學會測試第三方的接口,熟悉第三方的API。
單元測試
類
類的組織
類應該從一組變量列表開始,如果有公共靜態變量,應該先出現,然后是私有靜態變量,以及私有實體變量,很好會有公共變量。
公共函數應跟在變量列表之后,我們喜歡把由某個公共函數調用的私有工具函數緊隨在該公共函數后面。
有時我們也需要用到受保護的(protected)變量或工具函數,好讓測試可以訪問到。
類應該短小
關於類的第一個規則是類應該短小,第二條規則是還要更短小。
單一權責原則
單一權責原則認為,類或模塊應有且只有一條加以修改的理由“添加新的功能“,很多既定的操作在類編寫測試之后就基本確定。
從職責單一的角度而言,一個類只負責一個單一的職責,與該職責相關的修改成為修改這個類的理由,與該職責無關的修改,不成為類修改的理由。
再強調一下,系統應該由許多短小的類而不是少量巨大的類組成。每個小類封裝一個權責,只有一個修改的原因,並與少數其他類一起協同達成期望的系統行為。
內聚
如果一個類中的每個變量都被每個每個方法所使用,則該類具有最大的內聚性。
內聚性高,意味着類中的方法和變量互相依賴,互相結合成一個邏輯整體。
保持內聚性就會得到許多短小的類
為了修改而組織
我們希望將系統打造成在添加或修改特性時盡可能少惹麻煩的架子。在理想的系統中,我們通過拓展系統而非修改現有代碼來添加新特性。
隔離修改
依賴於具體細節的客戶類,當細節改變時,就會有風險。我們可以借助接口和抽象類來隔離這些細節帶來的影響。(意思就是將不變的代碼組成一類,將變化到的部分以接口和抽象的類的方式,提供出去,如果發生變化,則可以在變化的類中繼承或者實現這個接口,來重新實現具體細節,達到減少修改的目的,這是基於一個現實,修改原有的代碼的成本,比新開發一個具體實現的成本高,為了降低新的成本,所有選擇上面的方式)
通過降低連接度,這樣的類就遵循了另一條設計原則。依賴倒置原則。本質而言,DIP認為類應當依賴於抽象而不是依賴於具體細節。