1. 程式人生 > >大話重構連載16:超級大函式

大話重構連載16:超級大函式

事情總是這樣的:當我們對一個遺留系統一忍再忍,再忍,忍,還要忍……終於積攢到某一天,實在忍無可忍了,拍案而起,不能再忍了,重構!!!事情就這樣發生了。然而,在這時你突然發現,重構的工作千頭萬緒,真不知從何開始。堆積如山的問題此起彼伏,期望修改的設計思緒萬千。這裡有個想法,那裡有個思路,什麼都想做,卻什麼都做不了,真是腦子裡一團亂麻。這時候,沒有一個合理的步驟,清晰的計劃,瞎幹蠻幹是十分危險的,它會為你的重構帶來不可預期的未來。無數次的經驗告訴我,不論是什麼系統,採用什麼架構,從分解大函式開始,肯定沒有錯。

大函式,就是那些業務邏輯特別複雜、程式程式碼特別多、一提起就叫人頭疼不已的超級方法。超級大函式,很難讓人讀懂,更難於維護與變更,毫無疑問是軟體退化的重災區。它起初可能並不複雜,也邏輯清晰、易於讀懂,但隨著業務邏輯的一次次變更,不停地往裡面新增程式碼,再加上一些不合理的設計,經過天長日久,變得越來越臃腫,超級大函式就這樣產生了。超級大函式的產生是有它內在的客觀原因的。怎麼這麼說呢?前面我們談過,軟體發展的客觀規律就是業務邏輯越來越複雜。隨著業務邏輯越來越複雜,正確的辦法就是適時地重構和優化我們的程式碼。但非常遺憾地是,幾乎很少有人認識到這一點。這樣的結果就是,隨著業務邏輯越來越複雜,人們總是就著原有的程式結構不停地往裡面新增新的程式碼。原有的清晰而簡單的程式,隨著新程式碼的不斷新增,開始變得越來越複雜而難懂了。正因為如此,在大多數軟體企業的遺留系統中,超級大函式就變成了一種通病。讓我們用HelloWorld為例來演變一番它的歷程吧。

如前面第三章所述,最開初的HelloWorld程式是這樣的:

 1 /**
 2  * The Refactoring's hello-world program
 3  * @author fangang
 4  */
 5 public class HelloWorld {
 6     /**
 7      * Say hello to everyone
 8      * @param now
 9      * @param user
10      * @return the words what to say
11      */
12     public
String sayHello(Date now, String user){ 13 //Get current hour of day 14 Calendar calendar = Calendar.getInstance(); 15 calendar.setTime(now); 16 int hour = calendar.get(Calendar.HOUR_OF_DAY); 17 18 //Get the right words to say hello 19 String words = null
; 20 if(hour>=6 && hour<12){ 21 words = "Good morning!"; 22 }else if(hour>=12 && hour<19){ 23 words = "Good afternoon!"; 24 }else{ 25 words = "Good night!"; 26 } 27 words = "Hi, "+user+". "+words; 28 return words; 29 } 30 }

