在前面文章中,討論瞭如何直接減少構造函數和方法的參數,比如通過自定義類型、引入參數對象、Builder模式、重載和方法命名來減少參數。你可能會奇怪爲什麼會討論方法返回值。然而,實際開發中開發者會使用參數作爲返回值,從而增加了額外的參數。
非構造函數一般會在方法簽名中指定返回值,聲明返回值類型。然而,一個令人困擾的問題是:這種方法只支持一個返回值…
Java異常處理機制提供了另一種結果返回方式。檢查出的異常會通過throws語句通知調用者。正如Jim Waldo在他的書“Java:The Good Parts”中寫道:“將Java異常看作一種特殊的方法返回——只提供Throwable類型的返回值,這樣更容易理解。”
“向方法傳入可改變參數,然後在方法中改變參數狀態”,這種方法可以讓參數作爲返回值。方法執行以後,參數對象就包含了被方法改變的內容。調用者通過改變後的參數能獲得方法設定的最新狀態。雖然任何可變的參數對象都可以做到這一點,但是對於那些想通過方法參數傳遞返回值的開發者來說格外有吸引力。
但是,這種做法有一個缺點——它破壞了“最少驚訝原則”。大多數開發者都希望方法參數僅作爲輸入而不是輸出(Java不提供語法區分兩者的不同)。Bob Martin在他的”Clean Code“中寫道:“總的來看,應該避免使用輸出參數。”另外,這種做法還使傳入的可變參數更加混亂。考慮到這些情況,接下來本文將討論更好的辦法支持多個返回值。
譯註:最小驚訝原則(principle of least astonishment),通常是用在用戶界面方面,但同樣適用於編寫的代碼。指的是代碼應該儘可能減少讓讀者感到意外。
雖然Java中方法的返回值只能是一個單獨的對象或者基本類型,但是考慮到對象是可以由我們自行決定,因而不會對我們造成限制。有一些做法我並不推薦。其中一種做法是,返回一個Object對象集合或數組,而其中包含了毫不相關的Object對象。例如,方法返回一個包含了三個值的數組或集合。這種做法的一個變種是,用一個二元組或N元組返回許多的相關值。另一個變種是,返回一個Map對象;Map鍵值可以隨意設定以便關聯相關的值。和其它方法一樣,這樣會給客戶帶來不必要的負擔——因爲他們必須知道鍵值的含義,然後才能通過鍵值獲取他們想要的對象。
下面的示例代碼展示了在不破壞方法參數的前提下提供多個返回值。這不是一個很好的做法:
通過多個通用數據結構提供多個返回
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
//
=============================================================== //
注意:下面的示例只是爲了說明文中的觀點,不建議在生產代碼中使用。 //
=============================================================== /** *
獲取電影信息。 *
*
@return 電影信息數組。信息的內容依據索引號進行分類: *
0 : 電影標題 *
1 : 發佈時間 *
2 : 導演 *
3 : 分級 */ public
Object[] getMovieInformation() { final
Object[] movieDetails = { "World
War Z" ,
2013 ,
"Marc
Forster" ,
"PG-13" }; return
movieDetails; } /** *
獲取電影信息。 *
*
@return 電影信息列表。信息的內容依據信息在列表中的順序分類; *
順序分別是:電影標題、發佈時間、導演、分級。 */ public
List<Object> getMovieDetails() { return
Arrays.<Object>asList( "Ender's
Game" ,
2013 ,
"Gavin
Hood" ,
"PG-13" ); } /** *
獲取電影信息。 *
*
@return 電影信息Map。可以通過鍵值查找電影信息; *
支持的鍵值分別是:"Title"、"Year"、"Director"和"Rating"。 */ public
Map<String, Object> getMovieDetailsMap() { final
HashMap<String, Object> map = new
HashMap(); map.put( "Title" ,
"Despicable
Me 2" ); map.put( "Year" ,
2013 ); map.put( "Director" ,
"Pierre
Coffin and Chris Renaud" ); map.put( "Rating" ,
"PG" ); return
map; } |
示例代碼雖然不通過方法參數提供返回值,但是給調用者造成了一些不必要的負擔——調用者必須對返回的數據結構細節有詳細的瞭解。這些做法能夠減少方法參數,並且不違反“最少驚訝原則”。然而,要求客戶瞭解複雜數據結構的細節不是一種好的做法。
我更喜歡通過自定義類提供多個返回值。相比使用數組、集合或元組結構,這樣會增加一些工作量。但這一點點的工作量(使用IDE完成這項工作只需要幾分鐘)卻可以大大提高代碼的可讀性和流暢性。這是其它方法無法做到的:不用編寫Java doc文檔解釋;也不需要調用者認真閱讀我的代碼,瞭解數組和集合中提供的值是什麼、順序如何、元組中的值是什麼。自定義類提供的方法可以精確地描述它們提供了怎樣的值。
下面的示例代碼展示了一個由NetBeans編寫的Movie類。當方法返回該Movie實例時,可以將Movie作爲方法的返回類型,而不用使用可讀性較差的通用數據結構。
Movie.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
|
package
dustin.examples; import
java.util.Objects; /** *
Simple Movie類展示了怎樣在提供多個返回值的時候兼具良好的可讀性。 *
*
@author Dustin */ public
class
Movie { private
final
String movieTitle; private
final
int
yearReleased; private
final
String movieDirectorName; private
final
String movieRating; public
Movie(String movieTitle, int
yearReleased, String movieDirectorName, String movieRating) { this .movieTitle
= movieTitle; this .yearReleased
= yearReleased; this .movieDirectorName
= movieDirectorName; this .movieRating
= movieRating; } public
String getMovieTitle() { return
movieTitle; } public
int
getYearReleased() { return
yearReleased; } public
String getMovieDirectorName() { return
movieDirectorName; } public
String getMovieRating() { return
movieRating; } @Override public
int
hashCode() { int
hash = 3 ; hash
= 89
* hash + Objects.hashCode( this .movieTitle); hash
= 89
* hash + this .yearReleased; hash
= 89
* hash + Objects.hashCode( this .movieDirectorName); hash
= 89
* hash + Objects.hashCode( this .movieRating); return
hash; } @Override public
boolean
equals(Object obj) { if
(obj == null ) { return
false ; } if
(getClass() != obj.getClass()) { return
false ; } final
Movie other = (Movie) obj; if
(!Objects.equals( this .movieTitle,
other.movieTitle)) { return
false ; } if
( this .yearReleased
!= other.yearReleased) { return
false ; } if
(!Objects.equals( this .movieDirectorName,
other.movieDirectorName)) { return
false ; } if
(!Objects.equals( this .movieRating,
other.movieRating)) { return
false ; } return
true ; } @Override public
String toString() { return
"Movie{"
+ "movieTitle="
+ movieTitle + ",
yearReleased="
+ yearReleased + ",
movieDirectorName="
+ movieDirectorName + ",
movieRating="
+ movieRating + '}' ; } } |
用單個對象提供多個返回值
1
2
3
4
5
6
7
8
9
|
/** *
Provide movie information. * *
@return Movie information. */ publicMovie
getMovieInfo() { returnnewMovie( "Oblivion" , 2013 , "Joseph
Kosinski" , "PG-13" ); } |
通過NetBeans類創建嚮導選擇類名和包,然後鍵入了類的四個屬性。然後,我使用NetBeans的“插入代碼”功能插入get方法,同時重載了toString()、hashCode()和equals(Object)方法。如果覺得不需要這些,我可以讓類變得更簡單。整個創建過程非常簡單,只花費了我5分鐘。現在我有了一個更有用的返回類型,這一點可以在使用Movice類的代碼中看到:無需多餘的Java doc文檔對返回類型進行說明,因爲Movice類具有自描述性——它的get方法會告訴調用者返回的信息。我感覺與使用參數返回值或通用數據類型相比,爲返回值創建類是值得的。這樣做會給你帶來巨大的回報。
使用自定義類包裝多個返回值是一種很有吸引力的解決方案,這並不令人吃驚。畢竟從概念上來講,它和我之前文章中提到的“使用自定義類和參數對象給方法傳遞相關參數,而不是將它們分別傳遞“是相似的。Java是一門面向對象的語言,當看到在代碼中不使用對象來組織方法參數和返回值時,我會感到很吃驚。
好處和優點
使用自定義對象封裝多個返回值的優點非常明顯:方法參數僅僅作爲“輸入”,所有的輸出信息(除了通過異常輸出的錯誤信息) 都包含在方法返回的自定義類型實例中。相比使用數組、集合、Map、元組或者其他的通用數據結構,這是一種更加簡潔的方法;前者將開發的工作轉移給了方法的調用者。
代價和缺點
我沒有發現使用自定義對象封裝多個返回值有什麼缺點。也許最明顯的代價就是需要編寫和測試這些類,但這個代價實際上非常小——這些類大都是非常簡單,而且IDE會爲我們完成大部分工作;IDE會自動完成,而且產生的代碼沒有錯誤;這些類非常簡單,代碼審查人員閱讀和測試起來都非常容易。
如果繼續尋找其它的代價和缺點,有人可能會說“這些類會讓代碼變得臃腫”。我不認爲這是一個有力的反駁。儘管存在”編寫的類很糟糕“這種風險,但是我認爲調用者對一般類型參數錯誤地理解更有可能發生。另外一個可能的風險是,開發者將許多不相關的數據放入同一個類,而這些數據與方法的多個返回值之間沒有太大的關係。既便如此,我發現唯一的好辦法是修改代碼使其不需要返回多個值。在自定義類的對象中包含許多不相關的數據,比起一般類型的參數提供返回值還是要好。事實上,當包含的值與值之間相關性越來越弱時,一般類型的數據結構會變得越來越難用。
結論
自定義參數對象類型幫助我們直接地解決了Java方法中參數過多的問題。幸運的是,這些自定義類型和參數對象還間接地減少了需要的參數個數。因爲通過自定義類型可以從方法中返回多個值,無需額外增加參數用作返回值。