Java 太笨?純粹誹謗

出版圈郭志敏發表於2013-07-01

假設你要在一個交易(事務)處理系統中編寫一個新元件。這個系統的簡化檢視如圖7-1所示。

enter image description here

圖7-1 交易處理系統的例子

在圖中可以看到,系統有兩個資料來源:上游的收單系統(可以通過Web服務查詢)和下游的派發資料庫。

這是一個很現實的系統,是Java開發人員經常構建的系統。我們在這一節裡準備引入一小段程式碼把兩個資料來源整合起來。你會看到Java解決這個問題有點笨拙。之後我們會介紹函數語言程式設計的一個核心概念,並展示一下怎麼用對映(map)和過濾器(filter)等函式式特性簡化很多常見的程式設計任務。你會看到Java由於缺乏對這些特性的直接支援,程式設計會困難不少。

整合系統

我們需要一個整合系統來檢查資料確實到了資料庫。這個系統的核心是reconcile()方法,它有兩個引數:sourceData(來自於Web服務的資料,歸結到一個Map中)和dbIds

你需要從sourceData中取出main_ref鍵值,用它跟資料庫記錄的主鍵比較。程式碼清單7-1是進行比較的程式碼。

程式碼清單7-1 整合兩個資料來源

public void reconcile(List<Map<String, String>> sourceData,
Set<String> dbIds) {
  Set<String> seen = new HashSet <String>();
  MAIN: for (Map<String, String> row : sourceData) {
    String pTradeRef = row.get("main_ref"); //假定pTradeRef永遠不會為null 

    if (dbIds.contains(pTradeRef)) {
      System.out.println(pTradeRef +" OK");
   seen.add(pTradeRef);
 } else {
   System.out.println("main_ref: "+ pTradeRef +" not present in DB");
 }
  }

  for (String tid : dbIds) {  //特殊情況
    if (!seen.contains(tid)) {
      System.out.println("main_ref: "+ tid +" seen in DB but not Source");
    }
  }
}

這裡主要是檢查收單系統中的所有訂單是否都出現在派發資料庫裡。這項檢查由打上了MAIN標籤的for迴圈來做。

還有另外一種可能。比如有個實習生通過管理介面做了些測試訂單(他沒意識到這些訂單用的是生產系統)。這樣訂單資料會出現在派發資料庫裡,但不會出現在收單系統中。

為了處理這種特殊情況,還需要一個迴圈。這個迴圈要檢查所見到的集合(同時出現在兩個系統中的交易)是否包含了資料庫中的全部記錄。它還會確認那些遺漏項。下面是這個樣例的一部分輸出:

7172329 OK
1R6GV OK
1R6GW OK
main_ref: 1R6H2 not present in DB
main_ref: 1R6H3 not present in DB
1R6H6 OK

哪兒出錯了?原來是上游系統不區分大小寫而下游系統區分,在派發資料庫裡表示為1R6H12的記錄實際上是1r6h2。

如果你檢查一下程式碼清單7-1,就會發現問題出在contains()方法上。contains()方法會檢查其引數是否出現在目標集合中,只有完全匹配時才會返回true

也就是說其實你應該用containsCaseInsensitive()方法,可這是一個根本就不存在的方法!所以你必須把下面這段程式碼

if (dbIds.contains(pTradeRef)) {
  System.out.println(pTradeRef +" OK");
  seen.add(pTradeRef);
} else {
  System.out.println("main_ref: "+ pTradeRef +" not present in DB");
}

換成這樣的迴圈:

for (String id : dbIds) {
  if (id.equalsIgnoreCase(pTradeRef)) {
    System.out.println(pTradeRef +" OK");
    seen.add(pTradeRef);
    continue MAIN;
  }
}
System.out.println("main_ref: "+ pTradeRef +" not present in DB");

這看起來比較笨重。只能在集合上執行迴圈操作,不能把它當成一個整體來處理。程式碼既不簡潔,又似乎很脆弱。

隨著應用程式逐漸變大,簡潔會變得越來越重要——為了節約腦力,你需要簡潔的程式碼。

函數語言程式設計的基本原理

希望上面的例子中的兩個觀點引起了你的注意。

  • 將集合作為一個整體處理要比迴圈遍歷集合中的內容更簡潔,通常也會更好。
  • 如果能在物件的現有方法上加一點點邏輯來調整它的行為是不是很棒呢?

如果你遇到過那種基本就是你需要,但又稍微差點兒意思的集合處理方法,你就明白不得不再寫一個方法是多麼沮喪了,而函數語言程式設計(FP)恰好搔到了這個癢處。

換種說法,簡潔(並且安全)的物件導向程式碼的主要限制就是,不能在現有方法上新增額外的邏輯。這將我們引向了FP的大思路:假定確實有辦法向方法中新增自己的程式碼來調整它的功能。

這意味著什麼?要在已經固定的程式碼中新增自己的處理邏輯,就需要把程式碼塊作為引數傳到方法中。下面這種程式碼才是我們真正想要的(為了突出,我們把這個特殊的contains()方法加粗了):