了了數十行程式碼,簡單明瞭。隨後就開始變更了,首先是關於時間的問候變得複雜了,添加了一些特殊的節日的問題,如新年問候“Happy new year! ”、情人節問候“Happy valentine’s day! ”、三八婦女節問候“Happy women’s day! ”,等等。同時,對一天中的問候也變得更加精細。對於這樣的需求,IT攻城獅們敲著鍵盤就開始改寫了:

 1 /**
 2  * The Refactoring's hello-world program
 3  * @author fangang
 4  */
 5 public class HelloWorld {
 6     /**
 7      * Say hello to everyone
 8      * @param now
 9      * @param user
10      * @return the words what to say
11      */
12     public String sayHello(Date now, String user){
13         //Get current month, date and hour.
14         Calendar calendar = Calendar.getInstance();
15         calendar.setTime(now);
16         int hour = calendar.get(Calendar.HOUR_OF_DAY);
17         int month = calendar.get(Calendar.MONTH);
18         int day = calendar.get(Calendar.DAY_OF_MONTH);
19         
20         //Get the right words to say hello
21         String words = null;
22         if(month==1 && day==1){
23             words = "Happy new year!";
24         }else if(month==1 && day==14){
25             words = "Happy valentine's day!";
26         }else if(month==3 && day==8){
27             words = "Happy women's day!";
28         }else if(month==5 && day==1){
29             words = "Happy Labor day!";
30         
31         ……
32         
33         }else if(hour>=6 && hour<12){
34             words = "Good morning!";
35         }else if(hour==12){
36             words = "Good noon!";
37         }else if(hour>=12 && hour<19){
38             words = "Good afternoon!";
39         }else{
40             words = "Good night!";
41         }
42         words = "Hi, "+user+". "+words;
43         return words;
44     }
45 }

程式碼量開始翻倍。接著,客戶要求所有的使用者資訊應當來源於資料庫的使用者表,同時設計了問候語規則表,所有關於時間的問候都應來源於對該表的查詢。這時我們繼續膨脹sayHello()這個方法:

 1 /**
 2  * The Refactoring's hello-world program
 3  * @author fangang
 4  */
 5 public class HelloWorld {
 6     /**
 7      * Say hello to everyone
 8      * @param now
 9      * @param user
10      * @return the words what to say
11      */
12     public String sayHello(Date now, long userId){
13         //Get database connection.
14         try {
15             Class.forName("oracle.jdbc.driver.OracleDriver");
16         } catch (ClassNotFoundException e1) {
17             throw new RuntimeException("No found JDBC driver");
18         }
19         String url = "jdbc:oracle:thin:@localhost:1521:helloworld";
20         String username = "test";
21         String password = "testpwd";
22         Connection connection;
23         try {
24             connection = DriverManager.getConnection(url,username,password);
25         } catch (SQLException e1) {
26             throw new RuntimeException("Connect database failed!");
27         }
28         
29         //Get current month, date and hour.
30         Calendar calendar = Calendar.getInstance();
31         calendar.setTime(now);
32         int hour = calendar.get(Calendar.HOUR_OF_DAY);
33         int month = calendar.get(Calendar.MONTH);
34         int day = calendar.get(Calendar.DAY_OF_MONTH);
35         
36         //Get the right words to say hello
37         String words = null;
38         String greetingRuleSql = 
39             "select words, month, day, hourLower, hourUpper from greeting_rules";
40         try {
41             PreparedStatement statement = 
42                 connection.prepareStatement(greetingRuleSql);
43             ResultSet resultSet = statement.executeQuery();
44             while(!resultSet.isLast()){
45                 int monthOfRule = resultSet.getInt("month");
46                 int dayOfRule = resultSet.getInt("day");
47                 if(month==monthOfRule && day==dayOfRule){
48                     words = resultSet.getString("words");
49                     break;
50                 }
51                 int hourLower = resultSet.getInt("hourLower");
52                 int hourUpper = resultSet.getInt("hourUpper");
53                 if(hour>=hourLower && hour<hourUpper){
54                     words = resultSet.getString("words");
55                     break;
56                 }
57             }
58             if(words==null) 
59             throw new RuntimeException("Error when searching greeting rules.");
60         } catch (SQLException e1) {
61             throw new RuntimeException("Error when getting greeting rules.");
62         }
63         
64         //Get user's name
65         String user = "";
66         String userSql = "select name from rms_user where user_id=?";
67         try {
68             PreparedStatement statement = connection.prepareStatement(userSql);
69             statement.setLong(1, userId);
70             ResultSet resultSet = statement.executeQuery();
71             user = resultSet.getString(1);
72         } catch (SQLException e) {
73             throw new RuntimeException("Error when getting user's name.");
74         }
75         
76         words = "Hi, "+user+". "+words;
77         return words;
78     }
79 }

