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 getHeaderNames();
/** Returns all the values of the specified request
    header as an Enumeration of String objects. */
Enumeration 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,还有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 :-)
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 内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

你可能感兴趣的:(Clojure 与Java对比少数据结构多函数胜过多个单独类的优点)