Clojure 是一种出色的编程语言,因为它具有功能性、缺乏对象/对原始值的关注以及通过其无缝 Java 互操作提供的庞大 JVM 生态系统 与其他编程语言相比,Clojure 工程师的招聘和构建工程团队具有挑战性缺乏知名度,也缺乏大量经验丰富的工程师 在主要与 Ruby 合作多年后,我来到了 Nanit。那时我并不真正了解 Clojure,所以在我的第一阶段,我主要做 Ruby 工作以提供快速价值。 Nanit 的研发高级副总裁 Chen 已经在 Clojure 中实现了一些服务,这就是我如何将 Clojure 作为一种语言引入的。从那以后 6 年多过去了,今天 Clojure 是我工具箱中最强大的工具之一,也是我觉得最高效的语言。在这些年里,Nanit 的后端团队变得越来越大,关于选择 Clojure 作为我们主要编程语言的问题一再出现,这主要是因为缺乏经验丰富的 Clojure 工程师,这影响了招聘并引入了更长的入职流程,直到新工程师能够工作为止。当我试图为这个问题提供答案时,我总是觉得我必须回忆我的想法并将它们组织成连贯的论点,尽管我一直很清楚 Clojure 的优势。我决定有一天我会在一篇博客文章中表达我对 Clojure 的看法。这一天已经到来:) 我总是喜欢说,尽管我已经使用 Clojure 工作了五年多,但我绝不是“Clojure 专家”。因为我认为自己是一个倾向于深入研究主题的人,所以我认为这更多地反映了 Clojure 作为一种语言而不是作为软件工程师的我。只是与其他编程语言相比,Clojure 相当简单,简化一个主题,使专业知识变得深奥。换句话说,Clojure 允许您以很少的知识实现很多,因为要知道的并不多,这真的很棒。不应将简单与软弱混淆。相反,Clojure 的简单性是它的主要优势,因为您可以实现使用其他语言(如 Ruby、Java 或 Python)所能实现的一切,而代码中的开销和意外复杂性更少。我想尽量避免“语言战争”,得出一个绝对的结论,即 Clojure 是地球上最好的语言。 Clojure 是我工具箱中的另一个工具,可能比其他用例更适合某些用例。相反,我将尝试列出使我在使用 Clojure 时更轻松的客观参数以及一些我在使用 Clojure 作为语言在技术上遇到困难的主题,以及构建一个主要将 Clojure 作为他们的工具实践的软件工程师团队。
Clojure 是一种函数式编程 (FP) 语言。对我来说,作为一个软件开发者,FP 最大的优势是大部分代码库都是由“纯函数”组成的。纯函数有两个特性,使它们更容易测试、重构和组合成更复杂的函数:它们没有副作用。副作用包括网络 IO、磁盘交互或改变系统状态。他们的输出完全依赖于他们的论点。它们不依赖于外部状态来计算它们的返回值。当我想到我如何花时间创建软件时,我可以将其分为 4 个主要活动: 我阅读现有代码并尝试理解它 我重构需要重构的代码 我在编写新代码之前设计新代码 我用测试编写新代码— 此代码可能重用了现有代码 上述两个特征的组合使我更轻松地执行任何列出的活动: 纯函数使代码设计更容易:事实上,当您的代码库主要由纯函数组成时,几乎没有什么设计要做功能。您不必使用接口、扩展和实现来构建类层次结构。不需要像继承上的组合或访问者模式这样的高级设计技巧。您不必为多重继承问题或可怕的菱形图找到创造性的解决方案。在过去的 6 年里,我没有处理过任何这些问题,但我编写了精心制作、经过测试、可维护、可读、可扩展的生产级代码(或者至少这是我愿意相信的 :))。纯函数更容易重用:我可以根据需要多次使用纯函数,而不必考虑它如何影响系统,因为没有任何副作用。这就像计算机编程的 WYSIWYG——函数遵循它的主体而不是其他任何东西。无需考虑任何隐藏的考虑因素。纯函数通过消除必须调查我要重用的代码是否会影响系统以及如果是的话会产生什么影响的额外开销来鼓励代码重用。
纯函数更易于阅读和理解:每个纯函数都是一段孤立的、一致的和可预测的代码,仅依赖于其参数。您不需要熟悉数据库架构或 RabbitMQ 架构来推理代码——这完全是关于在函数体中完成的参数和数据转换。纯函数更容易测试:因为它们不依赖于外部状态,所以你测试函数所要做的就是将它应用到它的参数上。无需在数据库上创建夹具或模拟 HTTP 请求。此外,由于纯函数不会对系统应用任何更改,因此您只需测试返回值。纯函数更容易重构:它们缺乏外部依赖和无状态,将它们变成了一个独立的构建块,易于替换和组合。 Clojure 没有“对象”。我的意思是,确实如此,但大多数时候你不会觉得需要这些。相反,Clojure 依赖于原始值和它们的集合(数组、字典、集合等)。我在 Clojure 中所做的 99% 都是使用包含原始值的数组和字典。我的代码侧重于业务逻辑和数据转换,而不是描述域及其关系。每行代码都在执行业务逻辑,因此业务逻辑在整个代码库中非常突出。我不必熟悉数百个独特的对象和编码到它们中的行为才能有效:传入的 HTTP 请求?它是一个普通的 Clojure 字典。你想形成一个 SQL 查询?构建一个字典并将其传递给 SQL 库进行格式化。您想返回 HTTP 响应吗?您返回一个包含状态码、标题和正文键的字典。想要从 RabbitMQ 队列中读取消息?是的,你猜对了——你得到了一本字典。如果您熟悉 Clojure 对其基本数据结构(如字典)的操作,您将在 HTTP、SQL、RabbitMQ 和系统的每个其他特定领域部分中变得有效。它将域中所需的复杂性和熟悉程度降低到最低要求,因为从软件方面来看,您所做的只是重复构建、转换和移动字典从一个功能到另一个功能。 Clojure 的语法建立在它自己的数据类型之外。这种特性称为同质性。起初听起来很奇怪,但我会尝试证明:
Clojure 向量(其他语言中的数组)看起来像这样 [1 2 3 4] Clojure 列表看起来像这样: (1 2 3 4) 如您所见,代码是一个 Clojure 列表,其中包含符号 defn、函数名称,然后是参数向量。主体是一个列表,其中函数作为第一个成员 (+),后面是参数。通过宏生成代码感觉很自然。由于我们在 Clojure 中所做的大部分工作是转换和生成有利于业务逻辑的数据结构,因此使用相同的数据结构执行相同的操作来生成代码几乎不会引起注意。它将您必须熟悉的特殊符号和字符的数量减少到最少。代码和数据合二为一,因为它们共享相同的数据结构、行为和语法。 Clojure 的大多数值都是不可变的,这可以防止竞争条件并允许代码不受互斥锁和锁等共享访问控制的影响。那些不是一成不变的(例如原子)提供了操作它们存储的数据的安全方法。 Clojure 有大量用于并发编程的工具,称为 clojure.async。至少从我的经验来看,这些工具的亮点是 Channels,它允许在一组通道上进行安全的线程间通信和选择,就像 Golang 的 select 指令一样。 Clojure 不是一种广泛使用的编程语言,因此,常见用例缺少许多库。幸运的是,Clojure 与 Java 的互操作是无缝的,因此在实践中,Java 的庞大生态系统触手可及。通过这种方式,您可以享受使用 Clojure 的乐趣,但不会受到其缺乏流行度和库的影响。
是的,Clojure 很棒,但就像我们在生活中做出的大多数决定一样,使用 Clojure 做出的决定也是权衡取舍。 Clojure 的第一个方面是 JVM,这有 3 个原因: JVM 是一个众所周知的内存吞噬者,很难预测您的应用程序内存需求。此外,它似乎总是需要比运行应用程序所需的更多的内存。我确信相同的应用程序在其他运行时会占用更少的内存(尽管我从未花时间证明这一点)。调试远程服务器中的内存泄漏和堆大小非常困难。我们尝试了 VisualVM,但由于 Clojure 内存主要由原语(字符串、整数等)组成,因此很难理解正在累积应用程序的哪些数据以及原因。我假设在基于 Java 的常见应用程序中,大部分内存由 Java 对象组成,因此内存分析会更容易。随着项目规模的增长,Clojure 项目的启动时间可能会变得很长。尽管有 GraalVM 之类的解决方案,但我还没有机会在生产中体验它们以证明它们的成熟度和健壮性。总而言之,我不是 JVM 的粉丝,但我确实理解将 Clojure 的运行时定位到 JVM 的决定背后的原因。在大型的、不熟悉的 Clojure 代码库中工作时,我发现困难的第二个主题是打字。 Clojure 是一种动态语言,它有它的优点,但当我偶然发现一个接收字典参数的函数时,我发现自己花了很多时间来找出它拥有哪些键。有时我不得不在我们的集成环境中放置一个日志,以查看它接收到什么消息以及该消息中有哪些字段可供我使用。有时我会去测试该函数并查找我们在测试中使用的示例参数值,但这可能还不够,因为该字典中可能存在其他字段并且只是未在函数中使用时刻,因此它们也可能从测试值中丢失。有时我会查看函数的调用站点以了解传递了什么参数以及它是如何构建的。也有解决方案,例如 core.typed,但我自己从未体验过它们,我不确定它们的全面性和可用性。
使用 Clojure 的最后一件事是招聘和入职,我已经在这篇文章的前面提到过。招聘很难,因为现有的 Clojure 工程师人数很少,而且一些工程师出于职业发展的考虑故意避免使用不受欢迎的语言。其他工程师获得了特定语言的专业知识,并希望继续使用这些语言,因此 Clojure 不是他们的选择。入职还需要更多的关注和指导,因为大多数工程师对 Clojure 及其生态系统几乎一无所知。当 NodeJS 工程师加入公司时,他们已经知道 javascript,他们熟悉生态系统,他们有自己喜欢的 IDE 和插件,他们知道哪些工具可以使他们的本地开发环境尽可能高效。当工程师在没有先验知识的情况下加入使用 Clojure 的组织时,他们必须学习该语言,找到他们熟悉的 IDE,适应新的开发流程并配置他们的开发环境以提高生产力。这几乎就像重新学习系鞋带一样,并且必须从现有工程师那里获得适当的指导和可用性。 Clojure 缺乏普及引入的另一个有趣问题是,新工程师很难将 Clojure 特定知识从外部引入公司并丰富现有团队。再次回到 NodeJS 示例,加入新团队的具有丰富经验的工程师可能会引入新的工具/库/工作方法/开发流程,他们在之前的公司中获得了专业知识。没有特定领域经验的工程师无法真正以同样的方式丰富团队,因此团队必须依靠自我学习和改进,而不是从外部带来知识。我认为每个软件工程师至少需要让他们自己熟悉一种函数式编程语言,才能敞开心扉,看到 OOP 范式之外的东西。学习 Clojure 让我怀疑我以前作为软件工程师实践过的一切,并就我如何在正确的方向上花费精力来为我工作的公司提供价值的基本知识提出问题。我认为 Clojure 作为一种成熟的、可用于生产的、简单的编程语言,非常适合进行这种探索。您可以选择专业地使用它,用于业余项目或根本不使用它,但是让自己接触这种语言的经验肯定会丰富您对编程的看法并使您成为更好的开发人员。