6 年專業Clojure經驗分享 - Erez

banq發表於2021-08-15

  • Clojure 是一種很棒的程式語言,因為它具有函式性、缺乏物件/對原始值的關注以及透過其無縫 Java 互操作提供的龐大 JVM 生態系統
  • 與其他程式語言相比,Clojure 工程師的招聘和構建工程團隊具有挑戰性,因為它不受歡迎,並且缺乏大量經驗豐富的工程師

在主要與 Ruby 合作多年後,我來到了Nanit。那時我並不真正瞭解 Clojure,所以在我的第一階段,我主要做 Ruby 工作以提供快速價值。從那以後 6 年多過去了,今天 Clojure 是我工具箱中最強大的工具之一,也是我覺得最高效的語言。
在這些年裡,Nanit 的後端團隊變得越來越大,關於選擇 Clojure 作為我們主要程式語言的問題一再出現,這主要是因為缺乏經驗豐富的 Clojure 工程師。
當我試圖為這個問題提供答案時,我總是覺得我必須回憶我的想法並將它們組織成連貫的論點,儘管 Clojure 的優勢一直對我來說非常清楚。我決定有一天我會在一篇部落格文章中表達我對 Clojure 的看法。這一天到了:)
我總是喜歡說,儘管我已經與 Clojure 一起工作了五年多,但我絕不是“Clojure 專家”。因為我認為自己是一個傾向於深入研究主題的人,所以我認為這更多地反映了 Clojure 作為一種語言而不是作為軟體工程師的我。只是與其他程式語言相比,Clojure 相當簡單,簡化一個主題,使專業知識變得深奧。換句話說,Clojure 允許您以很少的知識實現很多,因為要知道的並不多,這真的很棒。
不應將簡單與軟弱混淆。相反,Clojure 的簡單性是它的主要優勢,因為您可以實現使用其他語言(如 Ruby、Java 或 Python)所能實現的一切,而程式碼中的開銷和意外複雜性更少。
我想盡量避免“語言戰爭”,得出一個絕對的結論,即 Clojure 是地球上最好的語言。Clojure 是我工具箱中的另一個工具,可能比其他用例更適合某些用例。相反,我將嘗試列出使我在使用 Clojure 時更輕鬆的客觀引數,以及一些我在使用 Clojure 作為語言在技術上遇到困難的主題,以及構建一個主要將 Clojure 作為他們的工具實踐的軟體工程師團隊。
 

函數語言程式設計
Clojure 是一種函數語言程式設計 (FP) 語言。對我來說,作為一個軟體開發者,FPs 最大的優勢就是大部分的程式碼庫都是由“純函式”組成的。純函式有兩個特性,使它們更容易測試、重構和組合成更復雜的函式:

  1. 它們沒有副作用。副作用包括網路 IO、磁碟互動或改變系統狀態。
  2. 他們的輸出完全依賴於他們的論點。它們不依賴於外部狀態來計算它們的返回值。

我建立軟體分為4個主要活動:
  • 我讀了現有的程式碼,並試圖瞭解它
  • 我重構,需要重構程式碼
  • 在寫新的程式碼之前設計
  • 我寫新程式碼的測試— 此程式碼可能重複使用現有程式碼