if (dbIds.contains(pTradeRef, matchFunction)) {
  System.out.println(pTradeRef +" OK");
  seen.add(pTradeRef);
} else {
  System.out.println("main_ref: "+ pTradeRef +" not present in DB");
}

如果能這樣寫,contains()方法就能做任何檢查,比如匹配區分大小寫。這需要能把匹配函式表示成值,即能把一段程式碼寫成“函式字面值”並賦值給一個變數。

函數語言程式設計要把邏輯(一般是方法)表示成值。這是FP的核心思想,我們還會再次討論,先看一個帶點兒FP思想的Java例子。

對映與過濾器

我們把例子稍微展開一些,並放在呼叫reconcile()的上下文中:

reconcile(sourceData, new HashSet<String>(extractPrimaryKeys(dbInfos)));

private List<String> extractPrimaryKeys(List<DBInfo> dbInfos) {
  List<String> out = new ArrayList<>();
  for (DBInfo tinfo : dbInfos) {
    out.add(tinfo.primary_key);
  }

  return out;
}

extractPrimaryKeys()方法返回從資料庫物件中取出的主鍵值(字串)列表。FP粉管這叫map()表示式:extractPrimaryKeys()方法按順序處理List中的每個元素,然後再返回一個List。上面的程式碼構建並返回了一個新列表。

注意,返回的List中元素的型別(String)可能跟輸入的List中元素的型別(DBInfo)不同,並且原始列表不會受到任何影響。

這就是“函數語言程式設計”名稱的由來,函式的行為跟數學函式一樣。函式f(x)=x*x不會改變輸入值2,只會返回一個不同的值4。

便宜的優化技巧
呼叫reconcile()時,有個實用但小有難度的技巧:把extractPrimaryKeys()返回的List傳入HashSet構造方法中,變成Set。這樣可以去掉List中的重複元素,reconcile()方法呼叫的contains()可以少做一些工作。

map()是經典的FP慣用語。它經常和另一個知名模式成對出現:filter()形態,請看程式碼清單7-2。

程式碼清單7-2 過濾器形態

List<Map<String, String>> filterCancels(List<Map<String, String>> in) {
  List<Map<String, String>> out = new ArrayList<>();  //防禦性複製
  for (Map<String, String> msg : in) {
    if (!msg.get("status").equalsIgnoreCase("CANCELLED")) {
        out.add(msg);
    }
  }

  return out;
} 

注意其中的防禦性複製,它的意思是我們返回了一個新的List例項。這段程式碼沒有修改原有的Listfilter()的行為跟數學函式一樣)。它用一個函式測試每個元素,根據函式返回的boolean值構建新的List。如果測試結果為true,就把這個元素新增到輸出List中。

為了使用過濾器,還需要一個函式來判斷是否應該把某個元素包括在內。你可以把它想象成一個向每個元素提問問題的函式:“我應該允許你通過過濾器嗎?”

這種函式叫做謂詞函式(predicate function)。這裡有一個用虛擬碼(幾乎就是Scala)編寫的方法:

(msg) -> { !msg.get("status").equalsIgnoreCase("CANCELLED") }

這個函式接受一個引數(msg)並返回boolean值。如果msg被取消了,它會返回false,否則返回true。用在過濾器中時,它會過濾掉所有被取消的訊息。

這就是你想要的。在呼叫整合程式碼之前,你需要移除所有被取消的訂單,因為被取消的訂單不會出現在派發資料庫中。

事實上, Java 8準備採用這種寫法(受到了Scala和C#語法的強烈影響)。我們在第14章還會討論這個主題,但在那之前我們會遇到幾次函式字面值(也稱為lambda表示式)。

我們接著往下看,討論一下其他情況,從JVM上可用的語言型別開始(有時候我們也把這稱為語言生態學)。

本文摘自即將上市的《Java程式設計師修煉之道》 預計7月15號可上市。 噹噹、京東、互動均在預售。

enter image description here

關於本書封面

本書封面上的畫像標題為“賣花人”,摘自19世紀法國出版的沙利文•馬雷夏爾(Sylvain Maréchal)四卷本的地域服飾風俗綱要。其中每幅插圖都是手工精心繪製並上色的。馬雷夏爾這套書展示的豐富服飾,令我們強烈感受到200年前的鄉村及城鎮的巨大文化差異。不同地域的人山水阻隔,言語不通。無論奔走於街巷,還是駐足於鄉間,通過他們的服飾,一眼就能看出他們的生活場所、職業,以及生活境況。

時過境遷,書中描繪的那些區域性服飾差異到如今已經不復存在。即使是不同國家,都很難再看出人們著裝的區別,再不必說市鎮和鄉村了。或許,我們今天多姿多彩的人生,正是從前那些文化差異的體現。只不過,如今的生活更加多元,而且技術環境下的生活節奏也要快得多。

今時今日,計算機圖書層出不窮,Manning就以馬雷夏爾這套書中多樣性的圖片,來表達對IT行業日新月異的發明與創造的讚美。

相關文章