1. 程式人生 > >程式碼之美——像寫作一樣去coding

程式碼之美——像寫作一樣去coding

開發十年,就只剩下這套架構體系了! >>>   

作為程式設計師,我們或許常常會被問到:你都學過什麼語言呢?你最擅長的是哪一門語言?是的,一門語言。

這裡所提到的語言並非我們的母語漢語,也不是英語亦或其他任何一種用於交流日常工作生活的語言。而是指程式設計過程中,連通人與機器、人與人之間的一種表達方式。讓機器讀懂程式碼很簡單,只需註明所用程式碼的語言規則就好,畢竟機器那麼聰明 :)但是如果想要讓其他人看懂,就不能這樣簡單粗暴了。人是感性與理性結合的動物,優雅“風趣”的表達能夠讓對方更快、更輕鬆的讀懂你的程式碼。

既然都是表達內容,那麼為什麼不用寫文章的方式來寫程式碼呢?文章是人們日常用於交流表達的一種方式,那麼我們是否可以吸收文章的優勢來用在寫程式碼上呢?

讓句子連在一起組成段落
我們可以試著把方法抽象成文章裡的一句話,方法內緊接著呼叫的另一個方法,就好像是第一句話還需要第二句話去完善一樣。所以我們應該把句子2放在句子 1 後面,也就是說我們可以把被呼叫的方法放在呼叫方法下面。

同理,一個方法內部兩個相鄰方法的呼叫先後順序就像是文章裡兩個相鄰句子的先後順序一樣。所以我們也應把這種順序作為方法上下排列的順序。

那麼如果我們不遵循這種規則會怎麼樣呢?

private void preparePizza(Pizza pizza) {
  getFlour();
}

private void boxPizza(Pizza pizza) {
...
}

public Pizza orderPizza(String type) {
  Pizza pizza = getBasePizza(type);
  preparePizza(pizza);
  boxPizza(pizza);
  return pizza;
}

private Flour getFlour() {
...
}

private Pizza getBasePizza(String type) {
...
} 

上面這段程式碼方法排序是隨意的,我們無法直觀的看到方法的執行順序。就好像是:“再然後我去吃早飯;然後我去洗漱;我早上七點起床”,混亂的順序增加了我們理解程式碼的困難度。

如果我們遵循這兩種規則來排序方法,那就如下面這樣:

public Pizza orderPizza(String type) {
  Pizza pizza;
  pizza = getPizza(type);

  preparePizza();
  boxPizza();
  return pizza;
}

private Pizza getPizza(String type) {
...
}

private void preparePizza() {
  getFlour();
}

private Flour getFlour() {
...
}

private void boxPizza() {
...
}
</pre>

當我們閱讀這段程式碼時,會覺得這是一個整體,只需要向讀文章一樣,上下滑動閱讀即可。

有的人可能會說,通過快捷鍵一樣可以定位到下一個方法。但是快捷鍵僅適用於邏輯簡單的情況,在複雜的邏輯中來回定位所產生的上下跳躍會讓人覺得非常難受,這也是我們應當竭力避免的。

定義小範圍章節目錄

一本書或一篇長文,一般都會有章節目錄。就好像一個類中有幾個提供給外界呼叫的public方法,這可以使我們有很好的全域性觀。所以我們應該把類中一些提供主要功能的對外方法放到一起,這些方法要以功能相近來集聚。

如下:

@Override
public ResultT getAResult(KeyT keySearch) {
  ...
}

@Override
public ResultT getAResultUntilTimeout(KeyT keyT, long timeout, TimeUnit timeUnit) throws TimeoutException {
  ...
}

@Override
public List<ResultT> getResultsUntilOneTimeout(KeyT keyT, long timeout, TimeUnit unit) {
  ...
}

@Override
public List<ResultT> getResultsUntilTimeout(KeyT keyT, long timeout, TimeUnit unit) {
  ...
}

@Override
public List<ResultT> getResultsUntilEnoughOrTimeout(KeyT keyT, int expectNum, long timeout, TimeUnit unit) {
  ...
}

@Override
public List<ResultT> getResultsUntilEnoughOrOneTimeout(KeyT keyT, int expectNum, long timeout, TimeUnit unit) {
  ...
}

@Override
public List<ResultT> getResultsUntilEnough(KeyT keyT, int expectNum) throws TimeoutException {
  ...
}
</pre>

這是我寫的一個search-framework中的部分程式碼,這些方法都是相近的,所以把它們放到一起。另外我們要小範圍的集聚,即把相似的開放式方法放在一起,這些方法的下面就是內部呼叫的方法,繼續遵循“讓句子連在一起組成段落”的理念。

其實有一種更好的辦法,就是可以用一種外掛讓IDEA自動生成一種目錄式方法。這種方法只包含基本資訊,沒有內部實現,並且我們可以點選目錄進入方法的準確位置(準確位置的方法排序遵循段落式描述法)。至於如何讓IDEA知道哪些方法應該生成目錄式方法,我們或許可以通過某種註解去定義。

