“讓代碼能工作”纔是專業開發者的頭等大事?看看Bob大叔怎麼說

 

當有人查看底層代碼實現時,我們希望他們爲其整潔、一致及所感知到的對細節的關注而震驚。我們希望他們高高揚起眉毛,一路看下去。我們希望他們感受到那些爲之勞作的專業人士們。但若他們看到的只是一堆像是由酒醉的水手寫出的鬼畫符,那他們多半會得出結論,認爲項目其他任何部分也同樣對細節漠不關心。

你應該保持良好的代碼格式。你應該選用一套管理代碼格式的簡單規則,然後貫徹這些規則。如果你在團隊中工作,則團隊應該一致同意採用一套簡單的格式規則,所有成員都要遵從。使用能幫你應用這些格式規則的自動化工具會很有幫助。

5.1 格式的目的

先明確一下,代碼格式很重要。代碼格式不可忽略,必須嚴肅對待。代碼格式關乎溝通,而溝通是專業開發者的頭等大事。

或許你認爲“讓代碼能工作”纔是專業開發者的頭等大事。然而,我希望本書能讓你拋掉那種想法。你今天編寫的功能,極有可能在下一版本中被修改,但代碼的可讀性卻會對以後可能發生的修改行爲產生深遠影響。原始代碼修改之後很久,其代碼風格和可讀性仍會影響到可維護性和擴展性。即便代碼已不復存在,你的風格和律條仍存活下來。

那麼,哪些代碼格式相關方面能幫我們最好地溝通呢?

5.2 垂直格式

從垂直尺寸開始吧。源代碼文件該有多大?在Java中,文件尺寸與類尺寸極其相關。討論類時再說類的尺寸。現在先考慮文件尺寸。

多數Java源代碼文件有多大?事實說明,尺寸各各不同,長度殊異,如圖5-1所示。

圖5-1 以對數標尺顯示的文件長度分佈(方塊高度=sigma)

圖5-1中涉及7個不同項目:Junit、FitNesse、testNG、Time and Money、JDepend、Ant和Tomcat。貫穿方塊的直線兩端顯示這些項目中最小和最大的文件長度。方塊表示在平均值以上或以下的大約三分之一文件(一個標準偏差[1])的長度。方塊中間位置就是平均數。所以FitNesse項目的文件平均尺寸是65行,而上面三分之一在40~100行及100行以上之間。FitNesse中最大的文件大約400行,最小是6行。這是個對數標尺,所以較小的垂直位置差異意味着文件絕對尺寸的較大差異。

Junit、FitNesse和Time and Money由相對較小的文件組成。沒有一個超過500行,多數都小於200行。Tomcat和Ant則有些文件達到數千行,將近一半文件長於200行。

對我們來說,這意味着什麼?意味着有可能用大多數爲200行、最長500行的單個文件構造出色的系統(FitNesse總長約50000行)。儘管這並非不可違背的原則,也應該樂於接受。短文件通常比長文件易於理解。

5.2.1 向報紙學習

想想看寫得很好的報紙文章。你從上到下閱讀。在頂部,你期望有個頭條,告訴你故事主題,好讓你決定是否要讀下去。第一段是整個故事的大綱,給出粗線條概述,但隱藏了故事細節。接着讀下去,細節漸次增加,直至你瞭解所有的日期、名字、引語、說法及其他細節。

源文件也要像報紙文章那樣。名稱應當簡單且一目瞭然。名稱本身應該足夠告訴我們是否在正確的模塊中。源文件最頂部應該給出高層次概念和算法。細節應該往下漸次展開,直至找到源文件中最底層的函數和細節。

報紙由許多篇文章組成;多數短小精悍。有些稍微長點兒。很少有佔滿一整頁的。這樣做,報紙纔可用。假若一份報紙只登載一篇長故事,其中充斥毫無組織的事實、日期、名字等,沒人會去讀它。

5.2.2 概念間垂直方向上的區隔

