Java開(kāi)發(fā)者可以從Clojure借鑒的4樣?xùn)|西
我在大學(xué)時(shí)學(xué)的Java。OOP(即面向?qū)ο缶幊蹋┠P蜕钪苍谖业乃季S中。我想分享一些我從Clojure中學(xué)到的東西。
Clojure當(dāng)然從Java借鑒了很多。如果能同時(shí)學(xué)習(xí)這兩門(mén)語(yǔ)言一定會(huì)很酷。下面是一些通用原則。事實(shí)上,這些原則在OOP的世界里眾所周知。你很可能已經(jīng)了解它們,所以本文不要求你學(xué)習(xí)Clojure,但是我推薦你去這么做。
1、使用不變值
Clojure 得以聞名的一個(gè)特性是它的不可變的數(shù)據(jù)結(jié)構(gòu)(immutable data structures)。甚至在Java的早期,不變值也是一種很受歡迎的做法。String是不可變的,這點(diǎn)在Java剛發(fā)布那會(huì)備受爭(zhēng)議。在那時(shí),C 和C++的字符串僅僅是可以改變的數(shù)組。不可變的String被認(rèn)為是低效的。但是,回頭再看,不可變的String似乎是一個(gè)正確的選擇。Java中的許多可變類現(xiàn)在被認(rèn)為是設(shè)計(jì)失誤。拿java.util.Date來(lái)說(shuō),改變一個(gè)日期的月份值有什么意思呢?
讓我們更深入地分析下。假設(shè)我是一個(gè)對(duì)象。你詢問(wèn)我的生日。我給你一張紙,上面寫(xiě)著我的生日是1981.7.18。你把這張紙帶回家,存在某個(gè)地方,甚至讓其他人看到這張紙。
其中有一個(gè)人看到這張紙上的日期后說(shuō)“cool,a date!”,并且修改為他自己的生日:通過(guò)調(diào)用setTime方法修改為1976.4.2。這樣下一個(gè)問(wèn)我生日的人得到的實(shí)際上是這個(gè)家伙的生日。這將是多么糟糕的一件事!我將后悔我將那張可以改變我生日的魔術(shù)紙給了別人。
讓值可變的導(dǎo)致這種magic-changing-at-a-distance行為常??赡馨l(fā)生。它之所以不當(dāng)?shù)囊粋€(gè)原因是它違反了信息隱藏原則。我的生日是我這個(gè)對(duì)象的部分狀態(tài)。如果我讓生日的月份、日期和年份可以直接被修改,那么我實(shí)際上是讓任何一個(gè)其他類都能夠直接訪問(wèn)我的內(nèi)部狀態(tài)。
答案當(dāng)然不是使用setters。而是保證對(duì)象一旦構(gòu)建后不可變。這樣,我這個(gè)對(duì)象的內(nèi)部狀態(tài)就一直處于封裝狀態(tài)。
這也適用于集合。你讀過(guò)Iterator的文檔嗎?你能告訴我當(dāng)?shù)撞康膌ist改變時(shí)將發(fā)生什么?我也不能。一個(gè)不可變的list不應(yīng)該有這么一個(gè)復(fù)雜的接口。
解決方案:不要寫(xiě)setter方法。對(duì)于集合,你有幾個(gè)可選方案。有一個(gè)簡(jiǎn)單方案是使用Google Guava不可變類庫(kù)。如果不使用Guava,那么任何時(shí)候你需要返回一個(gè)集合時(shí),先將集合拷貝一份,然后用java.util.Collections。unmodifiable()包裝一下這份拷貝,再扔掉對(duì)拷貝的引用。
- public static Map immutableMap(Map m) {
- return Collections.unmodifiableMap(new HashMap(m));
- }
2、不要在構(gòu)造函數(shù)中做多余的事情
設(shè)想這個(gè)場(chǎng)景:你的Person類有一個(gè)構(gòu)造函數(shù)接受一大堆信息(first name, last name,address等)并且將它們存為對(duì)象的狀態(tài)。你團(tuán)隊(duì)中的某個(gè)人需要將這些數(shù)據(jù)存到文件中,比如存為JSON。為了方便創(chuàng)建Person對(duì)象,你增加了一個(gè)構(gòu)造函數(shù),接收inputStream參數(shù)并將其解析成JSON,然后設(shè)置對(duì)象狀態(tài)。你還增加了一個(gè)構(gòu)造函數(shù)接收aFile參數(shù),讀取文件并解析。之后又有一個(gè)人想從指定URL的web請(qǐng)求中讀取內(nèi)容,你又增加了一個(gè)構(gòu)造函數(shù)。非常棒!你現(xiàn)在有了一個(gè)非常方便的類。
但是稍等一下!Person類的職責(zé)是什么?最初它用來(lái)表示某個(gè)人的個(gè)人信息?,F(xiàn)在它還負(fù)責(zé):
解析JSON
構(gòu)造Web請(qǐng)求
讀取文件
處理錯(cuò)誤
而且現(xiàn)在Person類很難測(cè)試。我們?nèi)绾尾拍軠y(cè)試File構(gòu)造函數(shù)?首先,我們必須向文件系統(tǒng)中寫(xiě)入一個(gè)臨時(shí)文件。不算太壞。那么我們?nèi)绾螠y(cè)試Web請(qǐng)求呢?設(shè)置一個(gè)Web服務(wù)器,配置Web服務(wù)器,然后調(diào)用構(gòu)造函數(shù)。
問(wèn)題在于Person類違反了單一職責(zé)原則。Person類被用來(lái)保存狀態(tài)信息,而不是用來(lái)持久化存儲(chǔ)或者序列化的。它應(yīng)該是一個(gè)數(shù)據(jù)對(duì)象,而不應(yīng)該做更多的。
解決方案:避免讓構(gòu)造函數(shù)包含多余的邏輯。將“便利構(gòu)造函數(shù)”(比如上面解析JSON的構(gòu)造函數(shù))分離到靜態(tài)工廠方法。
3、針對(duì)小接口編程
Clojure做得非常好的一點(diǎn)是定義了一些功能強(qiáng)大的小接口,它們抽象出訪問(wèn)模式。任何使用這個(gè)接口的函數(shù)可以使用實(shí)現(xiàn)這個(gè)接口的任何類型。任何新類型可以利用已有的功能。
拿Iterable接口來(lái)說(shuō),它泛化(或者抽象)了任何可以被順序訪問(wèn)的對(duì)象(比如一個(gè)list或者一個(gè)set)。如果一個(gè)方法需要在某對(duì)象上順序操作,那么這個(gè)方法只需要了解那對(duì)象實(shí)現(xiàn)了Iterable接口。這就意味著,當(dāng)程序員寫(xiě)程序時(shí)可以不必關(guān)注這個(gè)方法實(shí)際操作的對(duì)象的類型。
這符合依賴倒置原則,依賴倒置原則聲稱高層邏輯必需依賴于抽象而不是底層邏輯細(xì)節(jié)。接口很好的吻合了這條原則。高層邏輯應(yīng)該對(duì)接口操作,而底層邏輯實(shí)現(xiàn)接口。
解決方案:仔細(xì)思考類的訪問(wèn)模式,看看能否抽象出小接口。然后針對(duì)接口編程。記住,有兩個(gè)地方會(huì)用到接口:實(shí)現(xiàn)接口者和調(diào)用者。
4、表達(dá)計(jì)算過(guò)程,而不僅僅是世界(Represent computation, not the world)
當(dāng)我讀大學(xué)時(shí),老師告訴我們你們應(yīng)該用類來(lái)為現(xiàn)實(shí)世界的對(duì)象建模。典型的建模問(wèn)題是學(xué)生選課問(wèn)題。
一個(gè)課程可以有很多學(xué)生選,一個(gè)學(xué)生可以注冊(cè)很多課程。多對(duì)多的關(guān)系。
顯而易見(jiàn)地建一個(gè)Student類和一個(gè)Course類。每個(gè)類都包含一個(gè)對(duì)方的list。list表達(dá)了課程注冊(cè)關(guān)系。類似register和listCourses這樣的方法讓學(xué)生注冊(cè)課程或者列出他注冊(cè)的課程。
教授用這個(gè)問(wèn)題來(lái)探討不同設(shè)計(jì)方案的折中問(wèn)題。學(xué)生和課程的配置都不合理。一個(gè)聰明的數(shù)據(jù)建模者將能提煉出多對(duì)多關(guān)系模式。我們可以創(chuàng)建一個(gè)叫 ManyToMany<X, Y>的類來(lái)管理多對(duì)多關(guān)系。然后可以創(chuàng)建一個(gè)ManyToMany<CourseID, StudentID>對(duì)象來(lái)解決選課問(wèn)題。
唯一的問(wèn)題在于這樣做直接違背了教師課程中的意思。關(guān)系不是現(xiàn)實(shí)世界的對(duì)象,它最適合被表述為一種抽象概念。
而且它也可以用來(lái)解決泛化的抽象問(wèn)題。ManyToMany類可以在任何合適的地方被復(fù)用。甚至可以讓ManyToMany作為一個(gè)有很多不同實(shí)現(xiàn)的接口。
我認(rèn)為我的教授是錯(cuò)的。Java標(biāo)準(zhǔn)庫(kù)也包含了很多單純運(yùn)算的類。為什么應(yīng)用程序員不可以也自己寫(xiě)類似的類呢?更多內(nèi)容參考GOF設(shè)計(jì)模式。大部分模式都與抽象運(yùn)算有關(guān),而不是現(xiàn)實(shí)世界的對(duì)象。比如職責(zé)鏈模式,在維基百科中被描述為“通過(guò)給予多個(gè)對(duì)象處理請(qǐng)求的機(jī)會(huì),而避免調(diào)用請(qǐng)求與請(qǐng)求處理者耦合”。
解決方案:尋找代碼中的重復(fù)模式,構(gòu)建類來(lái)表示這些模式。使用這些類而不是在代碼中一再重復(fù)。