Java與groovy混編 - 一種兼顧接口清晰和實現敏捷的開發方式
原文鏈接: http://pfmiles.github.io/blog/java-groovy-mixed/
- 有大量平均水平左右的“工人”可被選擇、參與進來 —— 這意味著好招人
-
有成熟的、大量的程序庫可供選擇 ——
這意味著大多數項目都是既有程序庫的拼裝,標準化程度高而定制化場景少 -
開發工具、測試工具、問題排查工具完善,成熟 ——
基本上沒有團隊愿意在時間緊、任務重的項目情況下去做沒有把握的、基礎開發工具類的技術試探 -
有面向對象特性, 適合大型項目開發 ——
無數大型項目已向世人述說,“面向對象”是開發大型軟件的優秀代碼組織結構 -
能適應大型團隊、多人協作開發 ——
代碼需要簡單易懂,起碼在接口、api層面是這樣
—— 這是我所理解的“工業化開發編程語言”的概念
很顯然, java就是種典型的“工業語言”, 非常流行,很多企業靠它賺錢,很實際;但java也是常年被人黑,光是對其開發效率的詬病就已經足夠多,不過java始終屹立不倒;
這樣的局面其實無所謂高興還是擔憂,理性的程序員有很多種,其中一種是向“錢”看的 —— 我寫java代碼,就是因為工作需要而已,能幫助我的組織搞定業務,做出項目,這很好;當有人說java語言不好的時候,理性的程序員不會陷入宗教式的語 言戰爭之中,他會思考這些人說的是否有道理;如果真的發現整個java平臺大勢已去,他會毫不猶豫地扭頭就走,不過直到目前為止,還沒有這種跡象出現;
那么,從這些無數次的口水之爭中,我們能否從別人的“戰場”上發現一些有用的東西, 來改進我們的開發方式,從而使得java這種已經成為一個“平臺”的東西走得更遠,賺更多的錢呢?答案是“有的”,感謝那些參與口水戰爭的、各種陣營的年 輕程序員們,有了你們,java speaker們才有了更多的思考;
我就只談一個最實際的問題:
java被吐槽的這些年, 就開發效率這一點而言,到底有哪些東西是值得借鑒的?
也就是說,到底是哪些主要特性直接導致了某些其它語言在語法上相對于java的優越感?
豐富的literal定義
在groovy中定義map和list的慣用方式:
def list = [a, 2 ,3] def map = [a:0, b:1]
而java呢?只能先new一個list或map,再一個個add或put進去; 上面這種literal(字面量)形式的寫法便捷得多;
而javascript在這方面做得更絕, 我們都用過json,而json其實就是literal形式的object
極端情況下,一門編程語言里的所有數據類型,包括”內建”的和用戶自定義的,統統可以寫成literal形式;在這種情形下,其實這種語言連額外的對象序列化、反序列化機制都不需要了 —— 數據的序列化形式就是代碼本身, “代碼”和“數據”在形式上被統一了
java對這方面幾乎沒有任何支持,對于提高編碼效率來講,這是值得學習的一點, 起碼“內建”數據結構需要literal寫法支持
first-class function & higher-order function & function literal(lambda)
無論是js, 還是python/ruby,或是groovy,都可以將函數作為另一個函數的參數傳入,以便后者根據執行情況判斷是否要調用前者
或者能夠將一個函數作為另一個函數的返回值返回,以便后續再對其進行調用
這種高階函數特性,就不要再說java的匿名內部類“能夠”實現了, 如果認為匿名內部類已經”夠用”了的話,其實就已經與現在的話題“開發效率”相悖了
高階函數顯然是一種值得借鑒的特性,它會讓你少寫很多很多無聊的“包裝”代碼;
還有就是匿名函數(lambda)了我不喜歡lambda、lambda地稱呼這個東西,我更喜歡把它叫做“匿名函數”或者“函數字面量(literal)”, 因為它跟數學上的lambda演算還是有本質區別,叫”lambda”有誤導的危險
函數字面量的意思就是說,你可以在任何地方,甚至另一個函數體的調用實參或內部,隨時隨地地定義另一個新的函數這種定義函數的形式,除了“這個函數我只想在這里用一次,所以沒必要給它起個名字”這種理由之外,還有一個更重要的理由就是“閉包”了
所謂閉包,其實也是一個函數,但是在這個函數被定義時,其內部所出現的所有”自由變量(即未出現在該函數的參數列表中的變量)”已被當前外層上 下文給確定下來了(lexical), 這時候,這個函數擁有的東西不僅僅是一套代碼邏輯,還帶有被確定下來的、包含那些“自由變量”的一個上下文, 這樣這個函數就成為了一個閉包
那么閉包這種東西有什么好呢?其實如果懶散而鉆牛角尖地想,閉包的所有能力,是嚴格地小于等于一個普通的java對象的,也就是說,凡是可以用 一個閉包實現的功能,就一定可以通過傳入一個對象來實現,但反過來卻不行 —— 因為閉包只有一套函數邏輯,而對象可以有很多套,其次很多語言實現的閉包其內部上下文不可變但對象內部屬性可變
既然這樣,java還要閉包這種東西來干嘛?其實這就又陷入了”匿名內部類可以實現高階函數”的困境里了 —— 如果我在需要一個閉包的時候,都可以通過定義一個接口再傳入一個對象來實現的話,這根本就跟今天的話題“開發效率”背道而馳了
顯然,java是需要閉包的
強大而復雜的靜態類型系統
這和開發效率有關么?編程語言不是越“動態”,開發效率越高么?還需要強大而復雜的靜態類型系統么?
試想一下這種api定義:
def eat(foo) {
...
}
這里面你認識的東西可能只有’吃’了, 你知道foo是什么么?你知道它想吃什么么?吃完后要不要產出點什么東西? —— 你什么都不知道這種api極易調用出錯,這就好比我去買飯,問你想吃什么你說“隨便”,但買回肯德基你卻說你實際想吃的是麥當勞一樣
可能你還會反駁說,不是還有文檔么?你把文檔寫好點不就行了么? —— 不要逼我再提“匿名內部類”的例子,如果給每個函數寫上復雜詳盡的文檔是個好辦法,那就顯然 —— again, 與“開發效率”背道而馳了
那么,靜態類型系統,這里顯然就該用上了
靜態類型系統在多人協作開發、甚至團隊、組織間協作開發是非常有意義的;
擁有靜態類型系統的編程語言通常都有強大的、帶語法提示功能的IDE,這很正常,因為靜態類型語言的語法提示功能好做;
只要把別人的庫拿過來,導入IDE,各種函數簽名只需掃一眼 —— 很多情況下根本不需要仔細看文檔 —— 就已經知道這個函數是干嘛用的了, 合作效率成倍提升;
而且,作為”api”,作為“模塊邊界”,作為與其它程序員合作的“門面”, 函數簽名上能將參數和返回值類型“卡”得越緊越好 —— 這樣別人不用猜你這個函數需要傳入什么類型,甚至他在IDE里一“點”,這里就給自動填上了 :)
要做到“卡得緊”,光有靜態類型系統還不夠,這個系統還需強大, 試想一下這個例子:
/** * 我只吃香蕉和豬肉,請勿投食其它物品 */ public void eat(List<Object> list) { for(Object o: list) { if(o instanceof Banana){ ... // eating banana } else if(o instanceof Pork) { ... // eating pork } else { throw new RuntimeException("System err."); } } }
這段純java代碼已經是“定義精確”的靜態類型了
但如果沒有上面那行注釋,你很可能會被System err.無數次
而這行注釋之所以是必需的,完全是因為我找不到一個比List<Object>更好的表達“香蕉或豬肉”的形式, 這種情形足以讓人開始想念haskell的either monad
在“強大而復雜的類型系統”這一點上,jvm平臺上令人矚目的當屬scala了,可惜java沒有,這是值得借鑒的
不過這一點的“借鑒”還需java的compiler team發力,我等也只是說說(按照java保守的改進速度,估計HM類型系統是指望不上了)
動態類型系統,duck-typing
剛說完靜態類型,現在又來說動態類型系統合適么?
然而這與節操無關,我想表達的是,只要是有助于“開發效率”的,都能夠借鑒,這是一個理性的java speaker的基本素質
我們在開發項目的時候,大量的編碼發生在“函數”或“方法”的內部 —— 這就好比你在屋子里、在家里宅著一樣, 是不是應該少一些拘束,多一些直截了當?在這種情形下,動態類型系統要不要太爽? ——
Void visitAssert(AssertTree node, Void arg1) { def ahooks = this.hooks[VisitAssertHook.class] ahooks.each {it.beforeVisitCondition(node, errMsgs, this.ctx, resolveRowAndCol, setError)} scan((Tree)node.getCondition(), arg1); ahooks.each {it.afterVisitConditionAndBeforeDetail(node, errMsgs, this.ctx, resolveRowAndCol, setError)} scan((Tree)node.getDetail(), arg1); ahooks.each {it.afterVisitDetail(node, errMsgs, this.ctx, resolveRowAndCol, setError)} return null; }
你知道ahooks是什么類型么?你不知道但我(我是編碼的人)知道你知道ahooks身上有些什么方法可以調么?你同樣不知道但我知道
你不知道沒關系,只要我知道就行了,因為現在是我在寫這段代碼;這段代碼寫完以后,我只會把Void visitAssert(AssertTree node, Void arg1)這個類型明確的方法簽名提供給你調用,我并不會給你看函數體里面的那坨東西,因此你知不知道上面這些真的沒關系
方法內部滿是def, 不用書寫繁復的List<Map<String, List<Map<Banana, Foo>>>>這種反人類反社會標語, 每個對象我知道它們身上能“點”出些什么來,我只管“點”,跑起來之后invokedynamic會為我搞定一切
動態類型系統 —— 這就是方法內部實現應該有的樣子哪怕你的方法內部實現就是一坨shi,你也希望這坨shi能盡可能小只一點,這樣看起來更清爽是吧?
不要說我太分裂,我要笑你看不穿 —— 靜態類型和動態類型既然都有好處,那么他們能放在一起么?
能的,這里就需要點明這篇文章的政治目的了: “java與groovy混編”
而且,目前來看,jvm平臺上,只有它二者的結合,才能完成動態靜態混編的任務
曾經我發出過這樣一段感嘆:
公共api、對外接口聲明、應用程序邊界…這些對外的“臉面”部分代碼,如果擁有scala般強大的類型系統…就好了;而私有代 碼、內部實現、各種內部算法、邏輯,如果擁有groovy般的動態、簡單的類型系統…就好了;綜上,如果有門語言,在接口和實現層面分別持有上述特性,就 好了
這種“理想”中的語言或許某天我有空了會考慮實現一個
而現在,雖說不是scala,但我終于想要在java和groovy身上來試驗一把這種開發方式了
這里我坦白一下為什么沒用scala,原因很簡單,我在技術選型方面是勢利的,scala還不被大多數平均水平的java開發人員(參見”工業化開發編程語言”定義第一條)接受,這直接導致項目的推進會遇到困難
而相對來講,我暫且相信大多數java開發人員都還算愿意跨出groovy這一小步,當然這還需要時間證明
好了,下面還剩下一點點無關痛癢的牢騷 ——
元編程能力
macro, eval, 編譯過程切入, 甚至method missing機制,這些都算“元編程”
元編程能力的強弱直接決定了使用這種語言創作“內部DSL”的能力java在元編程方面的能力,幾乎為0
這是值得借鑒的
與groovy的混編,順便也能把groovy的元編程也帶進來
各種奇巧的語法糖
語法糖,關起門來吃最美味,這也是一種使得“方法內部實現更敏捷”的附加手段
網上隨便下載一份groovy的cheat sheet, 都會列舉groovy的那些寫代碼方面的奇技淫巧
這些奇技淫巧,在各種腳本語言之間其實都大同小異, 因為他們本來就是抄來抄去的
結合方法內部的動態類型環境,這一定會進一步縮小方法內部實現代碼的體積
java & groovy混編:一種最“勢利”的折衷
我不去討論什么語言才是The True Heir of Java, 那會使這篇文章變成一封戰書,我只關心如何更好地利用現有開發資源完成項目,高效地幫組織實現利益
所以說java和groovy的混編是一種最“勢利”的折衷,我不想強迫平均水平的開發人員去學習一種完全不同的語言,短期內不會對項目有任何好處,真正想去學的人他自己會找時間去學
而groovy,說它是java++也不為過,因為java代碼直接就可以被groovy編譯, groovy完全兼容java語法, 對一般java開發人員來說,這真是太親切了
這里我要提一下我對“java和groovy混編”的一個個人性質的小嘗試 —— kan-java項目
kan-java這個小工具,凡是用戶在編碼使用過程中能“碰”到的類和接口,全部都由java定義, 這確保用戶拿到的東西都有精確的類型定義
凡是對上述接口的實現,都以groovy代碼的形式存在
這貫徹了”接口靜態類型,內部實現動態類型”的宗旨, 或者說“凡是要提供給另外一個人看、調用的地方(接口或接口類),使用java,否則就用groovy”
當然了,單元測試也完全由groovy代碼實現
將kan-java的jar包引入到項目中使用時,就跟使用其它任何純java實現的jar包一樣 —— 接口清晰,參數類型明確,返回類型明確, 你不會也沒有必要知道開發人員在具體實現的時候,使用動態語言爽過一把
對于java和groovy的混編,項目的pom.xml如何配置,除了可以參考kan-java的配置外,還可以參考這個gist: https://gist.github.com/pfmiles/2f2ab77f06d48384f113 , 里面舉例了兩種配置方式,各有特色
具體的效果,還需要真正地去實際項目中體會另外,kan-java也是一個有趣的工具,這個工具所實現的功能我也是從未見到java世界內有其它地方討論過的,它可以輔助java做“內部DSL”,有場景的可以一試