幾乎所有的代碼都是從上往下讀,從左往右讀。每行展現一個表達式或一個子句,每組代碼行展示一條完整的思路。這些思路用空白行區隔開來。

以代碼清單5-1爲例。在封包聲明、導入聲明和每個函數之間,都有空白行隔開。這條極其簡單的規則極大地影響到代碼的視覺外觀。每個空白行都是一條線索,標識出新的獨立概念。往下讀代碼時,你的目光總會停留於空白行之後那一行。

代碼清單5-1 BoldWidget.java

package fitnesse.wikitext.widgets;

import java.util.regex.*;

public class BoldWidget extends ParentWidget {
 public static final String REGEXP = "'''.+?'''";
 private static final Pattern pattern = Pattern.compile("'''(.+?)'''",
  Pattern.MULTILINE + Pattern.DOTALL
 );

 public BoldWidget(ParentWidget parent, String text) throws Exception {
  super(parent);
  Matcher match = pattern.matcher(text);
  match.find();
  addChildWidgets(match.group(1));
 }

 public String render() throws Exception {
  StringBuffer html = new StringBuffer("<b>");
  html.append(childHtml()).append("</b>");
  return html.toString();
 }
}

如代碼清單5-2所示,抽掉這些空白行, 代碼可讀性減弱了不少。

代碼清單5-2 BoldWidget.java

package fitnesse.wikitext.widgets;
import java.util.regex.*;
public class BoldWidget extends ParentWidget {
 public static final String REGEXP = "'''.+?'''";
 private static final Pattern pattern = Pattern.compile("'''(.+?)'''",
  Pattern.MULTILINE + Pattern.DOTALL);
 public BoldWidget(ParentWidget parent, String text) throws Exception {
  super(parent);
  Matcher match = pattern.matcher(text);
  match.find();
  addChildWidgets(match.group(1));}
 public String render() throws Exception {
  StringBuffer html = new StringBuffer("<b>");
  html.append(childHtml()).append("</b>");
  return html.toString();
 }
}

在你不特意注視時,後果就更嚴重了。在第一個例子中,代碼組會跳到你眼中,而第二個例子就像一堆亂麻。兩段代碼的區別,展示了垂直方向上區隔的作用。

5.2.3 垂直方向上的靠近

如果說空白行隔開了概念,靠近的代碼行則暗示了它們之間的緊密關係。所以,緊密相關的代碼應該互相靠近。注意代碼清單5-3中的註釋是如何割斷兩個實體變量間的聯繫的。

代碼清單5-3

public class ReporterConfig {
 /**
  * The class name of the reporter listener
  */
 private String m_className;

 /**
  * The properties of the reporter listener
  */
 private List<Property> m_properties = new ArrayList<Property>();

 public void addProperty(Property property) {
  m_properties.add(property);
 }

代碼清單5-4更易於閱讀。它剛好“一覽無遺”,至少對我來說是這樣。我一眼就能看到,這是個有兩個變量和一個方法的類。看上面的代碼時,我不得不更多地移動頭部和眼球,才能獲得相同的理解度。

代碼清單5-4

public class ReporterConfig {
 private String m_className;
 private List<Property> m_properties = new ArrayList<Property>();

