筆記.代碼整潔之道.第1~6章

目錄及筆記鏈接

丹麥諺語:小處誠實非小事。

建筑師路德維希:神在細(xì)節(jié)之中。

日本的 5S 哲學(xué):

  1. 整理 (整理、せいり)
    組織,分類,排序,如恰當(dāng)?shù)孛?/p>

  2. 整頓?。ㄕD、せいとん)
    整齊,系統(tǒng)化。

美國諺語:物皆有其位,物盡歸其位(A place for everything, and everything in its place)。

  1. 清楚 (清楚、せいそ)
    整潔;清秀,秀麗。

諺語:整潔近乎虔誠(Cleanliness is next to godliness)。

  1. 清潔(清潔、せいけつ)
    干凈;純潔,純正。

  2. 身美(しつけ)
    綱紀(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ù)自己

DRY 原則,Don't Repeat Yourself

重復(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,525評論 19 139
  • 第5章 引用類型(返回首頁) 本章內(nèi)容 使用對象 創(chuàng)建并操作數(shù)組 理解基本的JavaScript類型 使用基本類型...
    大學(xué)一百閱讀 3,667評論 0 4
  • 我的耳洞都是為你而穿 盼來世情緣在陪你走一段http://t.cn/RzkG8KU七夕到了,你在哪? ??? ???
    世家珠寶小客服閱讀 220評論 0 0
  • 在我的記憶中,父親臉上永遠(yuǎn)帶著微笑,非常和藹、非常慈祥。用時下最時髦的一句話形容就是:天上飄來五個字,那都不是事!...
    靜靜的開閱讀 396評論 0 1
  • 在生活中,人們總是教導(dǎo)我們要學(xué)會控制自己的情緒。講說不應(yīng)該感到生氣等等。 為什么要壓抑自己的情緒?我們現(xiàn)在的人們都...
    ray君瑤閱讀 181評論 0 1

友情鏈接更多精彩內(nèi)容