上述兩個特徵的組合使我更容易列出任何列出的活動:
  1. 純函式使程式碼設計更容易:事實上,當您的程式碼庫主要由純函式組成時,幾乎不需要進行任何設計。您不必使用介面、擴充套件和實現來構建類層次結構。不需要像繼承上的組合或訪問者模式這樣的高階設計技巧。您不必為多重繼承問題或可怕的菱形圖找到創造性的解決方案。在過去的 6 年裡,我沒有處理過任何這些問題,但我編寫了精心製作、經過測試、可維護、可讀、可擴充套件的生產級程式碼(或者至少這是我願意相信的 :))。
  2. 純函式更易於重用:我可以根據需要多次使用純函式,而無需考慮它如何影響系統,因為沒有任何副作用。這就像計算機程式設計的 WYSIWYG——函式遵循它的主體而不是其他任何東西。無需考慮任何隱藏的考慮因素。純函式透過消除必須調查我要重用的程式碼是否會影響系統以及如果是的話會產生什麼影響的額外開銷來鼓勵程式碼重用。
  3. 純函式更易於閱讀 和 理解:每個純函式都是一段孤立的、一致的和可預測的程式碼,僅依賴於其引數。您不需要熟悉資料庫模式或 RabbitMQ 架構來推理程式碼——這完全是關於在函式體中完成的引數和資料轉換。
  4. 純函式更容易測試:由於它們不依賴於外部狀態,因此您測試函式所需要做的就是將其應用於其引數。無需在資料庫上建立夾具或模擬 HTTP 請求。此外,由於純函式不會對系統應用任何更改,因此您只需測試返回值。
  5. 純函式更容易重構:它們缺乏外部依賴和無狀態,將它們變成了一個獨立的構建塊,易於替換和組合。

 

只有值,只有基元
Clojure 沒有“物件”。我的意思是,確實如此,但大多數時候你不會覺得需要這些。相反,Clojure 依賴於原始值和它們的集合(陣列、字典、集合等)。我在 Clojure 中所做的 99% 都是使用包含原始值的陣列和字典。
作為一名軟體工程師,處理原始值對我來說更容易:

  1. 我的程式碼側重於業務邏輯和資料轉換,而不是描述域及其關係。每行程式碼都在執行業務邏輯,因此業務邏輯在整個程式碼庫中非常突出。
  2. 我不必熟悉數百個獨特的物件和編碼到它們中的行為才能有效:傳入的 HTTP 請求?它是一個普通的 Clojure 字典。你想形成一個 SQL 查詢?構建一個字典並將其傳遞給 SQL 庫進行格式化。您想返回 HTTP 響應嗎?您返回一個包含狀態碼、標題和正文鍵的字典。想要從 RabbitMQ 佇列中讀取訊息?是的,你猜對了——你得到了一本字典。如果您熟悉 Clojure 對其基本資料結構(如字典)的操作,您將在 HTTP、SQL、RabbitMQ 和系統的每個其他特定領域部分中變得有效。它將域中所需的複雜性和熟悉程度降低到最低要求,因為從軟體方面來看,您所做的只是重複構建、轉換和移動字典從一個功能到另一個功能。
  3. 測試變得更加容易,因為我不必模擬複雜的物件或建立所需介面的特定於測試的實現來允許程式碼執行。我所要做的就是建立資料這始終是一個Clojure的或多個圖元的集合。

 

最少的語法
Clojure 的語法建立在它自己的資料型別之外。此屬性稱為同質性。起初聽起來很奇怪,但我會嘗試證明:
Clojure 向量(其他語言中的陣列)如下所示[1 2 3 4]Clojure 列表如下所示:(1 2 3 4)
要定義一個函式,你會寫:
(defn my-sum [arg1 arg2] (+ arg1 arg2))
如您所見,程式碼是一個 Clojure 列表,其中包含符號defn、函式名稱和引數向量。主體是一個列表,其中函式作為第一個成員 (+),後面是引數。
為什麼這是一件好事,你可能會問自己?好問題!

  1. 透過宏生成程式碼感覺很自然。由於我們在 Clojure 中所做的大部分工作都是為了有利於業務邏輯而轉換和生成資料結構,因此使用相同的資料結構執行相同的操作來生成程式碼幾乎不會引起注意。
  2. 它將您必須熟悉的特殊符號和字元的數量減少到最少。程式碼和資料合二為一,因為它們共享相同的資料結構、行為和語法。

 