這是一個十分簡單的示例,但我們可以看到它已經由短短十來行膨脹成了60多行,膨脹了4倍之多。在最後這個版本中,sayHello()既要負責連線資料庫、查詢資料,又要獲得當前時間的月份、日期與小時,還要完成相應的業務邏輯的判斷,使程式變得相當複雜。我們可以繼續想象,如果繼續提出新的需求,比如支援多語言、支援多資料庫,程式質量將繼續下滑,直到我們無法忍受。

解決超級大函式問題最有效的辦法就是分解,按照功能一步一步分解,還原其應有的優化結構。在這個過程中我們常用的重構方法叫“抽取方法(Extract Method)”。重構是一個探索的過程,因為我們總是起初對要重構的系統並不瞭解,沒有設計文件(即使有,對不對還是一說呢),沒有熟悉系統的人(哪怕只是在不明白的時候問一問)。你,只是接到任務才開始接手這個系統,你對這個系統的瞭解簡直就是一貓黑。而這時,抽取方法是我們開始這種探索,瞭解這個系統最有效的工具,它往往是這樣進行的:

當我們在閱讀一段大函式時,我們可以自覺不自覺地為這些程式碼分段,為一段功能相對獨立的程式碼編寫註釋。有時我們可能還需要調整程式碼的先後順序,將一些有更多關聯的程式碼放在一起,如將變數的宣告與變數真正使用的程式碼放在一起,或者將有明顯前後關係的程式碼放在一起。這樣的調整是一個好的開端,因為它讓我們的程式碼開始變得有序,開始變得可讀。

隨後,我們就可以使用“抽取方法”了。將被我們分段、加上註釋的程式碼從原函式中抽取出來,放在另外一個獨立的函式中,為這個函式取個易懂的名稱——這是一個非常重要的好習慣。我常常為了給一個函式取一個正確的名字而思索很長時間,甚至修改好幾次。不要認為這是在浪費時間,它也是優化程式碼重要的一個環節。但起初我們對這段程式碼的理解可能不那麼深,因此我們往往選擇用結果變數為其命名。隨著我們對這段程式碼理解的深入,可以運用重構中的“重新命名方法(Rename Method)”,根據其程式碼意圖重新為其命名(許多開發工具,如eclipse,都支援該重構方法,它們使你在重新命名的同時,同步修改了所有對該方法的呼叫)。經過這樣對程式碼段的抽取,原始碼在這裡就變成了對這個新函式的呼叫。

舉一個例子吧,這時一段真實的遺留系統,原有程式是這樣寫的:

 1     ......
 2     int iCtbz = -1;
 3     ElecObj elecObj = null;
 4     //獲取辦理時間
 5     int iCzyf = Integer.valueOf(Stream[9]).intValue();
 6     String czMonth = String.valueOf(iCzyf % 20).toString().trim();
 7     String czYear = String.valueOf( (iCzyf - (iCzyf % 20)) / 20 + 2000).
 8                  toString();
 9     if (czMonth.length() == 1){
10       czMonth = "0" + czMonth;
11     }
12     long lBlsj = Long.valueOf(czYear + czMonth).longValue();
13     String dqyear = sysDate.toString().substring(0, 4);
14     ......

這段程式碼寫得並不好,有許多需要我們優化的地方。但記住“小步快跑”原則,此時不是解決其它問題的時候,現在我們首先是運用抽取方法優化程式結構。經過抽取以後,將以上加粗的部分改為了這樣:

1     ......
2     int iCtbz = -1;
3     ElecObj elecObj = null;
4     //獲取辦理時間
5     int iCzyf = Integer.valueOf(Stream[9]).intValue();
6     long lBlsj = getBlsj(iCzyf);
7     String dqyear = sysDate.toString().substring(0, 4);
8     ......

