Clojure开发初体验

先说说我为什么想试下Clojure。作为Emacs的忠实用户,有时需要用Emacs Lisp实现一些功能,但是Emacs Lisp的文档写的比较简洁,网上相关资料也非常少。很多时候都不了了之了。后来发现,有些基本概念是Lisp系语言共有的。搞不懂的问题,可以查查其他Lisp语言的资料,或许能有答案。

因此萌生了系统性学习Lisp语言的想法,想看看冰河翻译的Practical Common Lisp。但是如果看完只能解决我使用Emacs的问题,好像成本有点高了,毕竟Common Lisp不能拿来干活啊。

除了Common Lisp还有别的选择吗?当然有,那就是Clojure了。做为一门Lisp方言,Clojure有着优雅的语法和强大的表达能力,最关键的是真的可以拿来干活。跟Scala、Kotlin一样,Clojure也是基于jvm实现的,这意味着有大量成熟的Java生态的工具可以直接使用。

书籍

第一个步骤肯定是找些书来看看了。经过我的搜索,这些书都还不错,可以挑一些来看看。

Safari Books Online

说到看书,另外再分享一个大杀器,就是O’Reilly的Safari Books Online,基本所有的书都可以在线看了,而且都是正版,可以告别之前到处搜集PDF的日子了。正常注册的话价格很贵,一年399刀。为了解决这个问题,一般的思路都是几个人共用一个账号,共同承担费用,但其实也是不合规的。

近期发现了一个正规而且价格优惠的途径,就是注册ACM会籍。成为ACM会员之后,就可以免费使用Safari了。进入注册链接,选择ACM Basic Online Membership Package,价格只要15刀。注册完成后,点击Learning Center,进入E-Learning,然后再点击O’Reilly就可以登录了。

体验真的很香。就是觉得之前买的几本书有点贵了,一本书的价格足够注册几年的acm会员了。

Ring Framework

想用Clojure做个简单的web应用?没问题!Ring是clojure上的一个web框架,简单易用。写个hello world就这么简单。

(ns hello-world.core)

(defn handler [request]
  {:status 200
   :headers {"Content-Type" "text/html"}
   :body "Hello World"})

Python的很多web框架我都用过,比如Django、Flask。Ring和他们相比较,功能相对简单,提供了底层的基础功能。至于更丰富的一些功能要通过middleware来实现。我个人比较喜欢这种风格。原因是这大大的降低了开发者的学习成本。作为开发者,我最不希望的就是在所谓的框架上浪费太多时间。

Django这类框架的功能虽然强大,但是会把一些基础的功能进行封装,你能接触到的就是封装后的接口,熟悉这套框架体系需要花费大量的时间。最主要的是,在这里花的时间是没有价值的。换个框架,这些经验一点用都没有了。我更希望能够有更简单的实现方式,并且能够从更底层去实现。

比如用户鉴权,Flask有几个插件可以实现,之前也用过,但是并没有节省我的时间。即便用了插件我也不清楚插件是怎么去实现的。比如SQLAlchemy,最开始可能觉得ORM的概念比较高大上,从文档的复杂程度上就能看出来。用它可能确实能带来一些好处,也比较有挑战。但是我原本的目的就只是增删查改啊,何必浪费这些时间呢。

在Ring里面,我通过自己的middleware来实现用户鉴权,发现原来很简单。达到了“知其然,知其所以然”。其实在整个Clojure生态里面,都是秉承着这样的哲学,力求清晰简单。我非常喜欢。好的技术就应该是让你拿来就进去用,不然等你了解清楚了,黄花菜都凉了,风口都已经过去了,还搞什么?

Component

Component是clojure中维护依赖组件的框架,通过Component可以很方便的处理类似全局的db连接、客户端变量等等。在Scala和Java中可以通过Google Guice框架实现依赖注入,不过Clojure本身并没有面向对象的概念,所以不能直接使用这种依赖注入的框架。比较原始的做法是通过def定义全局变量来维护,但是大家都知道全局变量的问题比较多。在函数式编程的世界中,一个好的函数是不应该关注外部的状态的,直接访问全局变量恰恰打破了这个规则。

使用也比较简单,就是定义好component和system,然后把需要的组件传进函数即可。

;; component
(defrecord Database [host port connection]
  component/Lifecycle

  (start [component]
    (println ";; Starting database")
    (let [conn (connect-to-database host port)]
      (assoc component :connection conn)))

  (stop [component]
    (println ";; Stopping database")
    (.close connection)
    (assoc component :connection nil)))

;; system
(defn ->system [config-options]
  (let [{:keys [host port]} config-options]
    (component/system-map
     :db (new-database host port)
     :scheduler (new-scheduler)
     :app (component/using
           (example-component config-options)
           {:database  :db
            :scheduler :scheduler}))))
(def app
  (app-routes
   (component/start (->system (read-config)))))

(defn -main [& args]
  (run-jetty app {:port 3000}))

对于Component我其实没有特别深入的理解,可能要在代码量大到一定级别才会体现出它的优点。不过既然大家都这么用,那就先保留着吧。后面有时间再看看作者的介绍视频

测试

最近深刻体会到,测试驱动开发所带来的好处。以前写C++的时候不写测试用例,代码到后期越来越难以维护,改动一个功能要花费很长的时间进行测试才能确保逻辑正确。写测试用例第一点是可以保证逻辑的正确性,第二点是提升开发的效率。想象一下,开发一个后台接口,如果需要前端配合来发起请求,效率必然非常低下。通过测试用例来发请求不香吗?

Clojure自带了测试框架clojure.test,另外还有midje。我使用了midje,因为看起来文档比较清晰,容易上手。通过lein的midje插件可以实现自动化测试。

lein midje :autotest

只要改动代码,就会自动触发测试。

单元测试的最佳实践里很重要的一点就是,无论在什么样的环境下都可以跑测试,不需要外部依赖。一个接口肯定会依赖其他的接口,比如db、kv存储、其他的服务等等。如何在测试阶段解决这些外部依赖?如何分别验证这些接口成功和失败的情况?如果开发的时候这些接口还没有准备好呢?midje里面提供了provided来解决这个问题。

(fact
 (f "id1") => true
 (provided
  (g anything) => false))

通过provided,可以指定f所依赖的函数g返回的内容。provided一定要紧跟在fact的后面,如果需要先指定依赖,可以使用against-background

总结

整体的体验,clojure的开发效率非常高,框架齐全,基本功能都有提供,如果想要更高阶的,请自己实现。非常适合有一定经验的开发者。跟Java之间的interop也很方便,可以直接用java的包。

Clojure应用的启动时间非常慢,在我的老款mbp上大概要20秒。主要是因为启动JVM耗时较长,但是在clojure上这也不算什么缺点,因为有非常强大的reload机制。比如开发web应用,通过lein ring server-headless启动server,代码改动后可以实时reload,这点是很多静态语言做不到的。

缺少静态类型是clojure的一个缺点,但是也不算致命的缺点,只是让我少了一些安全感。也可以通过clojure.spec来增加一些检查。跟Scala对比一下,写clojure花的时间还是比较少的,因为在scala里面要把类型搞对是不容易的。

总之clojure是一个可以提供生产力的语言,也比较有意思,值得花时间去深入研究一下。