併發
使用 Clojure 時,併發感覺不是問題,主要有兩個原因:

  1. Clojure 的大部分值都是不可變的,這可以防止競爭條件並允許程式碼不受互斥鎖和鎖等共享訪問控制的影響。那些不是一成不變的(例如原子)提供了操作它們儲存的資料的安全方法。
  2. Clojure 有大量用於併發程式設計的工具,稱為clojure.async。至少從我的經驗來看,這些工具的亮點是 Channels,它允許在一組通道上進行安全的執行緒間通訊和選擇,就像Golang 的 select 指令一樣

 

Java互操作
Clojure 不是一種廣泛使用的程式語言,因此,常見用例缺少許多庫。幸運的是,Clojure 與 Java 的互操作是無縫的,因此在實踐中,Java 的龐大生態系統觸手可及。透過這種方式,您可以享受使用 Clojure 的樂趣,但不會受到其缺乏流行度和庫的影響。
 

缺點
是的,Clojure 很棒,但就像我們在生活中做出的大多數決定一樣,使用 Clojure 做出的決定也是權衡取捨。
Clojure 的第一個方面讓我過得很艱難是 JVM,原因有以下 3 個:

  1. JVM 是一個眾所周知的記憶體吞噬者,很難預測您的應用程式記憶體需求。此外,它似乎總是需要比執行應用程式所需的更多記憶體。我確信相同的應用程式在其他執行時會佔用更少的記憶體(儘管我從未花時間證明這一點)。
  2. 除錯遠端伺服器中的記憶體洩漏和堆大小非常困難。我們嘗試了VisualVM,但由於 Clojure 記憶體主要由原語(字串、整數等)組成,因此很難理解正在累積應用程式的哪些資料以及原因。我假設在基於 Java 的常見應用程式中,大部分記憶體由 Java 物件組成,因此記憶體分析會更容易。
  3. 隨著專案規模的增長,Clojure 專案的啟動時間可能會變得很長。儘管有GraalVM 之類的解決方案,我還沒有機會在生產中體驗它們以證明它們的成熟度和健壯性。

總而言之,我不是 JVM 的粉絲,但我確實理解將 Clojure 的執行時定位到 JVM 的決定背後的原因。

在大型的、不熟悉的 Clojure 程式碼庫中工作時,我發現困難的第二個主題是Typing.
Clojure 是一種動態語言,它有它的優點,但當我偶然發現一個接收字典引數的函式時,我發現自己花了很多時間來找出它擁有哪些鍵。有時我不得不在我們的整合環境中放置一個日誌,以檢視它接收到什麼訊息以及該訊息中有哪些欄位可供我使用。
有時我會去測試該函式並查詢我們在測試中使用的示例引數值,但這可能還不夠,因為該字典中可能存在其他欄位並且只是未在函式中使用時刻,因此它們也可能從測試值中丟失。有時我會檢視函式的呼叫站點以瞭解傳遞了什麼引數以及它是如何構建的。
也有解決方案,例如core.typed,但我自己從未體驗過它們,我不確定它們的全面性和可用性。

使用 Clojure 的最後一件難事是招聘和入職,我已經在這篇文章的前面提到過。招聘很難,因為現有的 Clojure 工程師人數很少,而且一些工程師出於職業發展的考慮故意避免使用不受歡迎的語言。其他工程師獲得了特定語言的專業知識,並希望繼續使用這些語言,因此 Clojure 不是他們的選擇。
 

結論
我認為每個軟體工程師至少需要讓他們自己熟悉一種函數語言程式設計語言,才能敞開心扉,看看 OOP 正規化之外的東西。學習 Clojure 讓我懷疑我以前作為軟體工程師實踐過的一切,並就我如何將精力花在正確的方向上,為我工作的公司提供有價值的基礎上提出問題。
我認為 Clojure 作為一種成熟的、可用於生產的、簡單的程式語言,非常適合進行這種探索。您可以選擇專業地使用它,用於業餘專案或根本不使用它,但是讓自己接觸這種語言的經驗肯定會豐富您對程式設計的看法並使您成為更好的開發人員。
 

相關文章