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