序
丹麥諺語:小處誠實非小事。
建筑師路德維希:神在細(xì)節(jié)之中。
日本的 5S 哲學(xué):
整理 (整理、せいり)
組織,分類,排序,如恰當(dāng)?shù)孛?/p>整頓?。ㄕD、せいとん)
整齊,系統(tǒng)化。
美國諺語:物皆有其位,物盡歸其位(A place for everything, and everything in its place)。
- 清楚 (清楚、せいそ)
整潔;清秀,秀麗。
諺語:整潔近乎虔誠(Cleanliness is next to godliness)。
清潔(清潔、せいけつ)
干凈;純潔,純正。身美(しつけ)
綱紀(jì);紀(jì)律;自律,在實踐中貫徹規(guī)程并樂于改進(jìn)。
諺語:守小節(jié)者不虧大節(jié)(He who is faithful in little is faithful in much)。
諺語:及時一針省九針(A stitch in time saves nine)。
諺語:日事日畢(Don't put off until tomorrow what you can do today)。
諺語:防病好過治?。ˋn ounce of prevention is worth a pound of cure)。
第1章 整潔代碼
1.2 糟糕的代碼
20世紀(jì)80年代末,有家公司寫了個流行的殺手應(yīng)用。后來該軟件的發(fā)布周期越來越長,缺陷總是不能修復(fù),崩潰幾率越來越大。
20年后一位雇員說:當(dāng)時他們趕著推出產(chǎn)品,代碼寫得亂七八糟。特性越加越多,代碼也越來越爛,最后再也沒法管理這些代碼了。
糟糕的代碼毀掉了這家公司。
我們都曾瞟一眼自己親手造成的混亂,并決定棄之不顧、走向明天。
我們都曾看到自己的爛程序居然能運行,然后斷言這總比什么都沒有要強(qiáng)。
我們都曾說過有朝一日再回頭清理。
勒布朗(LeBlanc)法則:稍后等于永不(Later equals never)。
1.3 混亂的代價
隨著混亂的增加,團(tuán)隊生產(chǎn)力也持續(xù)下降,趨向于零。當(dāng)生產(chǎn)力下降時,管理層就只有一件事可做了:增加更多人手到項目中,期望提升生產(chǎn)力??墒切氯瞬⒉皇煜は到y(tǒng)的設(shè)計。他們不清楚什么樣的修改符合設(shè)計意圖,什么樣的修改違背設(shè)計意圖。團(tuán)隊中的每個人都背負(fù)著提升生產(chǎn)力的可怕壓力,他們制造更多的混亂,驅(qū)動生產(chǎn)力向零端不斷下降。
1.3.1 華麗新設(shè)計
開發(fā)團(tuán)隊難以忍受舊系統(tǒng)的混亂,要求重新設(shè)計一套看上去很美的新系統(tǒng)。
在新系統(tǒng)完成的時候,這個故事會重演。
1.3.2 態(tài)度
因進(jìn)度和需求的壓力而對代碼質(zhì)量做出妥協(xié),這不是專業(yè)程序員應(yīng)有的態(tài)度。
1.3.4 整潔代碼的藝術(shù)
整潔代碼很像是繪畫。能分辨一幅畫是好是壞并不表示懂得繪畫。能分辨整潔代碼和骯臟代碼也不意味著會寫整潔代碼!
1.3.5 什么是整潔代碼
Bjarne Stroustrup(C++語言發(fā)明者):
我喜歡優(yōu)雅和高效的代碼。代碼邏輯應(yīng)當(dāng)直截了當(dāng),叫缺陷難以隱藏;盡量減少依賴關(guān)系,使之便于維護(hù);依據(jù)某種分層戰(zhàn)略完善錯誤處理代碼;性能調(diào)至最優(yōu),省得引誘別人做沒規(guī)矩的優(yōu)化,搞出一堆混亂來。整潔的代碼只做好一件事。
Grady Booch(Object Oriented Analysis and Design with Applications 作者)
整潔的代碼簡單直接。整潔的代碼如同優(yōu)美的散文。整潔的代碼從不隱藏設(shè)計者的意圖,充滿了干凈利落的抽象和直截了當(dāng)?shù)目刂普Z句。
1.4 思想流派
任何門派都并非絕對正確。
1.5 我們是作者
作者有責(zé)任與讀者做良好溝通。要想輕松寫代碼,需先讓代碼易讀。
1.6 童子軍軍規(guī)
童子軍軍規(guī):讓營地比你來時更干凈。
光把代碼寫好是不夠的,必須時時保持代碼整潔。
1.8 小結(jié)
藝術(shù)書并不保證你讀過之后能成為藝術(shù)家,只能告訴你其他藝術(shù)家用過的工具、技術(shù)和思維過程。
第2章 有意義的命名
2.2 名副其實
一旦發(fā)現(xiàn)有更好的名稱,就換掉舊的。
如果名稱需要注釋來補(bǔ)充,那就不算是名副其實。
糟糕的命名-例1
int d; // 消逝的時間,以日計
有意義的命名-示例1
int elapsedTimeInDays;
int daysSinceCreation;
int daysSinceModification;
int fileAgeInDays;
糟糕的命名-示例2
public List<int[]> getThem(){
List<int[]> list1 = new ArrayList<int[]>();
for (int[] x : theList)
if (x[0] == 4)
list1.add(x);
return list1;
}
有意義的命名-示例2
public List<Cell> getFlaggedCells(){
List<Cell> flaggedCells = new ArrayList<Cell>();
for (Cell cell : gameBoard)
if (cell.isFlagged())
flaggedCells.add(cell);
return flaggedCells ;
}
2.3 避免誤導(dǎo)
避免與類型名稱或專有名稱混淆導(dǎo)致誤導(dǎo)。
避免使用易混淆的字母與數(shù)字,如字母 l 與數(shù)字 1,字母 O 與數(shù)字 0。
2.4 做有意義的區(qū)分
避免使用廢話做無意義的區(qū)分。
無意義的區(qū)分-示例1
public static void copyChars(char a1[], char a2[]){
for (int i = 0; i < a1.length; i++){
a2[i] = a1[i];
}
}
有意義的區(qū)分-示例1
public static void copyChars(char source[], char destination[]){
for (int i = 0; i < source.length; i++){
destination[i] = source[i];
}
}
無意義的區(qū)分-示例2
class Prodect { }
class ProdectInfo { }
class ProdectData { }
無意義的區(qū)分-示例3
getActiveAccount();
getActiveAccounts();
getActiveAccountInfo();
2.5 使用讀得出來的名稱
讀不出來的名稱示例
class DtaRcrd102{
private Date genymdhms;
private Date modymdhms;
private final String pszqint = "102";
}
讀得出來的名稱示例
class Customer{
private Date generationTimestamp;
private Date modificationTimestamp;
private final String recordId = "102";
}
2.6 使用可搜索的名稱
名稱長短應(yīng)與其作用域大小相對應(yīng)。
糟糕代碼示例
for (int j = 0; j < 34; j++){
s += (t[j] * 4) / 5;
}
整潔代碼示例
int realDaysPerIdealDay = 4;
const int WORK_DAYS_PER_WEEK = 5;
int sum = 0;
for (int j = 0; j < NUMBER_OF_TASKS; j++){
int realTaskDays = taskEstimate[j] * realDaysPerIdealDay;
int realTaskWeeks = realTaskDays / WORK_DAYS_PER_WEEK;
sum += realTaskWeeks;
}
2.7 避免使用編碼
匈牙利語標(biāo)記法,Hungarian Notation,HN
匈牙利語標(biāo)記法在現(xiàn)代編譯器面前已無必要。
消除成員前綴或后綴。
小鐳:
作者不喜歡在接口和實現(xiàn)中使用編碼,如 IShapeFactory。但接口使用 I 前綴卻是 .NET 的接口命名準(zhǔn)則。
關(guān)于編碼,應(yīng)參照本原則并遵循特定語言的規(guī)范與約定風(fēng)格。
擴(kuò)展閱讀:Brad Abrams: Why do interface names begin with “I”
2.8 避免思維映射
不應(yīng)當(dāng)讓讀者在腦中把你的名稱翻譯為他們熟知的名稱。這常常出現(xiàn)在選擇是使用問題領(lǐng)域術(shù)語,還是解決方案領(lǐng)域術(shù)語。
2.9 類名
類名和對象名應(yīng)該是名詞或名詞短語。
2.10 方法名
方法名應(yīng)當(dāng)是動詞或動詞短語。
2.11 別扮可愛
不要使用特定文化的俗語。言到意到,意到言到。
2.12 每個概念對應(yīng)一個詞
給每個抽象概念選一個詞,并一以貫之。
思考:
- fetch、retrieve 和 get 的不同。
- DeviceManager 和 ProtocolController 的不同。
2.13 別用雙關(guān)語
同一術(shù)語用于不同概念,基本上就是雙關(guān)語了。
應(yīng)盡可能寫出易于閱讀的代碼,大眾化的平裝書模式好過晦澀的學(xué)院派模式。
思考 add、insert 和 append 的不同。
2.14 使用解決方案領(lǐng)域名稱
你的代碼的讀者是程序員,所以應(yīng)盡可能使用他們所知術(shù)語。
2.15 使用源自所涉問題領(lǐng)域的名稱
優(yōu)秀的程序員和設(shè)計師,其工作之一就是分離解決方案領(lǐng)域和問題領(lǐng)域的概念。
與所涉問題領(lǐng)域更為貼近的代碼,應(yīng)當(dāng)采用源自問題領(lǐng)域的名稱。
2.16 添加有意義的語境
名稱很難自我說明。你需要擁有良好命名的類、函數(shù)或名稱空間來放置名稱,給讀者提供語境。
代碼清單2-1,語境不明確的變量
private void printGuessStatistics(char candidate, int count){
String number;
String verb;
String pluralModifier;
if (count == 0){
number = "no";
verb = "are";
pluralModifier = "s";
} else if (count == 1){
number = "1";
verb = "is";
pluralModifier = "";
} else {
number = Integer.toString(count);
verb = "are";
pluralModifier = "s";
}
String guessMessage = String.format(
"There %s %s %s%s", verb, number, candidate, pluralModifier
);
print(guessMessage);
}
代碼清單2-2,有語境的變量
public class GuessStatisticsMessage{
private String number;
private String verb;
private String pluralModifier;
public String make(char candidate, int count){
createPluralDependentMessageParts(count);
return String.format(
"There %s %s %s%s", verb, number, candidate, pluralModifier
);
}
private void createPluralDependentMessageParts(int count){
if(count == 0){
thereAreNoLetters();
} else if (count == 1){
thereIsOneLetter();
} else {
thereAreManyLetters(count);
}
}
private void thereAreManyLetters(int count){
number = Integer.toString(count);
verb = "are";
pluralModifier = "s";
}
private void thereIsOneLetter(int count){
number = "1";
verb = "is";
pluralModifier = "";
}
private void thereAreNoLetters(int count){
number = "no";
verb = "are";
pluralModifier = "s";
}
}
2.17 不要添加沒用的語境
只要短名稱足夠清楚,就要比長名稱好。
2.18 最后的話
取好名字最難的地方在于需要良好的描述技巧和共有文化背景。
第3章 函數(shù)
3.1 短小
函數(shù)的第一條規(guī)則是短小,第二條規(guī)則是還要更短小。
3.2 只做一件事
函數(shù)應(yīng)該只做一件事并做好它。
3.3 每個函數(shù)一個抽象層級
如果函數(shù)只做了該函數(shù)名下同一抽象層上的步驟,則函數(shù)只做了一件事。
向下規(guī)則:我們想要讓代碼擁有自頂向下的閱讀順序。
3.4 switch 語句
單一權(quán)責(zé)原則,SRP,Single Responsibility Principle
開放閉合原則,OCP,Open/Closed Principle
3.5 使用描述性的名稱
長而具有描述性的名稱要比短而令人費解的名稱好,要比描述性的長注釋好。
選擇描述性的名稱能清理你關(guān)于模塊的設(shè)計思路,并幫你改進(jìn)它。追索好名稱,往往導(dǎo)致對代碼的改善重構(gòu)。
3.6 函數(shù)參數(shù)
最理想的參數(shù)數(shù)量是零,其次是一,再次是二,盡量避免三。
參數(shù)不易對付,它們帶有太多概念性。參數(shù)與函數(shù)名處在不同的抽象層級,他要求你了解目前并不特別重要的細(xì)節(jié)。
從測試的角度看,參數(shù)越多越麻煩。
輸出參數(shù)比輸入?yún)?shù)還要難以理解。因為我們習(xí)慣性地認(rèn)為信息通過參數(shù)輸入函數(shù),通過返回值從函數(shù)中輸出。
3.6.1 一元函數(shù)的普遍形式
兩種普遍理由:
- 問關(guān)于該參數(shù)的問題。
boolean fileExists("MyFile")
- 操作該參數(shù),將其轉(zhuǎn)化并輸出。
InputStream fileOpen("MyFile")
3.6.2 標(biāo)識參數(shù)
向函數(shù)傳入布爾值簡直就是駭人聽聞的做法。這樣做就等于宣布函數(shù)不只做一件事。
render(boolean isSuite)
應(yīng)將該函數(shù)一分為二:
renderForSuite()
renderForSingleTest()
3.6.3 二元函數(shù)
如非必須使用二元函數(shù),就應(yīng)該盡量利用一些機(jī)制將其轉(zhuǎn)換成一元函數(shù)。
writeField(outputStream, name)
可以通過重構(gòu)將 outputStream 做成類的一個
writeField(name)
3.6.4 三元函數(shù)
三元函數(shù)要比二元函數(shù)難懂得多。
3.6.5 參數(shù)對象
如果函數(shù)看來需要兩個、三個或三個以上參數(shù),就說明其中一些參數(shù)應(yīng)該封裝為類了。
3.6.6 參數(shù)列表
類似于 string.format 中的可變參數(shù)實則是二元函數(shù)。
3.6.7 動詞與關(guān)鍵詞
函數(shù)和參數(shù)應(yīng)當(dāng)形成一種非常良好的動詞/名詞對形式。例如 WriteField(name)。
3.7 無副作用
函數(shù)承諾只做一件事,但還是會做被藏起來的事。有時它會對自己類中的變量做出未能預(yù)期的改動,有時它會把變量搞成向函數(shù)傳遞的參數(shù)或是系統(tǒng)全局變量。
輸出參數(shù)
應(yīng)避免使用輸出參數(shù)。如果函數(shù)必須要修改某種狀態(tài),就修改所屬對象的狀態(tài)。
3.8 分隔指令與詢問
函數(shù)要么做什么事,要么回答什么事,兩樣都干常會導(dǎo)致混亂。
3.9 使用異常替代返回錯誤碼
返回錯誤碼會導(dǎo)致更深層次的嵌套結(jié)構(gòu),并要求調(diào)用者立刻處理錯誤。
使用異常替代返回錯誤碼,錯誤處理代碼就能從主路徑代碼中分離出來。
3.9.1 抽離 Try/Catch 代碼塊
最好把 Try/Catch 代碼塊抽離出來形成另外的函數(shù),這樣可避免把錯誤處理與正常流程混為一談,不致搞亂代碼結(jié)構(gòu)。
3.9.2 錯誤處理就是一件事
錯誤處理的函數(shù)不該做其他事。
3.9.3 錯誤碼依賴磁鐵
錯誤碼通常暗示某處有個類或是枚舉定義了所有錯誤碼,這個類或枚舉就是依賴磁鐵。其他許多類都得導(dǎo)入和使用它。當(dāng)它修改時,其他類就得重新編譯和部署。
3.10 別重復(fù)自己
重復(fù)可能是軟件中一切邪惡的根源。許多原則與實踐規(guī)則都是為控制和消除重復(fù)而創(chuàng)建。
3.11 結(jié)構(gòu)化編程
Edsger Dijkstra 的結(jié)構(gòu)化編程原則。
只要函數(shù)保持短小,偶爾出現(xiàn)的 return、break、continue 語句沒有壞處,甚至比單入單出原則更具有表達(dá)力。
3.12 如何寫出這樣的函數(shù)
先動手寫,再打磨推敲,同時配合單元測試。
3.13 小結(jié)
編程藝術(shù)是且一直就是語言設(shè)計的藝術(shù)。
大師級程序員把系統(tǒng)當(dāng)做故事來講,而不是當(dāng)做程序來寫。那種領(lǐng)域特定語言的一個部分,就是描述在系統(tǒng)中發(fā)生的各種行為的函數(shù)層級。
第4章 注釋
別給糟糕的代碼加注釋——重新寫吧。Kernighan and Plaugher, The Elements of Programming Style
若編程語言足夠有表達(dá)力或我們善于用這些語言表達(dá)意圖,那么就完全沒必要使用注釋。
注釋的恰當(dāng)用法是彌補(bǔ)我們在用代碼表達(dá)意圖時遭遇的失敗——不用注釋就表達(dá)不清楚。
注釋難能被程序員堅持維護(hù),唯有代碼能忠實地告訴你它做的事。
4.1 注釋不能美化糟糕的代碼
與其花時間寫清楚注釋,不如花時間清潔代碼。
4.2 用代碼來闡述
代碼對比:
// Check to see if the employee is eligible for full benefits
if ((employee.flags & HOURLY_FLAG) &&
(employee.age > 65))
與
if (employee.isEligibleForFullBenefits())
4.3 好注釋
4.3.1 法律信息
盡可能指向一份標(biāo)準(zhǔn)許可或其他外部文檔。
// Copyright (C) 2003,2004,2005 by Object Mentor, Inc. All rights reserved.
// Released under the terms of the GNU General Public License version 2 or later.
4.3.2 提供信息的注釋
應(yīng)盡可能利用函數(shù)名或調(diào)整代碼來取代注釋。
4.3.4 闡釋
如果闡釋是必要的,一定要保證注釋的正確性。
4.3.5 警示
public static SimpleDateFormat makeStandardHttpDateFormat()
{
//SimpleDateFormat is not thread safe,
//so we need to create each instance independantly.
SimpleDateFormat df = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z");
df.setTimeZone(TimeZone.getTimeZone("GMT"));
return df;
}
4.3.6 TODO 注釋
不要以此為借口留下糟糕的代碼,要定期查看及維護(hù)。
4.3.7 放大
注釋可以用來放大某種看來不合理之物的重要性。
// the trim is real important. It removes the starting
// spaces that could cause the item to be recognized
// as another list.
String listItemContent = match.group(3).trim();
4.3.8 公共 API 中的 Javadoc
被良好描述的公共 API 實用且優(yōu)雅。
4.4 壞注釋
壞注釋是借口。
如果決定要寫注釋,就要寫好寫清楚。
迫使讀者查看其他模塊的注釋才能弄清原委的注釋是糟糕的。
不要寫廢話注釋與誤導(dǎo)性注釋。
要用整理代碼的決心替代寫廢話的沖動。
不應(yīng)在代碼中保留日志、歸屬、署名以及注釋掉的代碼等內(nèi)容,應(yīng)使用版本控制系統(tǒng)。
如果想要在括號后面寫注釋,那么更好的做法是縮短函數(shù)。
別在注釋中給出與被注釋代碼無關(guān)的信息。
注釋與其代碼之間的聯(lián)系應(yīng)顯而易見,以便閱讀理解。
范例
代碼清單 4-8 PrimeGenerator.java
/**
* This class Generates prime numbers up to a user specified
* maximum. The algorithm used is the Sieve of Eratosthenes.
* Given an array of integers starting at 2:
* Find the first uncrossed integer, and cross out all its
* multiples. Repeat until there are no more multiples
* in the array.
*/
public class PrimeGenerator
{
private static boolean[] crossedOut;
private static int[] result;
public static int[] generatePrimes(int maxValue)
{
if(maxValue<2)
return new int[0];
else
{
uncrossIntegersUpTo(maxValue);
crossOutMultiples();
putUncrossedIntegersIntoResult();
return result;
}
}
private static void uncrossIntegersUpTo(int maxValue)
{
crossedOut = new boolean[maxValue + 1];
for (int i = 2; i < crossedOut.length; i++)
crossedOut[i] = false;
}
private static void crossOutMultiples()
{
int limit = determineIterationLimit();
for (int i = 2; i <= limit; i++)
if (notCrossed(i))
crossOutMultiplesOf(i);
}
private static int determineIterationLimit()
{
// Every multiple in the array has a prime factor that
// is less than or equal to the root of the array size,
// so we don't have to cross out multiples of numbers
// larger than that root.
double iterationLimit = Math.sqrt(crossedOut.length);
return (int) iterationLimit;
}
private static void crossOutMultiplesOf(int i)
{
for (int multiple = 2*i; multiple < crossedOut.length; multiple += i)
crossedOut[multiple] = true;
}
private static boolean notCrossed(int i)
{
return crossedOut[i] == false;
}
private static void putUncrossedIntegersIntoResult()
{
result = new int[numberOfUncrossedIntegers()];
for (int j = 0; i = 2; i < crossedOut.length; i++)
if (notCrossed(i))
result[j++] = i;
}
private static int numberOfUncrossedIntegers()
{
int count = 0
for (int i = 2; i < crossedOut.length; i++)
if (notCrossed(i))
count++;
return count;
}
}
第5章 格式
5.1 格式的目的
代碼風(fēng)格和可讀性會影響維護(hù)性和擴(kuò)展性。
5.2 垂直格式
5.2.1 向報紙學(xué)習(xí)
原文件應(yīng)像報紙文章。名稱應(yīng)當(dāng)簡單且一目了然。
源文件最頂部應(yīng)該給出高層次概念和算法,細(xì)節(jié)應(yīng)該往下漸次展開,直至找到源文件中最底層的函數(shù)和細(xì)節(jié)。
5.2.2 垂直方向上的區(qū)隔與靠近
獨立的概念應(yīng)用空白行隔開,相關(guān)的代碼應(yīng)該相互靠近。
5.2.4 垂直距離
相關(guān)函數(shù)
如果a函數(shù)調(diào)用了b函數(shù),就應(yīng)該把他們放到一起,而且a函數(shù)應(yīng)該盡可能放在b函數(shù)上面,形成自然順序。
5.3 橫向格式
作者的代碼行長度上限是120個字符。
小鐳:關(guān)于對齊與縮進(jìn),可使用 IDE 默認(rèn)的自動化格式或自定義格式。
5.5 鮑勃大叔的格式規(guī)則
代碼清單 5-6 CodeAnalyzer.java
public class CodeAnalyzer implements JavaFileAnalysis{
private int lineCount;
private int maxLineWidth;
private int widestLineNumber;
private LineWidthHistogram lineWidthHistogram;
private int totalChars;
public CodeAnalyzer(){
lineWidthHistogram = new LineWidthHistogram();
}
public static List<File> findJavaFiles(File parentDirectory){
List<File> files = new ArrayList<File>();
findJavaFiles(parentDirectory, files);
return files;
}
private static void findJavaFiles(File parentDirectory, List<File> files){
for (File file : parentDirectory.listFiles()){
if (file.getName().endsWith(".java"))
files.add(file);
else if (file.isDirectory())
findJavaFiles(file, files);
}
}
public void analyzeFile(File javaFile) throws Exception{
BufferedReader br = new BufferedReader(new FileReader(javaFile));
String line;
while ((line = br.readLine()) != null)
measureLine(line);
}
private void measureLine(String line){
lineCount++;
int lineSize = line.length();
totalChars += lineSize;
lineWidthHistogram.addLine(lineSize, lineCount);
recordWidestLine(lineSize);
}
private void recordWidestLine(int lineSize){
if (lineSize > maxLineWidth){
maxLineWidth = lineSize;
widestLineNumber = lineCount;
}
}
public int getLineCount(){
return lineCount;
}
public int getMaxLineWidth(){
return maxLineWidth;
}
public int getWidestLineNumber(){
return widestLineNumber;
}
public LineWidthHistogram getLineWidthHistogram(){
return lineWidthHistogram;
}
public double getMeanLineWidth(){
return (double) totalChars / lineCount;
}
public int getMedianLineWidth(){
Integer[] sortedWidths = getSortedWidths();
int cumulativeLineCount = 0;
for (int width : sortedWidths){
cumulativeLineCount += lineCountForWidth(width);
if (cumulativeLineCount > lineCount / 2)
return width;
}
throw new Error("Cannot get here");
}
private int lineCountForWidth(int width){
return lineWidthHistogram.getLinesforWidth(width).size();
}
private Integer[] getSortedWidths(){
Set<Integer> widths = lineWidthHistogram.getWidth();
Integer[] sortedWidths = (widths.toArray(new Integer[0]));
Arrays.sort(sortedWidths);
return sortedWidths;
}
}
第6章 對象和數(shù)據(jù)結(jié)構(gòu)
6.1 數(shù)據(jù)抽象
曝露抽象接口可以使用戶無需了解數(shù)據(jù)的實現(xiàn)就能操作數(shù)據(jù)本體。
代碼清單 6-3 具象機(jī)動車
public interface Vehicle{
double getFuelTankCapacityInGallons();
double getGallonsofGasoline();
}
代碼清單 6-4 抽象機(jī)動車
public interface Vehicle{
double getPecentFuelRemaining();
}
我們不愿曝露數(shù)據(jù)細(xì)節(jié),更愿意以抽象形態(tài)表述數(shù)據(jù)。
小鐳:
此處沒有完全理解。作者的意思是否是說,此處需要的就是百分比,所以接口應(yīng)直接表示所需數(shù)據(jù),而不是簡單地曝露變量、讓使用者利用獲取的變量自行計算完成。
但如果換成是生日(屬性)和年齡(方法),而兩項數(shù)據(jù)又都必要,兩者就都需要曝露。
6.2 數(shù)據(jù)、對象的反對稱性
對象把數(shù)據(jù)隱藏于抽象之后,曝露操作數(shù)據(jù)的函數(shù)。
數(shù)據(jù)結(jié)構(gòu)曝露其數(shù)據(jù),沒有提供有意義的函數(shù)。
代碼清單 6-5 過程式形狀代碼
public class Square{
public Point topLeft;
public double side;
}
public class Rectangle{
public Point topLeft;
public double height;
public double width;
}
public class Circle{
public Point center;
public double radius;
}
public class Geometry{
public final double PI = 3.1415926;
public double area(Object shape) throws 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 NoSuchShapeException();
}
}
代碼清單 6-6 多態(tài)式形狀代碼
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{
private Point center;
private double radius;
public final double PI = 3.1415926;
public double area(){
return PI * radius * radius;
}
}
注意,兩者是對立的,它們各有優(yōu)勢。他們之間的二分原理:
過程式代碼(使用數(shù)據(jù)結(jié)構(gòu)的代碼)便于在不改動既有數(shù)據(jù)結(jié)構(gòu)的前提下添加新函數(shù),面向?qū)ο蟠a便于在不改動既有函數(shù)的前提下添加新類。
反過來講:
過程式代碼難以添加新數(shù)據(jù)結(jié)構(gòu),因為必須修改所有函數(shù)。面向?qū)ο蟠a難以添加新函數(shù),因為必須修改所有類。
6.3 得墨忒耳定律
一種松耦合的方案。
得墨忒耳定律:模塊不應(yīng)了解它所操作對象的內(nèi)部情形。
Law of Demeter, wiki
例如,類 C 的方法 f 只應(yīng)該調(diào)用以下對象的方法:
- C
- 由 f 創(chuàng)建的對象;
- 作為參數(shù)傳遞給 f 的對象;
- 由 C 的實體變量持有的對象。
方法不應(yīng)調(diào)用由任何函數(shù)返回的對象的方法(只跟朋友談話,不跟陌生人談話)。
違反了得墨忒耳定律的例子:
火車失事代碼
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
6.3.1 火車失事
小鐳:思考 Lambda 表達(dá)式與火車失事代碼。
火車失事代碼的切分:
Options opts = ctxt.getOptions();
File scratchDir = opts.getScratchDir();
final String outputDir = scratchDir.getAbsolutePath();
這些代碼是否違反得墨忒耳定律取決于 ctxt、Options 和 ScratchDir 是對象還是數(shù)據(jù)結(jié)構(gòu)。
如果是對象,則它們的內(nèi)部結(jié)構(gòu)應(yīng)當(dāng)隱藏而不曝露,以上代碼涉及其內(nèi)部細(xì)節(jié)就違反了定律。如果它們是數(shù)據(jù)結(jié)構(gòu),沒有任何行為,則他們自然會曝露其內(nèi)部結(jié)構(gòu),定律在此也就不適用。
屬性訪問器函數(shù)的使用把問題復(fù)雜化了。不會提及違反定律的代碼:
final String outputDir = ctxt.options.scratchDir.absolutePath;
6.3.2 混雜
一半是對象,一半是數(shù)據(jù)結(jié)構(gòu),這是一種糟糕的設(shè)計。它增加了添加新函數(shù)的難度,也增加了添加新數(shù)據(jù)結(jié)構(gòu)的難度。
小鐳:
不幸的是很多編程書籍的例子都是這種混雜設(shè)計,作者只是想通過簡單的一次性代碼來講解編程概念,但這些欠缺設(shè)計的代碼卻在設(shè)計上誤導(dǎo)了讀者。
6.3.3 隱藏結(jié)構(gòu)
既然取得路徑的目的是為了創(chuàng)建文件,那么不妨讓 ctxt 對象來做這件事:
BufferedOutputStream bos = ctxt.createScratchFileStream(classFileName);
6.4 數(shù)據(jù)傳送對象
數(shù)據(jù)傳送對象
最為精煉的數(shù)據(jù)結(jié)構(gòu),是一個只有公共變量、沒有函數(shù)的類。
這種數(shù)據(jù)結(jié)構(gòu)有時被稱為 DTO,Data Transfer Objects,數(shù)據(jù)傳送對象。
小鐳:MVC 模式中的模型就是這樣的數(shù)據(jù)結(jié)構(gòu)。
豆結(jié)構(gòu)
豆結(jié)構(gòu)擁有由賦值器和取值器操作的私有變量。
小鐳:Java 沒有屬性
代碼清單 6-7 address.java
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 String getStreetExtra(){
return streetExtra;
}
public String getCity(){
return city;
}
public String getState(){
return state;
}
public String getZip(){
return zip;
}
}
Active Record
Active Record 是一種特殊的 DTO 形式。
Active record pattern, wiki