Clojure 與Java對比少數據結構多函數勝過多個單獨類的優點

前言:

在Clojure中,我們一次又一次地使用相同的數據結構,並在其上運行許多函數。另一方面,Java程序員為每一組數據創建一個唯一的類,並使用自己的“API”(getter、setter、return type等)來訪問和操作數據。由於被迫在兩個這樣的“類API”之間進行翻譯,我想與大傢分享我的經驗,從而在實踐中證明格言中的真理

請註意,本文談論的是數據和數據承載類,而不是“業務邏輯”,它將由Java中所述對象上的方法和Clojure中命名空間中的函數(最好是純函數)實現。

註意:本文會交替使用Java和Groovy,因為它們基本相同;本文所說的一個也適用於另一個。

問題所在

我一直在寫一個代理,接收javax.servlet.http.HttpServletRequest 並通過Apache HttpClientorg.apache.http.client.methods.HttpUriRequest,然後從org.apache.http.HttpResponsejavax.servlet.http.HttpServletResponse,尤其是關於(一個子集)頭的響應。

這是一件痛苦的事,因為每個人都有自己的頭表示和使用headers的API:

// javax.servlet.http.HttpServletRequest:
Enumeration<String> getHeaderNames();
/** Returns all the values of the specified request
    header as an Enumeration of String objects. */
Enumeration<String> getHeaders(String name);

// org.apache.http.client.methods.RequestBuilder:
/** Add a header; repeat to add multiple values */
RequestBuilder addHeader(String name, String value);

//-------------
// javax.servlet.http.HttpServletResponse:
/** Add a header; repeat to add multiple values */
void addHeader(String name, String value);

// org.apache.http.HttpResponse:
Header[] getAllHeaders();
// Header:
String getName();
String getValue();

這裡,枚舉和數組是通用的數據結構,但頭和請求對getHeaderNamesgetHeaders的拆分需要特定的代碼。

因此,我必須編寫translation函數,如:

def copyRequestHeaders(HttpServletRequest source, RequestBuilder target) {
    source.getHeaderNames().each { String hdr ->
        source.getHeaders(hdr).each { String val ->
            if (undesirable(hdr)) return
            target.addHeader(hdr, val)
        }
    }
}

static void copyResponseHeaders(HttpResponse source, HttpServletResponse target) {
    source.allHeaders.each { Header hdr ->
        if (target.getHeader(hdr.name.toLowerCase()) == hdr.value) return // avoid duplicates
        if (undesirable(hdr.name)) return
        target.addHeader(hdr.name, hdr.value)
    }
}

理想情況下,我希望能夠像target這樣做target.request.headers = omitKeys(undesirable, source.request.headers)。但這是不可能的,我必須從一組類型映射到另一組類型。這裡的主要問題是servlet請求被拆分為getHeaderNamesgetHeaders,而不是返回例如Map<String,String[]>,還有RequestBuilder,它有addHeader,但無法一次添加所有頭(除非我們首先將它們包裝在其域類中,即Header中)。

(可以說,我可以找到一個更好的例子來說明這一點。在這裡,我們仍然主要(但不總是)使用枚舉、字符串、數組等基元/泛型類型,而不是嵌套的自定義類型層次結構。)

Clojure解決方案

在Clojure中,請求隻是一個映射,標題很可能是列表的映射。即使這兩個庫(服務器、客戶端)在密鑰名稱或數據結構上不一致,也沒有“API”可學習-您隻需使用相同的舊已知函數從一個數據結構轉換到另一個數據結構,這是您在每個Clojure項目、web、數據或任何其他領域中所做的事情。唯一改變的是地圖中關鍵點的名稱。

註意:如果您不知道Clojure,那麼一些示例可能很難閱讀,例如assoc和reduce-kv (key-value)函數以及偶爾的單字母名稱。請記住,Clojure程序員反復使用相同的100個函數,並且非常熟悉它們。與其他一些語言相反,Clojure有意識地選擇為有經驗的開發人員進行優化。這對我來說很好。

案例1:相同的Keys

最簡單的情況是,使用相同的key,我們隻想選擇一個子集:

(assoc
  target-request
  :headers
  (select-keys (:headers source-request) [:pragma :content-type ...]))

唯一區分大小寫的部分是keys。在Java中,您不能像我們在這裡使用通用選擇鍵那樣一次選擇所有所需的keys,您需要通過類特定的getHeaders(name)逐個選擇它們。

案例2:不同的Key名,相同的數據結構

(assoc
  target-request
  :headersX
  (clojure.set/rename-keys
    (select-keys (:headersY source-request) [:Pragma :ContentType ...])
    {:Pragma :pragma, :ContentType :content-type}))

如果需要更復雜的key轉換,我們可以使用例如map:

(defn transform-key [k] ...)
(let [hdrs (->> (select-keys headers [:a])
                (map (fn [[k v]] [(transform-key k) v]))
                (into {}))]
    (assoc target-request :headersX hdrs))