加粗的部分就是被改寫的內容,同時將抽取的內容放進了一個獨立的函式:

 1 /**
 2 *@param iCzyf
 3 *@return 獲取辦理時間
 4 */
 5 private long getBlsj(int iCzyf) {
 6     String czMonth = String.valueOf(iCzyf % 20).toString().trim();
 7     String czYear = String.valueOf( (iCzyf - (iCzyf % 20)) / 20 + 2000).
 8               toString();
 9     if (czMonth.length() == 1){
10          czMonth = "0" + czMonth;
11     }
12     return Long.valueOf(czYear + czMonth).longValue();
13 }

在給這個新建立的函式命名時,我們對這段程式碼的理解並不深刻。在原函式中,這段程式碼執行的結果是獲得了lBlsj這個結果變數,即獲得了辦理時間。為此,我們先為該函式命名為getBlsj這個函式名。但在後續的工作中,我們逐漸理解到,它不僅可以獲取辦理時間,還可以獲取很多時間。它實質是將時間由一種表示方式轉換成另一種表示方式,因此我們運用開發工具的重新命名功能,將該函式命名為transformDate,其它呼叫它的程式碼也隨之修改了。重新命名後的函式就不再僅僅運用在此處獲取辦理時間,還應用在其它業務程式碼的處理中。

抽取方法可大可小,你可以將一段數百行的程式碼抽取走,也可能只抽取了數行。但不論怎樣,被抽取走的程式碼一定是功能內聚的,也就是說它們執行的是一個說得清道得明、清楚明確的功能。同時,被抽取走的程式碼一定是執行的一個清晰的功能,而不是多個。它可大可小,大也許還能分解為多個功能,但至少在邏輯上,這些功能是這個大的功能的組成部分。

抽取方法是一個探索的過程,最關鍵是那個紅線劃在哪裡,即抽取程式碼的範圍。多一行也對,少一行也對,關鍵在於我們抽取出來的這個函式,它的功能我們是怎樣定義的。公說公有理,婆說婆有理,沒有一個定論,所以方法的抽取常常是反反覆覆。開始我們按照一個思路抽取出來,後來想想覺得不對,因此又放回原函式,重新劃分,重新抽取,反覆多次。

有一次,我在重構一個Servlet的時候,抽取出來的函式是在執行一大段業務邏輯操作。但在完成了一系列業務操作以後,原程式要將返回值轉換成二進位制程式碼寫入response中,返回前端。起初,我將整個這一段都抽取出來。但令人很彆扭的是,傳參的時候必須要把response傳進去,這使得業務邏輯與Web應用環境耦合,不利於日後的優化,編寫自動化測試。隨後我還原了這段程式碼,重新進行抽取,將寫response的部分留在了原函式中,而將紅線畫在了完成業務邏輯操作之後,寫入response之前。新重構的函式得以與web應用解耦,為後面的進一步優化做好了準備。重構的過程,是考驗開發人員能力的過程,需要大家反覆練習與鑽研。

另外,抽取方法就像核裂變,開始由一個函式裂變為幾個函式。分解出來的函式又裂變為另外幾個函式,不斷這樣往復下去。同時,抽取方法總是在一個類中發生裂變。而當這個類分解出來的方法達到一定程度以後,隨之而來的就是類的裂變,由一個類分解成多個類,分解出來的類再分解……類的分解我們採用的是另一個方法——“抽取類(Extract Class)”,我們將在後面講述。

重構是一系列的等量變換,抽取方法是這些等量變換中最典型的例子。將一段程式碼從原函式中抽取出來,程式碼依然是那些程式碼,只是程式結構發生了變換。正因為如此,才能保證我們的重構過程的安全可靠。

特別說明:希望網友們在轉載本文時,應當註明作者或出處,以示對作者的尊重,謝謝!

相關推薦

大話重構連載16超級函式

