Clojure 与Java对比少数据结构多函数胜过多个单独类的优点

目录
  • 问题所在
  • Clojure解决方案
    • 案例1:相同的Keys
    • 案例2:不同的Key名,相同的数据结构
    • 案例3:不同的数据结构
    • 案例4:Reality
  • Java Vs Clojure
    • 选择
    • 转换
    • 验证、封装?
  • 结论

前言:

在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,那么一些示例可能很难阅读,例如assocreduce-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 内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 了解java中的Clojure如何抽象并发性和共享状态

    前言 在所有 Java 下一代语言中,Clojure 拥有最激进的并发性机制和功能.Groovy 和 Scala 都为并发性提供了改善的抽象和语法糖的一种组合,而 Clojure 坚持了它始终在 JVM 上提供独一无二的行为的强硬立场.在本期 Java 下一代 中,我将介绍 Clojure 中众多并发性选项的一部分.首先是为 Clojure 中易变的引用提供支撑的基础抽象:epochal 时间模型. Epochal 事件模型 或许 Clojure 与其他语言最显著的区别与易变的状态和值 密切相关

  • Clojure 与Java对比少数据结构多函数胜过多个单独类的优点

    目录 问题所在 Clojure解决方案 案例1:相同的Keys 案例2:不同的Key名,相同的数据结构 案例3:不同的数据结构 案例4:Reality Java Vs Clojure 选择 转换 验证.封装? 结论 前言: 在Clojure中,我们一次又一次地使用相同的数据结构,并在其上运行许多函数.另一方面,Java程序员为每一组数据创建一个唯一的类,并使用自己的“API”(getter.setter.return type等)来访问和操作数据.由于被迫在两个这样的“类API”之间进行翻译,我

  • Java集合和数据结构排序实例详解

    目录 概念 插入排序 直接插入排序 代码实现 性能分析 希尔排序 代码实现 性能分析 选择排序 直接选择排序 代码实现 性能分析 堆排序 代码实现 性能分析 交换排序 冒泡排序 代码实现 性能分析 快速排序 代码实现 性能分析 非递归实现快速排序 代码实现 性能分析 归并排序 归并排序 代码实现 性能分析 非递归实现归并排序 代码实现 性能分析 海量数据的排序问题 总结 概念 排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作. 平时的上下文中,如果提到排序,通常

  • Java 精炼解读数据结构逻辑控制

    目录 一.顺序结构 二.分支结构 switch语句  三.循环结构 3.1while循环  3.2break 3.3continue  3.4for循环  3.5dowhile循环(选学)  总结: 一.顺序结构 程序的执行和代码的执行顺序有关,如果调整代码的书写顺序, 则执行顺序也发生变化 二.分支结构 基本语法形式1: if(布尔表达式){     //条件满足时执行代码 } 基本语法形式2 if(布尔表达式){     //条件满足时执行代码 }else{     //条件不满足时执行代码

  • 浅谈Java中常用数据结构的实现类 Collection和Map

    线性表,链表,哈希表是常用的数据结构,在进行Java开发时,JDK已经为我们提供了一系列相应的类来实现基本的数据结构.这些类均在java.util包中.本文试图通过简单的描述,向读者阐述各个类的作用以及如何正确使用这些类. Collection ├List │├LinkedList │├ArrayList │└Vector │ └Stack └Set Map ├Hashtable ├HashMap └WeakHashMap Collection接口 Collection是最基本的集合接口,一个C

  • java实现队列数据结构代码详解

    什么是队列结构 一种线性结构,具有特殊的运算法则[只能在一端(队头)删除,在另一端(队尾)插入]. 分类: 顺序队列结构 链式队列结构 基本操作: 入队列 出队列 给出一些应用队列的场景 1):当作业被送到打印机的时候,就可以按到达的顺序排起来,因此每一份作业是队列的节点. 2):售票口的人买票的顺序的按照先来先买的顺序售票. 3):当所有的终端被占用,由于资源有限,来访请求需要放在一个队列中等候. 队列是先进先出的! 我们设置一个叫做LinkQueue<T>的泛型集合类,该类里面有 Node

  • java编程队列数据结构代码示例

    队列是一种特殊的线性表,只允许在表的前端进行删除,在表的后端进行插入,表的前端称为(front)队头,表的后端称为(rear)队尾. 所以队列跟生活的场景很是相似,在电影院买电影票,人们排成一排,第一个人进入队尾最先到达队头后买票进入影院,后面排队的人按照排队的次序买到票后进入影院. 所以 队列是一种先进先出的数据结构(FIFO). 编程实现对循环链队列的入队和出队操作. ⑴根据输入的队列长度n和各元素值建立一个带头结点的循环链表表示的队列(循环链队列),并且只设一个尾指针来指向尾结点,然后输出

  • Java语言实现数据结构栈代码详解

    近来复习数据结构,自己动手实现了栈.栈是一种限制插入和删除只能在一个位置上的表.最基本的操作是进栈和出栈,因此,又被叫作"先进后出"表. 首先了解下栈的概念: 栈是限定仅在表头进行插入和删除操作的线性表.有时又叫LIFO(后进先出表).要搞清楚这个概念,首先要明白"栈"原来的意思,如此才能把握本质. "栈"者,存储货物或供旅客住宿的地方,可引申为仓库.中转站,所以引入到计算机领域里,就是指数据暂时存储的地方,所以才有进栈.出栈的说法. 实现方式是

  • Java 单链表数据结构的增删改查教程

    我就废话不多说了,大家还是直接看代码吧~ package 链表; /** * *1)单链表的插入.删除.查找操作: * 2)链表中存储的是int类型的数据: **/ public class SinglyLinkedList { private Node head = null; //查找操作 public Node findByValue(int value){ Node p = head; //从链表头部开始查找 while(p.next != null && p.data != va

  • Java最简洁数据结构之冒泡排序快速理解

    目录 一.什么是冒泡排序 二.图解冒泡排序 三.代码实现 四.代码的优化 1.整体的思路 2.代码示例 一.什么是冒泡排序 冒泡排序的英文是bubble sort,它是一种基础的交换排序.说到冒泡是不是就想起了快乐肥宅水呢?汽水中有许多小小的水泡哗啦哗啦的浮到上面来.这是因为组成小气泡的二氧化碳比水轻,所以小气泡可以一点一点地向上浮动. 而冒泡排序之所以叫冒泡排序,正是因为这种排序算法的每一个元素都可以像小气泡一样,根据自身的大小,一点一点的向着数组的一侧移动. 二.图解冒泡排序 我们先看一个例

  • Java实现链表数据结构的方法

    什么是链表? 链表是一种物理存储单元上非连续.非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针连接次序实现的.每一个链表都包含多个节点,节点又包含两个部分,一个是数据域(储存节点含有的信息),一个是引用域(储存下一个节点或者上一个节点的地址). 链表的理解示意图: 链表的特点是什么? 获取数据麻烦,需要遍历查找,比数组慢方便插入.删除 简单的链表的实现原理 创建一个节点类,其中节点类包含两个部分,第一个是数据域(你到时候要往节点里面储存的信息),第二个是引用域(相当于指针,单向链表有一个指

随机推荐