 public void addProperty(Property property) {
  m_properties.add(property);
 }

5.2.4 垂直距離

你是否曾經在某個類中摸索,從一個函數跳到另一個函數,上下求索,想要弄清楚這些函數如何操作、如何互相相關,最後卻被搞糊塗了?你是否曾經苦苦追索某個變量或函數的繼承鏈條?這讓人沮喪,因爲你想要理解系統做什麼,但卻花時間和精力在找到和記住那些代碼碎片在哪裏

關係密切的概念應該互相靠近[G10]。顯然,這條規則並不適用於分佈在不同文件中的概念。除非有很好的理由,否則就不要把關係密切的概念放到不同的文件中。實際上,這也是避免使用protected變量的理由之一。

對於那些關係密切、放置於同一源文件中的概念,它們之間的區隔應該成爲對相互的易懂度有多重要的衡量標準。應避免迫使讀者在源文件和類中跳來跳去。

變量聲明。變量聲明應儘可能靠近其使用位置。因爲函數很短,本地變量應該在函數的頂部出現,就像Junit4.3.1中這個稍長的函數中那樣。

private static void readPreferences() {
 InputStream is= null;
 try {
  is= new FileInputStream(getPreferencesFile());
  setPreferences(new Properties(getPreferences()));
  getPreferences().load(is);
 } catch (IOException e) {
  try {
   if (is != null)
    is.close();
  } catch (IOException e1) {
  }
 }
}

循環中的控制變量應該總是在循環語句中聲明,如下列來自同一項目的絕妙小函數所示。

public int countTestCases() {
 int count= 0;
 for (Test each : tests)
   count += each.countTestCases();
 return count;
}

偶爾,在較長的函數中,變量也可能在某個代碼塊頂部,或在循環之前聲明。你可以在以下摘自TestNG中一個長函數的代碼片段中找到類似的變量。

...
for (XmlTest test : m_suite.getTests()) {
    TestRunner tr = m_runnerFactory.newTestRunner(this, test);
    tr.addListener(m_textReporter);
    m_testRunners.add(tr);

    invoker = tr.getInvoker();

    for (ITestNGMethod m : tr.getBeforeSuiteMethods()) {
     beforeSuiteMethods.put(m.getMethod(), m);
    }

    for (ITestNGMethod m : tr.getAfterSuiteMethods()) {
     afterSuiteMethods.put(m.getMethod(), m);
    }
   }
...

實體變量應該在類的頂部聲明。這應該不會增加變量的垂直距離,因爲在設計良好的類中,它們如果不是被該類的所有方法也是被大多數方法所用。

關於實體變量應該放在哪裏,爭論不斷。在C++中,通常會採用所謂“剪刀原則”(scissors rule),所有實體變量都放在底部。而在Java中,慣例是放在類的頂部。沒理由去遵循其他慣例。重點是在誰都知道的地方聲明實體變量。大家都應該知道在哪兒能看到這些聲明。

例如JUnit 4.3.1中的這個奇怪情形。我極力刪減了這個類,好說明問題。如果你看到代碼清單大致一半的位置,會看到在那裏聲明瞭兩個實體變量。如果放在更好的位置,它們就會更明顯。而現在,讀代碼者只能在無意中看到這些聲明(就像我一樣)。

public class TestSuite implements Test {
 static public Test createTest(Class<? extends TestCase> theClass, 
                   String name) {
  ...
 }

 public static Constructor<? extends TestCase> 
 getTestConstructor(Class<? extends TestCase> theClass) 
 throws NoSuchMethodException {
  ...
 }

 public static Test warning(final String message) {
  ...
 }

 private static String exceptionToString(Throwable t) {
  ...
 }

 private String fName;
 
 private Vector<Test> fTests= new Vector<Test>(10);

 public TestSuite() {
 }

 public TestSuite(final Class<? extends TestCase> theClass) {
  ...
 }

 public TestSuite(Class<? extends TestCase> theClass, String name) {
  ...
 }
 ... ... ... ... ...
}

相關函數。若某個函數調用了另外一個,就應該把它們放到一起,而且調用者應該儘可能放在被調用者上面。這樣,程序就有個自然的順序。若堅定地遵循這條約定,讀者將能夠確信函數聲明總會在其調用後很快出現。以源自FitNesse的代碼清單5-5爲例。注意頂部的函數是如何調用其下的函數,而這些被調用的函數又是如何調用更下面的函數的。這樣就能輕易找到被調用的函數,極大地增強了整個模塊的可讀性。

代碼清單5-5 WikiPageResponder.java

public class WikiPageResponder implements SecureResponder {
 protected WikiPage page;
 protected PageData pageData;
 protected String pageTitle;
 protected Request request;
 protected PageCrawler crawler;