關鍵是,在從一個數據結構映射到另一個數據結構的過程中,我們仍然使用我們所知道和喜愛的相同功能,唯一針對具體情況的部分是鍵和鍵轉換函數。我們可以簡單地映射頭映射,這在HttpServletRequest的頭上是不可能的。

案例3:不同的數據結構

headers作為name-value對列表(可能有重復的名稱)進入name-value映射:

(def headers-in [["pragma" "no-cache"] ["accept" "X"] ["accept" "Y"]])
(->> headers-in
     (group-by first)
     (reduce-kv
       (fn [m k vs]
         (assoc
           m
           k
           (map second vs)))
       {}))
; => {"pragma" ("no-cache"), "accept" ("X" "Y")}

案例4:Reality

實際上,我們可能會使用Ring作為服務器,並將Clojure包裝器clj-http用於Apache HttpClient。

請求如下所示:

{:headers {"accept" "x,y", "pragma" "no-cache"}}

(我們可以添加ring-request-headers-middleware,將連接的值轉換為單個值的列表。)

Clj-http遵循Ring規范,因此支持相同的格式,但更為寬松:

clj http對頭的處理比ring規范指定的要寬松一些。

clj http允許任何大小寫的字符串或關鍵字,而不是強制所有請求頭都是小寫字符串。關鍵字將轉換為它們的規范表示形式,因此:content-md5標頭將作為“content-md5”發送到服務器。但是,請求頭中的字符串鍵將被發送到服務器,其大小寫保持不變。

響應標題可以作為任何大小寫的關鍵字或字符串讀取。如果服務器以“Date”標頭響應,則可以訪問該標頭的值,如:Date、“Date”、“Date”等。

這就是上面第1種情況。

Java Vs Clojure

我想指出的一點是,Clojure在解決兩個問題方面更為有效:數據選擇和轉換,這要歸功於對其使用通用數據結構和函數。

選擇

在Clojure中,通過選擇另一個映射的子集來創建映射非常簡單(assoc將鍵與值關聯,select keys返回映射):

(assoc
  request
  :headers
  (select-keys
    (:headers other-request)
    [:pragma ...]))

使用典型的Java數據類(還記得DTOs嗎?)您需要逐個獲取和設置各個屬性。即使我們使用Groovy便利:

new Person(
  firstName: employee.firstName,
  lastName: employee.lastName,
  ...)

這裡的重點並不是鍵入的數量,而是在Clojure中,我們可以使用現有函數(並將它們組合成新的可重用函數)來完成這項工作,而在Java中,您必須編寫(更多)自定義的一次性代碼。(或者使用映射器庫、註釋和其他黑魔法:-))

轉換

如上所述,在Clojure中,將頭從一個請求復制到另一個請求是微不足道的。在典型的Java中,標頭將由它們自己的類型(可能是標頭)表示,因此,即使它們在兩個庫中具有相同的形狀,它們仍然是不同的類型,我們需要從一種類型轉換為另一種類型:

// fake code <img src="https://javakk.com/wp-content/themes/Tint-master/images/smilies/icon_smile.gif" alt=":-)" />
def toClientHdr(servlet.Header hdr) {
  return new httpclient.Header(
    name: hdr.name,
    values: hdr.values)
}
clientRequest.headers =
  servletRequest.headers
    .map(toClientHdr)

在Clojure中,toClientHdr是不必要的,因為我們隻有映射,沒有要從/映射到的類型。我們在這裡的前提是,數據的“形狀”在兩端都是相同的,但即使不是,也更容易從一個轉換到另一個,因為數據轉換是FP的主要優勢之一,尤其是Clojure。核心庫中有許多有用的數據選擇和轉換功能,旨在以多種強大的方式進行組合。

驗證、封裝?

即使您同意使用一些具有強大功能的通用數據結構比將數據包裝在類型中更有效,您也可能會擔心類的其他好處,例如封裝和數據驗證。這超出瞭本文的范圍,但請確保FP/Clojure具有滿足這些需求的解決方案,盡管它們明顯不同於OOP。

結論

Clojure在任何地方都使用相同的少數數據結構(map、set、list、vector),並具有許多操作這些結構的函數(許多函數如map on all,一些函數如select key only on some)。最終,您將非常熟練地使用這些功能以及將它們結合起來以實現您想要的任何功能的方法。

Java開發人員必須為每個類學習一個新的“數據訪問API”,並進行大量的手動翻譯。她在一節課上學到的東西在另一節課上通常是無用的。

Clojure方法似乎更有成效。但它超越瞭開發人員的生產力。所有Clojure庫都使用相同的少數通用數據結構,因此可以編寫同樣通用的實用程序庫來處理數據,如Specter或Balagan,這些數據可以用於Ring請求、Hiccup HTML表示、“來自後端服務的json”數據以及其他任何數據。

到此這篇關於Clojure 與Java對比少數據結構多函數勝過多個單獨類的優點的文章就介紹到這瞭,更多相關Clojure 與 Java 內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: