前言: 捧讀像這一類的書對於自己來說總帶著一些神聖感,感謝自己並沒有被這麼巨集大的主題嚇退,看完了這裡分享輸出一下自己的筆記。
一、理解重構
什麼是重構?
按書中 P45 中的說法,重構這個概念被分成了動詞和名詞的方面被分別闡述:
- 重構(名詞): 對軟體內部結構的一種調整,目的是在不改變軟體可觀察行為的前提下,提高其可理解性,降低其修改成本。
- 重構(動詞): 使用一系列重構手法,在不改變軟體可觀察行為的前提下,調整其結構。
在過去的幾十年時間裡,重構這個詞似乎被用來代指任何形式的程式碼清理,但上面的定義所指的是一種特定的清理程式碼的方式。重構的關鍵在於運用大量微小且保持軟體行為的步驟,一步一步達成大規模的修改。
每一次的重構要麼很小,要麼包含了若干個小步驟,即使重構沒有完成,也應當可以在任何時刻停下來,所以如果有人說它們的程式碼在重構過程中有一兩天時間不可用,基本上可以確定,他們做的事不是重構。
與效能優化的區別
重構與效能優化有很多相似的地方:兩者都需要修改程式碼,並且兩者都不會改變程式的整體功能。
兩者的差別在於起目的:
- 重構是為了讓程式碼 “更容易理解,更容易修改”。這可能使程式執行得更快,也可能使程式執行的更慢。
- 效能優化則只關心程式是否執行的更快。對於最終得到的程式碼是否容易理解和維護就不知道了。
為什麼重構?
重構不是包治百病的靈丹妙藥,也絕對不是所謂的“銀彈”。重構只是一種工具,能夠幫助你始終良好的控制程式碼而已。使用它,可能基於下面的幾個目的。
這裡有一個有意思的科普(引用自百度百科:沒有銀彈
):在民俗傳說裡,所有能讓我們充滿夢靨的怪物之中,沒有比狼人更可怕的了,因為它們會突然地從一般人變身為恐怖的怪獸,因此人們嘗試著查詢能夠奇蹟似地將狼人一槍斃命的銀彈。我們熟悉的軟體專案也有類似的特質(以一個不懂技術的管理者角度來看),平常看似單純而率直,但很可能一轉眼就變成一隻時程延誤、預算超支、產品充滿瑕疵的怪獸,所以,我們聽到了絕望的呼喚,渴望有一種銀彈,能夠有效降低軟體開發的成本,就跟電腦硬體成本能快速下降一樣。
1. 改進軟體的設計
當人們只為短期目的而修改程式碼時,他們經常沒有完全理解架構的整體設計。於是程式碼逐漸失去了自己的結構。程式設計師越來越難以通過閱讀程式碼來理解原來的設計。程式碼結構的流失有累積效應。越難看出程式碼所代表的設計企圖,就越難以保護其設計,於是設計就腐敗得越快。
完成同樣一件事,設計欠佳的程式往往需要更多程式碼,這常常是因為程式碼在不同的地方使用完全相同的語句做同樣的事情,因此改進設計的一個重要方向就是消除重複程式碼。消除重複程式碼,我就可以確定所有事物和行為在程式碼中只表述一次,這正是優秀設計的根本。
2. 使軟體更容易理解
所謂程式設計,很大程度上就是與計算機對話:我編寫程式碼告訴計算機做什麼,而它的響應是按照我的指示精確行動。一言以蔽之,我所做的就是填補“我想要它做什麼”和“我告訴它做什麼”之間的縫隙。程式設計的核心就在於“準確說出我想要的”。
然而別忘了,除計算機之外,原始碼還有其他讀者,並且很大概率還是幾個月後的自己,如何更清晰地表達我想要做的,這可能就需要一些重構的手法。
這裡我聯想到了軟體設計的 KISS 原則:KISS 原則,Keep It Simple and Stupid ,簡單的理解這句話就是,要把一個系統做的連白痴都會用。
3. 幫助找到 BUG
對程式碼的理解,可以幫助找到系統中存在的一些 BUG。搞清楚程式結構的同時,也可以對自己的假設做一些驗證,這樣一來 BUG 想不發現都難。
Kent Beck 經常形容自己的一句話是:“我不是一個特別好的程式設計師,我只是一個有著一些特別好的習慣的還不錯的的程式設計師。”重構能夠幫助我們更有效地寫出健壯的程式碼。
4. 提高程式設計速度
聽起來可能有些反直覺,因為重構可能會花大量的時間改善設計、提高閱讀性、修改 BUG,難道不是在降低開發速度嘛?
軟體開發者交談時的故事:一開始他們進展很快,但如今想要新增一個新功能需要的時間就要長得多。他們需要花越來越多的時間去考慮如何把新功能塞進現有的程式碼庫,不斷蹦出來的bug修復起來也越來越慢。程式碼庫看起來就像補丁摞補丁,需要細緻的考古工作才能弄明白整個系統是如何工作的。這份負擔不斷拖慢新增功能的速度,到最後程式設計師恨不得從頭開始重寫整個系統。
下面這幅圖可以描繪他們經歷的困境。
但有些團隊的境遇則截然不同。他們新增新功能的速度越來越快,因為他們能利用已有的功能,基於已有的功能快速構建新功能。
兩種團隊的區別就在於軟體的內部質量。需要新增新功能時,內部質量良好的軟體讓我可以很容易找到在哪裡修改、如何修改。良好的模組劃分使我只需要理解程式碼庫的一小部分,就可以做出修改。如果程式碼很清晰,我引入 BUG 的可能性就會變小,即使引入了 BUG,除錯也會容易得多。理想情況下,程式碼庫會逐步演化成一個平臺,在其上可以很容易地構造與其領域相關的新功能。
這種現象被作者稱為“設計耐久性假說”:通過投入精力改善內部設計,我們增加了軟體的耐久性,從而可以更長時間地保持開發的快速。目前還無法科學地證明這個理論,所以說它是一個“假說”。
20年前,行業的陳規認為:良好的設計必須在開始程式設計之前完成,因為一旦開始編寫程式碼,設計就只會逐漸腐敗。重構改變了這個圖景。現在我們可以改善已有程式碼的設計,因此我們可以先做一個設計,然後不斷改善它,哪怕程式本身的功能也在不斷髮生著變化。由於預先做出良好的設計非常困難,想要既體面又快速地開發功能,重構必不可少。
什麼時候重構?
- 三次法則:
第一次做某件事時只管去做;第二次做類似的事會產生反感,但無論如何還是可以去做;第三次再做類似的事,你就應該重構。
什麼時候不應該重構?
重構並不是必要,當然也有一些不那麼需要重構的情況:
- 不需要修改,那些醜陋的程式碼能隱藏在一個 API 之下。 只有當我需要理解其工作原理時,對其進行重構才會有價值;
- 重寫比重構容易。 這可能就需要良好的判斷力和豐富的經驗才能夠進行抉擇了。
二、重構的幾種姿勢
預備性重構:讓新增新功能更容易
重構的最佳時機就在新增新功能之前。在動手新增新功能之前,我會看看現有的程式碼庫,此時經常會發現:如果對程式碼結構做一點微調,我的工作會容易得多。也許已經有個函式提供了我需要的大部分功能,但有幾個字面量的值與我的需要略有衝突。如果不做重構,我可能會把整個函式複製過來,修改這幾個值,但這就會導致重複程式碼—如果將來我需要做修改,就必須同時修改兩處(更麻煩的是,我得先找到這兩處)。而且,如果將來我還需要一個類似又略有不同的功能,就只能再複製貼上一次,這可不是個好主意。
這就好像我要往東去100公里。我不會往東一頭把車開進樹林,而是先往北開20公里上高速,然後再向東開100公里。後者的速度比前者要快上3倍。如果有人催著你“趕快直接去那兒”,有時你需要說:“等等,我要先看看地圖,找出最快的路徑。”這就是預備性重構於我的意義。
——Jessica Kerr
修復bug時的情況也是一樣。在尋找問題根因時,我可能會發現:如果把3段一模一樣且都會導致錯誤的程式碼合併到一處,問題修復起來會容易得多。或者,如果把某些更新資料的邏輯與查詢邏輯分開,會更容易避免造成錯誤的邏輯糾纏。用重構改善這些情況,在同樣場合再次出現同樣bug的概率也會降低。
幫助理解的重構:使程式碼更易懂
我需要先理解程式碼在做什麼,然後才能著手修改。這段程式碼可能是我寫的,也可能是別人寫的。一旦我需要思考“這段程式碼到底在做什麼”,我就會自問:能不能重構這段程式碼,令其一目瞭然?我可能看見了一段結構糟糕的條件邏輯,也可能希望複用一個函式,但花費了幾分鐘才弄懂它到底在做什麼,因為它的函式命名實在是太糟糕了。這些都是重構的機會。
看程式碼時,我會在腦海裡形成一些理解,但我的記性不好,記不住那麼多細節。正如 Ward Cunningham 所說,通過重構,我就把腦子裡的理解轉移到了程式碼本身。隨後我執行這個軟體,看它是否正常工作,來檢查這些理解是否正確。如果把對程式碼的理解植入程式碼中,這份知識會儲存得更久,並且我的同事也能看到。
重構帶來的幫助不僅發生在將來——常常是立竿見影。是我會先在一些小細節上使用重構來幫助理解,給一兩個變數改名,讓它們更清楚地表達意圖,以方便理解,或是將一個長函式拆成幾個小函式。當程式碼變得更清晰一些時,我就會看見之前看不見的設計問題。如果不做前面的重構,我可能永遠都看不見這些設計問題,因為我不夠聰明,無法在腦海中推演所有這些變化。Ralph Johnson說,這些初步的重構就像掃去窗上的塵埃,使我們得以看到窗外的風景。在研讀程式碼時,重構會引領我獲得更高層面的理解,如果只是閱讀程式碼很難有此領悟。有些人以為這些重構只是毫無意義地把玩程式碼,他們沒有意識到,缺少了這些細微的整理,他們就無法看到隱藏在一片混亂背後的機遇。
撿垃圾式重構
幫助理解的重構還有一個變體:我已經理解程式碼在做什麼,但發現它做得不好,例如邏輯不必要地迂迴複雜,或者兩個函式幾乎完全相同,可以用一個引數化的函式取而代之。這裡有一個取捨:我不想從眼下正要完成的任務上跑題太多,但我也不想把垃圾留在原地,給將來的修改增加麻煩。如果我發現的垃圾很容易重構,我會馬上重構它;如果重構需要花一些精力,我可能會拿一張便箋紙把它記下來,完成當下的任務再回來重構它。
當然,有時這樣的垃圾需要好幾個小時才能解決,而我又有更緊急的事要完成。不過即便如此,稍微花一點工夫做一點兒清理,通常都是值得的。正如野營者的老話所說:至少要讓營地比你到達時更乾淨。如果每次經過這段程式碼時都把它變好一點點,積少成多,垃圾總會被處理乾淨。重構的妙處就在於,每個小步驟都不會破壞程式碼——所以,有時一塊垃圾在好幾個月之後才終於清理乾淨,但即便每次清理並不完整,程式碼也不會被破壞。
有計劃的重構和見機行事的重構
上面的例子——預備性重構、幫助理解的重構、撿垃圾式重構——都是見機行事的:我並不專門安排一段時間來重構,而是在新增功能或修復 BUG 的同時順便重構。這是我自然的程式設計流的一部分。不管是要新增功能還是修復 BUG,重構對我當下的任務有幫助,而且讓我未來的工作更輕鬆。這是一件很重要而又常被誤解的事:重構不是與程式設計割裂的行為。你不會專門安排時間重構,正如你不會專門安排時間寫 if
語句。我的專案計劃上沒有專門留給重構的時間,絕大多數重構都在我做其他事的過程中自然發生。
還有一種常見的誤解認為,重構就是人們彌補過去的錯誤或者清理骯髒的程式碼。當然,如果遇上了骯髒的程式碼,你必須重構,但漂亮的程式碼也需要很多重構。在寫程式碼時,我會做出很多權衡取捨:引數化需要做到什麼程度?函式之間的邊界應該劃在哪裡?對於昨天的功能完全合理的權衡,在今天要新增新功能時可能就不再合理。好在,當我需要改變這些權衡以反映現實情況的變化時,整潔的程式碼重構起來會更容易。
長久以來,人們認為編寫軟體是一個累加的過程:要新增新功能,我們就應該增加新程式碼。但優秀的程式設計師知道,新增新功能最快的方法往往是先修改現有的程式碼,使新功能容易被加入。所以,軟體永遠不應該被視為“完成”。每當需要新能力時,軟體就應該做出相應的改變。越是在已有程式碼中,這樣的改變就越顯重要。
不過,說了這麼多,並不表示有計劃的重構總是錯的。如果團隊過去忽視了重構,那麼常常會需要專門花一些時間來優化程式碼庫,以便更容易新增新功能。在重構上花一個星期的時間,會在未來幾個月裡發揮價值。有時,即便團隊做了日常的重構,還是會有問題在某個區域逐漸累積長大,最終需要專門花些時間來解決。但這種有計劃的重構應該很少,大部分重構應該是不起眼的、見機行事的。
長期重構
大多數重構可以在幾分鐘—最多幾小時—內完成。但有一些大型的重構可能要花上幾個星期,例如要替換一個正在使用的庫,或者將整塊程式碼抽取到一個元件中並共享給另一支團隊使用,再或者要處理一大堆混亂的依賴關係,等等。
即便在這樣的情況下,我仍然不願讓一支團隊專門做重構。可以讓整個團隊達成共識,在未來幾周時間裡逐步解決這個問題,這經常是一個有效的策略。每當有人靠近“重構區”的程式碼,就把它朝想要改進的方向推動一點。這個策略的好處在於,重構不會破壞程式碼—每次小改動之後,整個系統仍然照常工作。例如,如果想替換掉一個正在使用的庫,可以先引入一層新的抽象,使其相容新舊兩個庫的介面。一旦呼叫方已經完全改為使用這層抽象,替換下面的庫就會容易得多。(這個策略叫作Branch By Abstraction[mf-bba]。)
複審程式碼時重構
至於如何在程式碼複審的過程中加入重構,這要取決於複審的形式。在常見的pull request模式下,複審者獨自瀏覽程式碼,程式碼的作者不在旁邊,此時進行重構效果並不好。如果程式碼的原作者在旁邊會好很多,因為作者能提供關於程式碼的上下文資訊,並且充分認同複審者進行修改的意圖。對我個人而言,與原作者肩並肩坐在一起,一邊瀏覽程式碼一邊重構,體驗是最佳的。這種工作方式很自然地導向結對程式設計:在程式設計的過程中持續不斷地進行程式碼複審。
三、壞程式碼長什麼樣?
這讓我想起之前在捧讀《阿里巴巴 Java 開發手冊》時學習的程式碼規範的問題(傳送門)
,只不過當時學習的是好的程式碼應該長什麼樣,而現在討論的事情是:壞的程式碼長什麼樣?
其實大部分的情況應該作為程式設計師的我們都有一定的共識,所以我覺得簡單列一下書中提到的情況就足以說明:
神祕命名
重複程式碼
過長函式
過長引數列表
全域性資料: 全域性資料的問題在於,從程式碼庫的任何一個角落都可以修改它,而且沒有任何機制可以探測出到底哪段程式碼做出了修改。一次又一次,全域性資料造成了一些詭異的 BUG,而問題的根源卻在遙遠的別處。
可變資料: 對資料的修改經常導致出乎意料的結果和難以發現的 BUG。我在一處更新資料,卻沒有意識到軟體中的另一處期望著完全不同的資料。
發散式變化: 模組經常因為不同的原因在不同的方向上發生變化。
散彈式修改: 每遇到某種變化,你都必須在許多不同的類內做出許多小修改。
依戀情結: 所謂模組化,就是力求將程式碼分出區域,最大化區域內部的互動、最小化跨區域的互動。但有時你會發現,一個函式跟另一個模組中的函式或者資料交流格外頻繁,遠勝於在自己所處模組內部的交流。
資料泥團: 你經常在很多地方看到相同的三四項資料:兩個類中相同的欄位、許多函式簽名中相同的引數。
基本型別偏執: 很多程式設計師不願意建立對自己的問題域有用的基本型別,如錢、座標、範圍等。
重複的 switch: 在不同的地方反覆使用相同的 switch 邏輯。問題在於:每當你想增加一個選擇分支時,必須找到所有的 switch,並逐一更新。
迴圈語句: 我們發現,管道操作(如 filter 和 map)可以幫助我們更快地看清被處理的元素一級處理它們的動作。
冗餘的元素
誇誇其談通用性: 函式或類的唯一使用者是測試用例。
臨時欄位: 有時你會看到這樣的類:其內部某個欄位僅為某種特定情況而定。這樣的程式碼讓人不理解,因為你通常認為物件在所有時候都需要它的所有欄位。在欄位未被使用的情況下猜測當初設定它的目的,會讓你發瘋。
過長的訊息鏈
中間人: 過度運用委託。
內幕交易: 軟體開發者喜歡在模組之間築起高牆,極其反感在模組之間大量交換資料,因為這會增加模組間的耦合。在實際情況裡,一定的資料交換不可避免,但我們必須儘量減少這種情況,並把這種交換都放到明面上來。
過大的類
異曲同工的類
純資料類: 所謂純資料類是指:他們擁有一些欄位,以及用於訪問(讀寫)這些欄位的函式,除此之外一無長物。純資料類常常意味著行為被放在了錯誤的地方。也就是說,只要把處理資料的行為從客戶端搬移到純資料類裡來,就能使情況大為改觀。
被拒絕的遺贈: 拒絕繼承超類的實現,我們不介意:但如果拒絕支援超類的介面,這就難以接受了。
註釋: 當你感覺需要纂寫註釋時,請先嚐試重構,試著讓所有註釋都變得多餘。
四、重構的一些方法
書中花了大量的章節介紹我們應該如何重構我們的程式,有幾個關鍵的點是我自己能夠提煉出來的:找出程式碼中不合理的地方、結構化、容易理解、測試確保正確。總之圍繞這幾個點,書中介紹了大量的方法,下面結合自己的一些理解來簡單概述一下吧。
結構化程式碼
結構化的程式碼更加便於我們閱讀和理解,例如最常使用的重構方法:提煉函式
- 動機:把意圖和實現分開
void printOwing(double amount) {
printBanner();
//print details
System.out.println ("name:" + _name);
System.out.println ("amount" + amount);
}
=>
void printOwing(double amount) {
printBanner();
printDetails(amount);
}
void printDetails (double amount) {
System.out.println ("name:" + _name);
System.out.println ("amount" + amount);
}
更清楚的表達用意
要保持軟體的 KISS 原則是不容易的,但是也有一些方法可以借鑑,例如:引入解釋性變數
動機:用一個良好命名的臨時變數來解釋對應條件子句的意義,使語義更加清晰。
if ( (platform.toUpperCase().indexOf("MAC") > -1) &&
(browser.toUpperCase().indexOf("IE") > -1) &&
wasInitialized() && resize > 0 )
{
// do something
}
=>
final boolean isMacOs = platform.toUpperCase().indexOf("MAC") > -1;
final boolean isIEBrowser = browser.toUpperCase().indexOf("IE") > -1;
final boolean wasResized = resize > 0;
if (isMacOs && isIEBrowser && wasInitialized() && wasResized) {
// do something
}
另外由於 lambda 表示式的盛行,我們現在有一些更加優雅易讀的方法使我們的程式碼保持可讀:以管道取代迴圈就是這樣一種方法。
const names = [];
for (const i of input) {
if (i.job === "programer")
names.push(i.name);
}
=>
const names = input
.filter(i => i.job === "programer")
.map(i => i.name)
;
合理的組織結構
例如上面介紹的提煉函式的方法,固然是一種很好的方式,但也應該避免過度的封裝,如果別人使用了太多間接層,使得系統中的所有函式都似乎只是對另一個函式的簡單委託(delegation),造成我在這些委託動作之間暈頭轉向,並且內部程式碼和函式名稱同樣清晰易讀,那麼就應該考慮行內函數。
動機:①去處不必要的間接性;②可以找出有用的間接層。
int getRating() {
return (moreThanFiveLateDeliveries()) ? 2 : 1;
}
boolean moreThanFiveLateDeliveries() {
return _numberOfLateDeliveries > 5;
}
=>
int getRating() {
return (_numberOfLateDeliveries > 5) ? 2 : 1;
}
合理的封裝
封裝能夠幫助我們隱藏細節並且,能夠更好的應對變化,當我們發現我們的類太大而不容易理解的時候,可以考慮使用提煉類的方法。
動機:類太大而不容易理解。
class Person {
get officeAreaCode() { return this._officeAreaCode; }
get officeNumber() { return this._officeNumber; }
}
=>
class Person {
get officeAreaCode() { return this._telephoneNumber.areaCode; }
get officeNumber() { return this._telephoneNumber.number; }
}
class TelephoneNumber {
get areaCode() { return this._areaCode; }
get number() { return this._number; }
}
反過來,如果我們發現一個類不再承擔足夠責任,不再有單獨存在的理由的時候,我們會進行反向重構:內斂類
class Person {
get officeAreaCode() { return this._telephoneNumber.areaCode; }
get officeNumber() { return this._telephoneNumber.number; }
}
class TelephoneNumber {
get areaCode() { return this._areaCode; }
get number() { return this._number; }
}
=>
class Person {
get officeAreaCode() { return this._officeAreaCode; }
get officeNumber() { return this._officeNumber; }
}
簡化條件表示式
分解條件式: 我們能通過提煉程式碼,把一段 「複雜的條件邏輯」 分解成多個獨立的函式,這樣就能更加清楚地表達自己的意圖。
if (date.before (SUMMER_START) || date.after(SUMMER_END))
charge = quantity * _winterRate + _winterServiceCharge;
else charge = quantity * _summerRate;
=>
if (notSummer(date))
charge = winterCharge(quantity);
else charge = summerCharge (quantity);
另外一個比較受用的一條建議就是:以衛語句取代巢狀條件式。根據經驗,條件式通常有兩種呈現形式。第一種形式是:所有分支都屬於正常行為。第二種形式則是:條件式提供的答案中只有一種是正常行為,其他都是不常見的情況。
精髓是:給某一條分支以特別的重視。如果使用 if-then-else
結構,你對 if
分支和 else
分支的重視是同等的。 這樣的程式碼結構傳遞給閱讀者的訊息就是:各個分支有同樣的重要性。衛語句(guard clauses)就不同了,它告訴閱讀者:「這種情況很罕見,如果它真的發生了,請做 一些必要的整理工作,然後退出。」
「每個函式只能有一個入口和一個出口」的觀念,根深蒂固於某些程式設計師的腦海裡。 我發現,當我處理他們編寫的程式碼時,我經常需要使用 Replace Nested Conditional with Guard Clauses。現今的程式語言都會強制保證每個函式只有一個入口, 至於「單一出口」規則,其實不是那麼有用。在我看來,保持程式碼清晰才是最關鍵的:如果「單一出口」能使這個函式更清楚易讀,那麼就使用單一出口;否則就不必這麼做。
double getPayAmount() {
double result;
if (_isDead) result = deadAmount();
else {
if (_isSeparated) result = separatedAmount();
else {
if (_isRetired) result = retiredAmount();
else result = normalPayAmount();
};
}
return result;
};
=>
double getPayAmount() {
if (_isDead) return deadAmount();
if (_isSeparated) return separatedAmount();
if (_isRetired) return retiredAmount();
return normalPayAmount();
};
自我測試程式碼
如果認真觀察程式設計師把最多時間耗在哪裡,你就會發現,編寫程式碼其實只佔非常小的一部分。有些時間用來決定下一步幹什麼,另一些時間花在設計上面,最多的時間則是用來除錯(debug)。每個程式設計師都能講出「花一整天(甚至更多)時間只找出一隻小小臭蟲」的故事。修復錯誤通常是比較快的,但找出錯誤卻是噩夢一場。當你修好一個錯誤,總是會有另一個錯誤出現,而且肯定要很久以後才會注意到它。 彼時你又要花上大把時間去尋找它。
「頻繁進行測試」是極限程式設計( extreme programming XP)[Beck, XP]的重要一 環。「極限程式設計」一詞容易讓人聯想起那些編碼飛快、自由而散漫的黑客(hackers), 實際上極限程式設計者都是十分專注的測試者。他們希望儘可能快速開發軟體,而他們也知道「測試」可協助他們儘可能快速地前進。
在重構之前,先保證一組可靠的測試用例(有自我檢驗的能力),這不僅有助於我們檢測 BUG,其中也有一種以終為始的思想在裡面,實際上,我們可以通過編寫測試用例,更加清楚我們最終的函式應該長什麼樣子,提供什麼樣的服務。
結束語
感謝您的耐心閱讀,以上就是整個學習的筆記了。
重構不是一個一蹴而就的事,需要長期的實踐和經驗才能夠完成得很好。重構強調的是 Be Better,那在此之前我們首先需要先動起手來搭建我們的系統,而不要一味地“完美主義”,近些時間接觸的敏捷式開發也正是這樣的一種思想。
如果有興趣閱讀,這裡只找到一份第一版可以線上閱讀的地方,請自行食用吧:https://www.kancloud.cn/sstd521/refactor/194190
按照慣例黏一個尾巴:
歡迎轉載,轉載請註明出處!
簡書ID:@我沒有三顆心臟
github:wmyskxz
歡迎關注公眾微訊號:wmyskxz
分享自己的學習 & 學習資料 & 生活
想要交流的朋友也可以加qq群:3382693