 public Response makeResponse(FitNesseContext context, Request request)
  throws Exception {
  String pageName = getPageNameOrDefault(request, "FrontPage");
  loadPage(pageName, context);
  if (page == null)
   return notFoundResponse(context, request);
  else
   return makePageResponse(context);
 }

 private String getPageNameOrDefault(Request request, String defaultPageName) 
 {
  String pageName = request.getResource();
  if (StringUtil.isBlank(pageName))
   pageName = defaultPageName;
  return pageName;
 }

 protected void loadPage(String resource, FitNesseContext context)
  throws Exception {
  WikiPagePath path = PathParser.parse(resource);
  crawler = context.root.getPageCrawler();
  crawler.setDeadEndStrategy(new VirtualEnabledPageCrawler());
  page = crawler.getPage(context.root, path);
  if (page != null)
   pageData = page.getData();
 }

 private Response notFoundResponse(FitNesseContext context, Request request)
  throws Exception {
  return new NotFoundResponder().makeResponse(context, request);
 }

 private SimpleResponse makePageResponse(FitNesseContext context)
  throws Exception {
  pageTitle = PathParser.render(crawler.getFullPath(page));
  String html = makeHtml(context);

  SimpleResponse response = new SimpleResponse();
  response.setMaxAge(0);
  response.setContent(html);
  return response;
 }
 ...

說句題外話,以上代碼片段也是把常量保持在恰當級別的好例子[G35]。FrontPage常量可以埋在getPageNameOrDefault函數中,但那樣就會把一個衆人皆知的常量埋藏到位於不太合適的底層函數中。更好的做法是把它放在易於找到的位置,然後再傳遞到真實使用的位置。

概念相關。概念相關的代碼應該放到一起。相關性越強,彼此之間的距離就該越短。

如上所述,相關性應建立在直接依賴的基礎上,如函數間調用,或函數使用某個變量。但也有其他相關性的可能。相關性可能來自於執行相似操作的一組函數。請看以下來自Junit 4.3.1的代碼片段:

public class Assert {
 static public void assertTrue(String message, boolean condition) {
  if (!condition)
   fail(message);
 }

 static public void assertTrue(boolean condition) {
  assertTrue(null, condition);
 }

 static public void assertFalse(String message, boolean condition) { 
  assertTrue(message, !condition);
 }

 static public void assertFalse(boolean condition) {
  assertFalse(null, condition);
 }
 ...

這些函數有着極強的概念相關性,因爲他們擁有共同的命名模式,執行同一基礎任務的不同變種。互相調用是第二位的。即便沒有互相調用,也應該放在一起。

5.2.5 垂直順序

一般而言,我們想自上向下展示函數調用依賴順序。也就是說,被調用的函數應該放在執行調用的函數下面[2]。這樣就建立了一種自頂向下貫穿源代碼模塊的良好信息流。

像報紙文章一般,我們指望最重要的概念先出來,指望以包括最少細節的方式表述它們。我們指望底層細節最後出來。這樣,我們就能掃過源代碼文件,自最前面的幾個函數獲知要旨,而不至於沉溺到細節中。代碼清單5-5就是如此組織的。或許,更好的例子是代碼清單15-5,及代碼清單3-7。

5.3 橫向格式

一行代碼應該有多寬?要回答這個問題,來看看典型的程序中代碼行的寬度。我們再一次檢驗7個不同項目。圖5-2展示了這7個項目的代碼行寬度分佈情況。其中展現的規律性令人印象深刻,45個字符左右的寬度分佈尤爲如此。其實,20~60的每個尺寸,都代表全部代碼行數的1%。也就是總共40%!或許其餘30%的代碼行短於10個字符。記住,這是個對數標尺,所以圖中長於80個字符部分的線性下降在實際情況中會極其可觀。程序員們顯然更喜愛短代碼行。

圖5-2 Java程序代碼行長度分佈

這說明,應該盡力保持代碼行短小。死守80個字符的上限有點僵化,而且我也並不反對代碼行長度達到100個字符或120個字符。再多的話,大抵就是肆意妄爲了。

我一向遵循無需拖動滾動條到右邊的原則。但近年來顯示器越來越寬,而年輕程序員又能將顯示字符縮小到如此程度,屏幕上甚至能容納200個字符的寬度。別那麼做。我個人的上限是120個字符。

5.3.1 水平方向上的區隔與靠近

我們使用空格字符將彼此緊密相關的事物連接到一起,也用空格字符把相關性較弱的事物分隔開。請看以下函數:

private void measureLine(String line) {
 lineCount++;
 int lineSize = line.length();
 totalChars += lineSize;
 lineWidthHistogram.addLine(lineSize, lineCount);
 recordWidestLine(lineSize);
}

我在賦值操作符周圍加上空格字符,以此達到強調目的。賦值語句有兩個確定而重要的要素:左邊和右邊。空格字符加強了分隔效果。

另一方面,我不在函數名和左圓括號之間加空格。這是因爲函數與其參數密切相關,如果隔開,就會顯得互無關係。我把函數調用括號中的參數一一隔開,強調逗號,表示參數是互相分離的。

空格字符的另一種用法是強調其前面的運算符。

public class Quadratic {
 public static double root1(double a, double b, double c) {
  double determinant = determinant(a, b, c);
  return (-b + Math.sqrt(determinant)) / (2*a);
 }

 public static double root2(int a, int b, int c) {
  double determinant = determinant(a, b, c);
  return (-b - Math.sqrt(determinant)) / (2*a); 
 }

 private static double determinant(double a, double b, double c) {
  return b*b - 4*a*c;
 }
}

看看這些等式讀起來多舒服。乘法因子之間沒加空格,因爲它們具有較高優先級。加減法運算項之間用空格隔開,因爲加法和減法優先級較低。

不幸的是,多數代碼格式化工具都會漠視運算符優先級,從頭到尾採用同樣的空格方式。在重新格式化代碼後,以上這些微妙的空格用法就消失殆盡了。

5.3.2 水平對齊

當我還是個彙編語言程序員時[3],使用水平對齊來強調某些程序結構。開始用C、C++編碼,最終轉向Java後,我繼續盡力對齊一組聲明中的變量名,或一組賦值語句中的右值。我的代碼看起來大概是這樣:

public class FitNesseExpediter implements ResponseSender
{
 private   Socket       socket;
 private   InputStream    input;
 private   OutputStream   output;
 private   Request      request;
 private   Response      response;
 private   FitNesseContext  context;
 protected  long         requestParsingTimeLimit;
 private   long         requestProgress;
 private   long         requestParsingDeadline;
 private   boolean        hasError;

 public FitNesseExpediter(Socket      s, 
               FitNesseContext context) throws Exception
{
  this.context =      context;
  socket =          s;
  input =           s.getInputStream();
  output =          s.getOutputStream();
  requestParsingTimeLimit = 10000;
 }

我發現這種對齊方式沒什麼用。對齊,像是在強調不重要的東西,把我的目光從真正的意義上拉開。例如,在上面的聲明列表中,你會從上到下閱讀變量名,而忽視了它們的類型。同樣,在賦值語句代碼清單中,你也會從上到下閱讀右值,而對賦值運算符視而不見。更麻煩的是,代碼自動格式化工具通常會把這類對齊消除掉。

所以,我最終放棄了這種做法。如今,我更喜歡用不對齊的聲明和賦值,如下所示,因爲它們指出了重點。如果有較長的列表需要做對齊處理,那問題就是在列表的長度上而不是對齊上。下例FitNesseExpediter類中聲明列表的長度說明該類應該被拆分了。

public class FitNesseExpediter implements ResponseSender
{
 private Socket socket;
 private InputStream input;
 private OutputStream output;
 private Request request;
 private Response response;
 private FitNesseContext context;
 protected long requestParsingTimeLimit;
 private long requestProgress;
 private long requestParsingDeadline;
 private boolean hasError;