事情總是這樣的:當我們對一個遺留系統一忍再忍,再忍,忍,還要忍……終於積攢到某一天,實在忍無可忍了,拍案而起,不能再忍了,重構!!!事情就這樣發生了。然而,在這時你突然發現,重構的工作千頭萬緒,真不知從何開始。堆積如山的問題此起彼伏,期望修改的設計思緒萬千。這裡有個想法,那裡有個思路,什麼都想做,卻什麼都做不

大話重構連載19物件的演化過程

很好,我們終於邁出了重構的第一步,而這第一步我們瞄準了程式碼問題的重災區——超級大函式。超級大函式之所以是程式碼問題的重災區,就是因為它們往往難於閱讀、難於維護。面對大函式我們採取的辦法是拆分,以功能為核心將其拆分成一個一個獨立的函式。拆分後的程式變得易於閱讀了,因為要讀懂程式你不再需要讀完所有程式碼,選擇性

大話重構連載10小設計而不是布局

開車的朋友一定深有體會,駕駛汽車其實就是在不斷矯正汽車行駛方向的一個過程。在整個駕駛過程中,你必須全神貫注地緊盯前方,通過方向盤不斷矯正方向,否則即使行駛在直線路段也可能偏離車道。那些疲勞駕駛的司機,因為進入睡眠狀態,無法再矯正方向,車輛就會越來越偏離航向。這種情況下,即使數秒鐘的小盹,也能造成車毀人亡的嚴重

大話重構連載13自動化測試——想說愛你不容易

正如許多事情都有其兩面性一樣,測試方法也是這樣。要保證測試方法正確,最簡單、最直觀地想法就是多寫些測試用例,從更多地角度去測試,但這必然增加我們的測試成本。小步快跑要求我們頻繁進行測試,假如我們重構的週期是20分鐘,但測試卻要花掉10分鐘,那麼這樣的成本就實在太大了。假如這種測試還是開發人員手工測試,每天都有

大話重構連載12你不能沒有保險索

通過前面的描述你已經對重構中“小步快跑”的開發模式有了一個清楚的認識。學會和習慣小步快跑的開發模式,對於重構工作極其重要,因為它讓這種大範圍、大幅度修改程式碼的重構工作變得不再像以往那樣讓人膽戰心驚。究其原因,雖然從結果上是在大範圍、大幅度調整,但每一步卻是小範圍、小福度調整,並且能保證每一步都是正確的。

大話重構連載首頁

ooo 我們 family 不能 blank 順序 關系 trac 工廠 《大話重構》這本書是我寫的第一本書,從今天起我將通過連載的形式逐漸跟大家分享。 這本書讓你: 告別遊擊隊轉變為正規軍。 遠離劣質代碼走向精妙設計 真正明確專業級的軟件開發是如

JS能力測評16正確的函式定義

思路: 這道題是考函式宣告與函式表示式的區別,原題的寫法,是在兩個邏輯分支裡面各有一個函式宣告,但是對於函式宣告,解析器會率先讀取並且讓其在執行任何程式碼前可用,意思就是別的程式碼還沒執行呢,兩個getValue宣告已經被讀取,所以總是執行最新的那個。函式表示式,當解析器執行到它所在的程式碼行時

對話吳恩達(Andrew Ng)超級咖深度解析人工智慧 以及如何成為已經資料探勘工程師

【數盟致力於成為最卓越的資料科學社群,聚焦於大資料、分析挖掘、資料視覺化領域,業務範圍:線下活動、線上課程、獵頭服務、專案對接】 【優惠倒計時】資料定義未來,2016年5月12日-14日DTCC2016中國資料庫技術大會登陸北京!4月20日前輸入數盟專屬購票優惠碼iir46am3立享88折上折,猛戳文末“

Python全棧學習筆記day 16匿名函式

匿名函式:為了解決那些功能很簡單的需求而設計的一句話函式 這段程式碼 def calc(n): return n**n print(calc(10)) 換成匿名函式 calc = lambda n:n**n print(calc(10)) 下面給出了一個關於匿名函式格式的說