那麼它看起來就好像下面這樣:

public class ConcurrentEntirelySearch<KeyT, ResultT, PathT> implements EntirelySearch<KeyT, ResultT> {
  private static final long MAX_WAIT_MILLISECOND = 1000 * 60 * 2;

  private final List<PathT> rootCanBeSearch;
  private final ConcurrentEntirelyOpenSearch<KeyT, ResultT, PathT> openSearch;

  public ConcurrentEntirelySearch(List<PathT> rootCanBeSearch, SearchModel searchModel) {
    this.rootCanBeSearch = rootCanBeSearch;
    this.openSearch = new ConcurrentEntirelyOpenSearch<>(searchModel);
  }

/** 目錄(如何展示細節待設計)*/
  @Override
--- public ResultT getAResult(KeyT keySearch) {...}

  @Override
--- public ResultT getAResultUntilTimeout(KeyT keyT, long timeout, TimeUnit timeUnit) throws TimeoutException {...}

  @Override
--- public List<ResultT> getResultsUntilOneTimeout(KeyT keyT, long timeout, TimeUnit unit) {...}

  @Override
--- public List<ResultT> getResultsUntilTimeout(KeyT keyT, long timeout, TimeUnit unit) {...}
/** 目錄完(虛擬內容,可點選跳轉至方法)------------------- */

  @Override
  public ResultT getAResult(KeyT keySearch) { // 此為真實方法,非目錄
    methodA();      //方法排序遵循上述的 段落式描述法
    methodB();
  }

  private void methodA() {
  ...
  }

  private void methodB() {
  ...
  }
  //下同,方法內呼叫的方法略
  @Override
  public ResultT getAResultUntilTimeout(KeyT keyT, long timeout, TimeUnit timeUnit) throws TimeoutException {
    ...
  }

  @Override
  public List<ResultT> getResultsUntilOneTimeout(KeyT keyT, long timeout, TimeUnit unit) {
    ...
  }   @Override
  public List<ResultT> getResultsUntilTimeout(KeyT keyT, long timeout, TimeUnit unit) {
    ...
  }
}

這些目錄我認為應該放在構造方法的下面,這樣看起來更加有條理。

寫文章時不要讓每一行過長

相信沒有人願意去看由一行行長文所組成的段落,長度適中的段落能夠讓讀者在跳行時有一個休息,也給大腦一個輕微的緩衝,這樣的閱讀舒適感會高很多。所以我們要善於利用這個度,不要讓程式碼過長,但是有時候也可以利用這個度去做inline,只要不超過那個限度就ok。

 

這個思想是在一次ThoughWorks的活動中受到的啟發,inline是很好,但是它不能過度,只要我們遵循“寫文章時不要讓每一行過長”的理念就ok。讓讀者得以take a breath。

就好像下面這次重構一樣:

//重構前
@Test
public void should_return_1B_given_1000000_length() {
  Gold gold = new Gold(1000000);
  String length = gold.getLength();
  assertEquals("1B", length);
}
//重構後
@Test
public void should_return_1B_given_1000000_length() {
  assertEquals("1B", new Gold(1000000).getLength());
}

上面這個例子就利用了這種理念,在讀者讀一行程式碼時,能接受的最多字元是有限的,過長就會產生疲倦感、厭惡感。

下面來看一個反例:

//重構前
int previousNumber = getNumberByUnit(lastIndex);
String target = numberString.substring(0, lastIndex);
compute(Long.parseLong(target), previousNumber);
//重構後
int previousNumber = getNumberByUnit(lastIndex);
compute(Long.parseLong(numberString.substring(0, lastIndex)), previousNumber);

這裡有必要解釋一下“度”的概念,我認為度不應該以每一行能容納的字元數來衡量。而是要以 該行內變數或方法命名的長度、該行內巢狀呼叫的方法數量、該行內呼叫方法的引數數量 這三點綜合去考慮。

“某一行命名比較長” 、 “某一行巢狀呼叫的方法比較多” 和 “某一行方法的引數比較多” 所能承受的最大字元數是不一樣的。比如:讀者能接受的“命名比較長”的最大長度跟所能接受的 “呼叫方法多的” 最大長度所能容納的字元數肯定不一樣,因為命名就算再長點也還像是一句話,我們也還算可以理解,而呼叫的方法逐漸變多那理解的複雜度就會幾何增長。

總結

如文載道,要想讓自己的程式碼發揮更大的影響,就一定要花時間去琢磨怎麼把它寫的更易讀。我們應堅持寫“笨”程式碼的思想,如果程式碼能像文章那樣有條理,有規律可循,那無疑可以增強程式碼的可維護性。這樣的程式碼閱讀起來也會讓人更加舒適
歡迎加入java中高階架構師交流群:603619042
面向1-5年java人員
幫助突破划水瓶頸