 public FitNesseExpediter(Socket s, FitNesseContext context) throws Exception
 {
  this.context = context;
  socket = s;
  input = s.getInputStream();
  output = s.getOutputStream();
  requestParsingTimeLimit = 10000;
 }

5.3.3 縮進

源文件是一種繼承結構,而不是一種大綱結構。其中的信息涉及整個文件、文件中每個類、類中的方法、方法中的代碼塊,也涉及代碼塊中的代碼塊。這種繼承結構中的每一層級都圈出一個範圍,名稱可以在其中聲明,而聲明和執行語句也可以在其中解釋。

要讓這種範圍式繼承結構可見,我們依源代碼行在繼承結構中的位置對源代碼行做縮進處理。在文件頂層的語句,例如大多數的類聲明,根本不縮進。類中的方法相對該類縮進一個層級。方法的實現相對方法聲明縮進一個層級。代碼塊的實現相對於其容器代碼塊縮進一個層級,以此類推。

程序員相當依賴這種縮進模式。他們從代碼行左邊查看自己在什麼範圍中工作。這讓他們能快速跳過與當前關注的情形無關的範圍,例如if或while語句的實現之類。他們的眼光掃過左邊,查找新的方法聲明、新變量,甚至新類。沒有縮進的話,程序就會變得無法閱讀。

試看以下在語法和語義上等價的兩個程序:

public class FitNesseServer implements SocketServer { private FitNesseContext 
context; public FitNesseServer(FitNesseContext context) { this.context = 
context; } public void serve(Socket s) { serve(s, 10000); } public void 
serve(Socket s, long requestTimeout) { try { FitNesseExpediter sender = new 
FitNesseExpediter(s, context); 
sender.setRequestParsingTimeLimit(requestTimeout); sender.start(); } 
catch(Exception e) { e.printStackTrace(); } } }

-----

public class FitNesseServer implements SocketServer {
 private FitNesseContext context;
 public FitNesseServer(FitNesseContext context) {
  this.context = context;
 }

 public void serve(Socket s) {
  serve(s, 10000);
 }

 public void serve(Socket s, long requestTimeout) {
  try {
   FitNesseExpediter sender = new FitNesseExpediter(s, context);
   sender.setRequestParsingTimeLimit(requestTimeout);
   sender.start();
  }
  catch (Exception e) {
   e.printStackTrace();
  }
 }
}

你能很快地洞悉有縮進的那個文件的結構。你幾乎能立即就辨別出那些變量、構造器、存取器和方法。只需要幾秒鐘就能瞭解這是一個套接字的簡單前端,其中包括了超時設定。而未縮進的版本則不經過一番折騰就無法明白。

違反縮進規則。有時,會忍不住想要在短小的if語句、while循環或小函數中違反縮進規則。一旦這麼做了,我多數時候還是會回頭加上縮進。這樣就避免了出現以下這種範圍層級坍塌到一行的情況:

public class CommentWidget extends TextWidget
{
 public static final String REGEXP = "^#[^\r\n]*(?:(?:\r\n)|\n|\r)?";

 public CommentWidget(ParentWidget parent, String text){super(parent, text);}
 public String render() throws Exception {return ""; }
}

我更喜歡擴展和縮進範圍,就像這樣:

public class CommentWidget extends TextWidget {
 public static final String REGEXP = "^#[^\r\n]*(?:(?:\r\n)|\n|\r)?";