條款4~5GotW#16 具有最可複用的通用Containers

問題: 為下面的定長(fixed-length)vector class實現拷貝構造和拷貝賦值操作,以提供最大的可用性(usability)。提示:請考慮使用者程式碼可能會用它做哪些事情。 template<typename T,size_t size> class fixed_

工業資料漫談16物聯網(IOT)與工業資料的關係

上一次談了談工業大資料和工業4.0的關係,今天來聊一聊物聯網和工業大資料的關係。 我們先看一看什麼是物聯網是怎麼來的。物聯網的概念其實起源很早,1999年,在美國召開的移動計算和網路國際會議首先提出了物聯網(Internet of Things)這個概念。提出者是1999年

機器學習筆記熵(模型,推導,與似然函式關係的推導,求解)

1、最大熵模型 最大熵原理:最大熵原理認為在學習概率模型時,在所有可能的概率模型中,熵最大的模型是最少的模型。 該原理認為要選擇的概率模型首先得承認已有的現實(約束條件),對未來無偏(即不確定的部分是等可能的)。比如隨機變數取值有A,B,C,另外已知

重構-改善既有程式碼的設計重新組織函式的九種方法(四)

         函式過長或者邏輯太混亂,重新組織和整理函式的程式碼,使之更合理進行封裝。 提煉函式:(由複雜的函式提煉出獨立的函式或者說大函式分解成由小函式組成)你有一段程式碼可以被組織在一起並獨立出來。將這段程式碼放進一個獨立函式,並讓函式名稱解釋該函式的用途。

Hibernate hql查詢語句 Count統計函式 Min求最小值函式 Max求最函式 Sum求和函式 Avg求平均數函式

在HQL中可以呼叫 Count:統計函式 Min:求最小值函式 Max:求最大值函式 Sum:求和函式 Avg:求平均數函式  Count:統計函式 Session session = HibernateSessionFactory.getSession(); Transaction tx = sess

Guru of the Week 條款16具有最可複用性的通用Containers

GotW #16 Maximally Reusable Generic Containers<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />著者:Herb Sutter翻譯:kingofar

從TensorFlow到PyTorch深度學習框架哪款最適合你?

方法 愛好 board ebo 部分 速度 智能 這也 解釋器 開源的深度學習神經網絡正步入成熟,而現在有許多框架具備為個性化方案提供先進的機器學習和人工智能的能力。那麽如何決定哪個開源框架最適合你呢?本文試圖通過對比深度學習各大框架的優缺點,從而為各位讀者提供一個參考。你

重構改善既有代碼設計--重構手法02Inline Method (內聯函數)& 03 Inline Temp(內聯臨時變量)

臨時變量 替代 xtra 移動 get replace 16px ber ble Inline Method (內聯函數) 一個函數調用的本體與名稱同樣清楚易懂。在函數調用點插入函數體,然後移除該函數。 int GetRating()

重構改善既有代碼設計--重構手法01Extract Method (提煉函數)

設置 都是 覆寫 list() 為什麽 新建 細粒度 align 容易 背景: 你有一段代碼可以被組織在一起並獨立出來。將這段代碼放進一個獨立函數,並讓函數名稱解釋該函數的用途。 void PrintOwing(double amount)

重構改善既有代碼設計--重構手法06Split Temporary Variable (分解臨時變量)

font bsp 責任 獨立 剖析 ron 代碼 一個 變量 你的程序有某個臨時變量被賦值超過一次,它既不是循環變量,也不被用於收集計算結果。針對每次賦值,創造一個獨立、對應的臨時變量 double temp = 2 * (_height + _width); Sy

重構改善既有代碼設計--重構手法07Remove Assignments to Parameters (移除對參數的賦值)

改善 產生 移除 你在 nal 處理 other 問題 多少 代碼對一個 參數賦值。以一個臨時變量取代該參數的位置。 int Discount(int inputVal, int quantity, int yearTodate) {