秀克利亞Clojure團隊開發
这篇文章是2019年Clojure Advent Calendar第18天的文章。我发现两年前我写的那篇关于如何快速开发Clojure的文章仍然有人在阅读,所以我决定更新信息,并将当前的观点整理出来。
当时的观点特别注重Clojure开发的速度,并认为它特别适合个人开发,可以在短时间内进行。然而,由于缺乏经验,我们纯属猜测,认为它可能不太适合中大型团队开发。然而,由于目前我们正在团队中使用Clojure进行开发,所以我将从这个角度写一下”使用Clojure进行团队开发如何?”的意见。
我和Clojure之后
自从2014年在上司的指导下开始学习Clojure,已经过去了五年。
-
- 2018
引き続きシリコンバレー駐在。
プロトタイプ開発などで許される限り Clojure/Script を使う。
Clojure/conj 2018 参加。
ミートアップで Clojure の紹介をしたり Clojure 勉強会をやったり草の根活動。
Clojure 転職活動開始。
2019
Clojure 転職成功。
帰国と同時に前職を退職。
毎日 Clojure を書いている。
最大的更新是成功转职至Clojure领域。我在日本和美国都进行了职业转换的尝试,但最终被旧金山的一家Fin Tech初创公司选中了,他们使用Clojure进行开发。
“关于转职活动,没有办法普遍适用到可以写出类似于’你在日本还没有尝试Clojure就消耗了吗?’这样挑衅性的文章的内容,当时是因为对未来的不安而变胖或变瘦。转职到美国企业有许多障碍,再加上要依靠运气的因素,依然不是一个轻而易举可以向别人推荐的事情。总之,我在与人的连接方面以及其他方面,都得到了 Clojure 的帮助。”
然而,作为一名 Clojure 工程师进行职业转换,比起 AI/Blockchain 工程师来说,这是一次成功的利基战略。而且,Clojure 相关的工作机会并不少。在职业转换过程中,我主要使用 LinkedIn 进行了搜索,虽然不确定该做什么,但我暂时发布了个人简历,结果接连不断地收到了只关于 Clojure 的私信,很快就能进行电话面试了。而且,即使是在大型企业中,也有公开招聘 Clojure 工程师的岗位。例如,某 A 公司就在招聘面向数据科学的 Clojure 工程师。
无论如何,我成功地度过了作为一个Clojurian的人生。目前因签证问题我在日本远程工作,但一旦签证批准我就会计划前往美国。
目前的Clojure技术堆栈
从独自开发原型到团队开发,技术堆栈并没有发生大的变化。如果非要说变化的话,就是现在更加依赖于Emacs作为编辑器,并且在构建工具方面转向了 Clojure CLI 和 shadow-cljs 等新的工具。顺便提一下,并不是团队中的每个人都使用Emacs。
-
- エディタ/ IDE
Emacs
CIDER
ビルドツール
Clojure CLI … 新しい Clojure ビルド CLI。tools.deps という新しい依存解決機構を持つ。
shadow-cljs … 新しい ClojureScript ビルドツール。npm パッケージを自然に利用できる。
leiningen
figwheelh
boot
その他ツール
eastwood
cljfmt
kibit
clj-kondo … 新しい Clojure リンター。コードをこんまりしてくれる。
joker … Golang 製の Clojure インタプリタ・リンター。高速に起動し Go の標準ライブラリが利用できる。
closh … Clojure が使える Shell。
フレームワーク
integrant … 最早外せないデータドリブンなモジュールベースフレームワーク。
duct … integrant でサーバサイドアプリを開発するためのフレームワーク。
lacinia … GraphQL の Clojure 実装。
ライブラリ
clojure.spec
umlaut … スキーマ定義言語。共通スキーマ定義から様々なスキーマ定義ファイルを生成できる。
jackdaw … Kafka クライアント、Kafka streams ライブラリ。Clojure らしい書き方で Kafka topology を定義できる。
clara-rules … ルールエンジン。ルールを Clojure コードで書け Immutable なスタイルで実行することが出来る。
ring
pedestal
aws-api … Cognitect 謹製の新しい AWS ライブラリ。API 定義から自動生成を行うことで AWS 側のバージョン変更にも追従。
timbre … ロギングライブラリ。
フロントエンド
reagent
re-frame … reagent 上で Redux 的アーキテクチャを実現するフレームワーク。
re-graph … re-frame のハンドラとして利用できる GraphQL クライアント。
garden
その他
Datomic On-Prem
Datomic Cloud
Metabase … データ可視化ツール。Clojure 製というだけではなく後述の Datomic Analytics により Datomic のデータ可視化にも利用できるようになった。
Clojure 適合的開發的特點是什麼?
我两年前的观点是,Clojure 的魅力在于利用 REPL 等生态系统进行快速开发,并且特别适合个人进行原型开发。然而,由于我个人在团队开发中对 Clojure 的经验较少,而且由于 Clojure 缺乏类型,所以在团队中使用它可能会面临一些困难。
因此,我加入了一家全面采用Clojure的公司,开始了我在未曾接触过的Clojure团队开发工作。总的来说,我并没有感到太大的压力。相反,我开始感受到Clojure具备支持高效团队开发的语言特性,因为它允许我们放弃类型限制。接下来,我将介绍一些相关特性。
1. 強制使用函数式的写法
首先,在使用Clojure进行开发时,函数式的写法是强制性的,因此与可以混合使用OOP的Java或Scala相比,个人之间的写作方式没有太大差异的感觉。因此,理解他人编写的代码的负担相对较少。此外,如果使用诸如duct等框架进行开发,则可以将函数的位置声明性地描述,从而进一步提高可读性。
Clojure 服务器端框架Duct 指南
2. 一切皆为数据 jiē
这个特性我在前篇文章中已经提到过,Clojure 的”一切皆为数据”非常适合团队开发。系统的内部数据几乎全部以EDN的形式表示,因此可以轻松共享配置值、模式定义、函数的输入输出等等的字符串片段。此外,Clojure 的代码本身也以数据的形式表示,可以轻松地提取和共享有意义的部分。共享的代码片段可以通过REPL输入,方便地重新模拟情景。
在可疑的地方事先输出数据日志,通过这种方法,在问题发生时可以简单地重现情况并确定原因,这种方法在团队中被广泛使用。我个人喜欢的输出方法是前面提到的一个名为 timbre 的日志库中的 spy 宏。taoensso.timbre/spy 可以记录并输出执行时指定的表达式解析为什么样的值,同时还会将该值直接返回。你可以将它插入线程宏中,或者将其包装在参数中,这样可以在不影响现有代码的情况下观察运行时的中间值。
(require '[taoensso.timbre :as log])
(def a {:a 1 :b "2"})
(log/spy a) ;; => {:a 1 :b "2"}
;; 19-12-18 12:15:11 Kazuki-MBP.local DEBUG [test-app.handler.example:22] - a => {:a 1, :b "2"}
(defn test-fn [arg]
(-> arg
log/spy
(assoc :id 1)
log/spy
(assoc :country "Japan")
log/spy))
(def data {:name "Kazuki"})
(test-fn (log/spy data)) ;; => {:name "Kazuki", :id 1, :country "Japan"}
;; 19-12-18 12:19:04 Kazuki-MBP.local DEBUG [test-app.handler.example:34] - data => {:name "Kazuki"}
;; 19-12-18 12:19:04 Kazuki-MBP.local DEBUG [test-app.handler.example:?] - arg => {:name "Kazuki"}
;; 19-12-18 12:19:04 Kazuki-MBP.local DEBUG [test-app.handler.example:?] - (assoc (log/spy arg) :id 1) => {:name "Kazuki", :id 1}
;; 19-12-18 12:19:04 Kazuki-MBP.local DEBUG [test-app.handler.example:?] - (assoc (log/spy (assoc (log/spy arg) :id 1)) :country "Japan") => {:name "Kazuki", :id 1, :country "Japan"}
如果您正在将系统作为一个完整项目进行开发,那么即使某些信息无法作为通常数据(例如DB连接等)处理,这些信息也可以通过一个名为system的映射来进行片段化管理。比如下面这个profile duct应用的例子。
{:duct.profile/base
{:duct.core/project-ns test-app
:duct.router/cascading
[#ig/ref [:test-app.handler/example]]
:test-app.handler/example
{:db #ig/ref :duct.database/sql}} ;; db 接続を利用する ring ハンドラ
:duct.profile/dev #duct/include "dev"
:duct.profile/local #duct/include "local"
:duct.profile/prod {}
:duct.module/logging {}
:duct.module.web/api
{}
:duct.module/sql ;; duct の db 接続モジュール。接続情報を :duct.database/sql に展開。
{}}
在dev环境中使用go启动REPL后,可以通过integrant.repl.state/system来查看系统状态。因此,可以将可以从REPL执行的包括数据库连接的函数执行片段化如下。接收方可以在同一项目中启动REPL并执行此片段。
(defn get-customer [db]
;;...
)
(def db (:duct.database/sql integrant.repl.state/system))
(get-customers db)
在Clojure中,可以轻松共享以可评估的单元为基础的代码片段,为团队开发发挥润滑剂的作用。
3. 共享模式定义
在Clojure中,如上所述,一切都可以表示为数据(EDN)。因此,许多由Clojure制作的库和框架通常使用EDN来定义模式。如果使用EDN,自动生成也很容易,存在着可以自动生成模式定义的库。其中一种典型的例子是之前提到的umlaut,我们的团队也在使用它。
使用umlaut,可以根据共同的模式定义生成Datomic模式、Lacinia模式、clojure.spec定义文件等,还可以通过编写生成器自己进行扩展。稍微有点不满意的是,共同定义模式不是以EDN格式存在。
@doc "Wrestler of Grand Sumo Tournament"
type Rikishi {
id: ID
shikona: String
banduke: String
heya: String
}
@lang/lacinia identifier query
type QueryRoot {
rihishi_by_id(id: ID?): Rishiki? {
@doc "Access a Rikishi by its unique id, if it exists."
@lang/lacinia resolver query_rikishi-by-id
}
}
可以根据上述通用定义模式生成Lacinia、Datomic等模式。
{:objects
{:Rikishi
{:fields
{:id {:type (non-null ID), :isDeprecated false},
:shikona {:type (non-null String), :isDeprecated false},
:banduke {:type (non-null String), :isDeprecated false}
:heya {:type (non-null String), :isDeprecated false}},
:implements [],
:description "Wrestler of Grand Sumo Tournament"}},
:enums {},
:interfaces {},
:mutations {},
:queries
{:rikishi_by_id
{:type :Rikishi,
:description "Access a Rikishi by its unique id, if it exists.",
:isDeprecated false,
:args {:id {:type ID}},
:resolve :query_rikishi-by-id}}}
(#:db{:ident :rikishi/id,
:valueType :db.type/string,
:cardinality :db.cardinality/one,
:unique :db.unique/identity}
#:db{:ident :rikishi/shikona,
:valueType :db.type/string,
:cardinality :db.cardinality/one}
#:db{:ident :rikishi/banduke,
:valueType :db.type/string,
:cardinality :db.cardinality/one}
#:db{:ident :rikishi/heya,
:valueType :db.type/string,
:cardinality :db.cardinality/one})
通过集中定义模式,即使在跨多个团队的开发中,也可以轻松进行数据模型的开发,而不会感到太大的负担。而在实现考虑时,可以在改变umlaut模式定义的同时进行推敲,一旦确定了数据模型,团队成员就可以独立进行其他地方的实现,如Lacinia Resolver和Datomic Query等。
4. Clojure CLI(工具.deps)
目前,系统正在采用拆分为多个微服务的方式进行开发,但是服务之间的依赖关系很复杂,而且跨多个团队进行的激烈开发不太适合语义化版本控制。因此,为了解决公司内部仓库之间的依赖关系,我们采用了Clojure CLI(tools.deps)的 Git Coordinate 功能。通过使用这个功能,我们可以将其他库的依赖指定为 Git 的提交哈希(SHA)。
{:deps
{github-yourname/time-lib {:git/url "https://github.com/yourname/time-lib"
:sha "04d2744549214b5cba04002b6875bdf59f9d88b6"}}}
比如,当为了对一个服务进行更改而修改库时,我们可以在不影响依赖于该库的其他服务的情况下进行开发,并且可以轻松执行回滚等操作,从而获得一些好处。此外,由于不再需要对库本身进行发布和版本管理,我们也可以解放出来。然而,要使用 Git Coordinate,参考仓库也必须是 Clojure CLI 项目,因此这可以说是 Clojure 特有的方法。
然而,这种结构也面临着各种限制,目前我们采取了一种名为monorepo的方法,用一个仓库来管理所有库和服务。我们将在另一篇文章中详细介绍这种方法。
5. 无论在哪里,都可以使用 Clojure。
团队中有前端工程师,但我们使用ClojureScript开发Web UI。基本上,我们将前端和后端分工处理,但也有工程师参与两者,这是Clojure的特点。前面提到的umlaut成果还可以从前端使用,因此通过采用ClojureScript,我们能够避免数据模型的双重管理。
此外,许多开发者工具也是用Clojure编写的。命令行工具通常不会直接使用Clojure(JVM),因为它启动速度较慢,但是通过GraalVM的本机映像构建,现在可以使用Clojure编写经常在Git提交钩子等中频繁调用的命令。同时,我们还使用了前面提到的closh和joker等工具。
有很多产品是用Clojure编写的,即使是我没有参与开发的项目,我也可以在紧要时刻进行查看,这给人一种安心感。
过度采用Clojure会给产品经理和QA等不直接编写Clojure的人造成困扰,这是事实。举例来说,存在着难以参考Datomic内容的问题。虽然Datomic并不仅限于Clojure连接,但附带的Datomic Console并不易于使用,并非Clojure工程师以外的人可以轻松参考的工具。
但是,最近Datomic发布了一个名为Datomic Analytics的功能。今年的Clojure Advent Calendar上,hden先生详细总结了这个功能,通过支持与查询引擎Presto的连接,Datomic现在可以通过SQL来引用其数据。
presto:my_db> select * from rikishi;
id | shikona | banduke | heya |
----+---------+---------+-------
0 | 鶴竜 | 横綱 | 陸奥
1 | 白鳳 | 横綱 | 宮城野
2 | 豪栄道 | 大関 | 境川
通过与支持Presto的Metabase连接,用户可以在UI界面上与数据进行交互,并且可以用于报告目的。通过这个功能,我们能够减轻对Datomic参考的负担。但在这类问题中,对于Clojure的理解程度相对较高,即使是非工程师的人也会认为”我可以写Clojure吗?”。
顺便提一下,在Datomic Analytics中需要准备一个名为Metaschema的映射定义文件,以将以图形形式表示的Datomic模式结构转换为可以通过SQL进行查询的模式结构。通常情况下,不需要人工直接思考和编写该文件中的内容,因为可以通过机械方式实现,团队利用umlaut来与Datomic模式进行通用化并进行自动生成。
Clojure 在团队开发中有什么缺点?
1. 毫无型格
目前的公司仍处在激烈的服务开发过程中,不像Rich Hickey的演讲中所说,强类型化使得数据结构紧密嵌入在代码基础中,而代码基础的改动变得困难。
《Effective Programs – 10 Years of Clojure – Rich Hickey》
对此当然存在正反两种观点,但作为已经采用Clojure的立场来说,我并不感觉到难以承受的不便之处。
Clojure 提供了 clojure.spec 作为开放数据的检查工具,至少在服务边界上进行了 spec 检查,因此在数据模型更改时并不感到太大的不安。但目前的 spec 无法检测数据中的多余键,这让人感到有些不便(例如在引入 Datomic 时)。正如 athos 先生在他的文章中所提到的,我们期待 spec2 能解决这个问题。
2. 代码审查 mǎ chá)
这个式子并不难读。
然而,在进行代码审查时,如果存在S表达式的结构性变化,阅读diff会变得困难。如果代码需要根据传入的值进行条件分支,使用defmethod编写可以提高可扩展性,并使代码更易于理解变更。
另外,在将未经spec检查的地图传递给函数的地方,如果存在关键字的typo,通常很难在代码检查中捕获,并且可能需要重新进行后续的操作验证。虽然使用IDE的自动补全功能可以在一定程度上避免这种情况,但这种情况还是相当常见。
3. Clojure工程师需求量不够。
在職轉職活動中經常聽到的一件事就是,對於企業來說很難找到 Clojure 工程師。這在某種程度上可能是採用 Clojure 的最大障礙。然而,同時在轉職活動中聽到的故事中,如果一個人喜歡並願意學習 Clojure,那麼作為一名工程師,他們可以期望達到一定的高水平,這樣選擇成本就會降低。
在各种不同的Clojurian工作中,可以学习Clojure的资料也大大增加,从零培养Clojure工程师的目标也变得现实。参加上个月的clojure.tokyo的Clojure Hands-on活动中,很多人表示想要从零开始学习Clojure,感受到了底子的广泛。对于努力推广Clojure的人们,如dosync radio和渋谷 Lisp,我十分敬佩。
最后
回首回想,我觉得自己过着非常幸福的 Clojurian 人生。
回顾了上一篇文章以后的Clojure活动,我表达了我对Clojure的意见。我认为Clojure是适合团队开发的。尽管Clojure的采用可能比较困难,但一旦建立起体制并开始运行,就能很容易地利用Clojure的能力推进团队开发而不会遇到太大的问题。
让我们一起决定吧。