 public CommentWidget(ParentWidget parent, String text) {
  super(parent, text);
 }
 public String render() throws Exception {
  return "";
 }
}

5.3.4 空範圍

有時,while或for語句的語句體爲空,如下所示。我不喜歡這種結構,儘量不使用。如果無法避免,就確保空範圍體的縮進,用括號包圍起來。我無法告訴你,我曾經多少次被靜靜安坐在與while循環語句同一行末尾的分號所欺騙。除非你把那個分號放到另一行再加以縮進,否則就很難看到它。

while (dis.read(buf, 0, readBufferSize) != -1)
;

5.4 團隊規則

每個程序員都有自己喜歡的格式規則,但如果在一個團隊中工作,就是團隊說了算[4]。

一組開發者應當認同一種格式風格,每個成員都應該採用那種風格。我們想要讓軟件擁有一以貫之的風格。我們不想讓它顯得是由一大票意見相左的個人所寫成。

2002年啓動FitNesse項目時,我和開發團隊一起制訂了一套編碼風格。這隻花了我們10分鐘時間。我們決定了在什麼地方放置括號,縮進幾個字符,如何命名類、變量和方法,如此等等。然後,我們把這些規則編寫進IDE的代碼格式功能,接着就一直沿用。這些規則並非全是我喜愛的;但它們是團隊決定了的規則。作爲團隊一員,在爲FitNesse項目編寫代碼時,我遵循這些規則。

記住,好的軟件系統是由一系列讀起來不錯的代碼文件組成的。它們需要擁有一致和順暢的風格。讀者要能確信,他們在一個源文件中看到的格式風格在其他文件中也是同樣的用法。絕對不要用各種不同的風格來編寫源代碼,這樣會增加其複雜度。

5.5 鮑勃大叔的格式規則

我個人使用的規則相當簡單,如代碼清單5-6所示。可以把這段代碼看作是展示如何把代碼寫成最好的編碼標準文檔的範例。

代碼清單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.getWidths();
  Integer[] sortedWidths = (widths.toArray(new Integer[0]));
  Arrays.sort(sortedWidths);
  return sortedWidths;
 }
}

[1] 原注:方塊顯示平均數的sigma/2以上及以下長度。沒錯,我知道文件長度分佈不太尋常,所以標準偏差也並非那麼精確。不過在此並不尋求精確,只是找個感覺罷了。

[2] 原注:Pascal、C和C++等語言中完全不同,在這些語言中,函數應該在被調用之前定義,至少是聲明。

[3] 原注:開什麼玩笑!到現在我仍是個彙編語言程序員。把男孩從鐵旁邊趕走容易,從男孩身邊把鐵拿走可難!

本文摘自《代碼整潔之道》羅伯特·C.,馬丁(Robert,C.,Martin) 著,韓磊 譯

“閱讀這本書有兩種原因:第一,你是個程序員;第二,你想成爲更好的程序員。很好,IT行業需要更好的程序員!”——羅伯特·C. 馬丁(Robert C. Martin) 

儘管糟糕的代碼也能運行,但如果代碼不整潔,會使整個開發團隊泥足深陷,寫得不好的代碼每年都要耗費難以計數的時間和資源。但是,這種情況並非無法避免。 

著名軟件專家羅伯特·C. 馬丁(Robert C. Martin) 在本書中爲你呈現了革命性的視野。他攜同Object Mentor公司的同事,從他們有關整潔代碼的*佳敏捷實踐中提煉出軟件技藝的價值觀,以饗讀者,讓你成爲更優秀的程序員——只要你着手研讀本書。 

閱讀本書需要你做些什麼呢?你將閱讀代碼——大量代碼。本書會促使你思考何謂正確的代碼,何謂錯誤的代碼。更重要的是,本書將促使你重新評估自己的專業價值觀,以及對自己技藝的承諾。 

書中的具體內容包括: 

  • 好代碼和糟糕的代碼之間的區別; 
  • 如何編寫好代碼,如何將糟糕的代碼轉化爲好代碼; 
  • 如何創建好名稱、好函數、好對象和好類; 
  • 如何格式化代碼以實現其可讀性的*大化; 
  • 如何在不妨礙代碼邏輯的前提下充分實現錯誤處理; 
  • 如何進行單元測試和測試驅動開發。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章