新聞中心
許多細(xì)小的重構(gòu)看似無足輕重,例如方法重命名,提取方法,即使重構(gòu)了,似乎對代碼的結(jié)構(gòu)也沒有太大的影響,于是就決定淡然處之,心里想“事情還未到不可挽回的地步,實現(xiàn)功能要緊,至于重構(gòu),還是以后再做吧!”這樣一想,于是就會滋生得過且過的想法,等到代碼開始變得一團(tuán)糟時,重構(gòu)已經(jīng)變得無比困難了。此時需要的重構(gòu)技巧,將百倍難于發(fā)現(xiàn)壞味道之初。

創(chuàng)新互聯(lián)服務(wù)項目包括竹山網(wǎng)站建設(shè)、竹山網(wǎng)站制作、竹山網(wǎng)頁制作以及竹山網(wǎng)絡(luò)營銷策劃等。多年來,我們專注于互聯(lián)網(wǎng)行業(yè),利用自身積累的技術(shù)優(yōu)勢、行業(yè)經(jīng)驗、深度合作伙伴關(guān)系等,向廣大中小型企業(yè)、政府機(jī)構(gòu)等提供互聯(lián)網(wǎng)行業(yè)的解決方案,竹山網(wǎng)站推廣取得了明顯的社會效益與經(jīng)濟(jì)效益。目前,我們服務(wù)的客戶以成都為中心已經(jīng)輻射到竹山省份的部分城市,未來相信會繼續(xù)擴(kuò)大服務(wù)區(qū)域并繼續(xù)獲得客戶的支持與信任!
1
在我參加的前一個項目中,我們定義了一個處理OrderSet的Controller。剛剛開始開發(fā)時,對于OrderSet的操作并不多,主要是Search與Count操作。OrderSet分為WithDetails與WithoutDetail兩種類型。
為了實現(xiàn)的簡單,我們將這兩種類型的操作都放在一個Controller中。隨著操作的逐漸增多,這個Controller就變得越來越龐大,逐漸變得臃腫起來。
每當(dāng)我需要調(diào)用或者修改Controller時,我都在想:“嗯,這個代碼太糟糕了,什么時候給它重構(gòu)一下。”想是這么想,卻一直扮演著說話的巨人,行動的矮子。即使興起這樣的念頭,又因為其他的工作將此念頭澆滅。直到有一天,這個Controller的代碼已經(jīng)到了忍無可忍的地步,我和我的Pair終于達(dá)成一致意見,決定對此代碼進(jìn)行手術(shù)。
我們花費(fèi)了近一天的時間對Controller以及相關(guān)的Repository進(jìn)行了徹底的重構(gòu)。重構(gòu)前后的代碼天差地別,我終于可以不用像吃了蒼蠅那般看著代碼惡心了。這種重構(gòu)后體驗到的愉悅簡直無與倫比,最關(guān)鍵是結(jié)果令人滿意,重構(gòu)后的代碼變得更可讀,更簡單,也更容易增加新的功能。
2
如今工作的項目,需要對遺留系統(tǒng)進(jìn)行遷移。首要的任務(wù)是為此系統(tǒng)編寫B(tài)DD測試,以期搭建遷移的測試保護(hù)網(wǎng),并能夠形成可讀與可工作的測試用例文檔。
在開始接觸這個任務(wù)時,客戶方的開發(fā)人員已經(jīng)基本搭建好了初步的框架。當(dāng)我們在面對不良代碼時,第一個浮現(xiàn)在腦海中的念頭是“重構(gòu)”,然而考慮到時間因素,隨之又強(qiáng)迫自己滅了這個念頭,因為我們需要考慮項目的進(jìn)度。我們總是在這樣的取舍之中艱難前進(jìn),終于,在系統(tǒng)需要增加一個新消息的測試時,我強(qiáng)烈地感受到重構(gòu)的迫在眉睫。即使代碼有諸多破窗,修補(bǔ)了一扇,總強(qiáng)過聽之任之。經(jīng)過接近一天多的重構(gòu)(當(dāng)然還包括run tests以及build花費(fèi)的時間),結(jié)果令人滿意。
回顧這個過程,我發(fā)現(xiàn)在發(fā)現(xiàn)壞味道時,如果能及時地對代碼進(jìn)行重構(gòu),并保證重構(gòu)的小步前進(jìn),并不會阻礙開發(fā)進(jìn)度,相反還能夠在一定程度改善代碼質(zhì)量,提高代碼的可讀性、可重用性以及可擴(kuò)展性。
所謂“勿以善小而不為”,千萬不要因為小重構(gòu)對代碼質(zhì)量的影響微乎其微而輕視她,或者忽略她,又或者推遲到忍無可忍再想到重構(gòu)。重構(gòu)并非最后的救命稻草,而是隨時保持我們正確前進(jìn)的一把尺子。
3
說完了重構(gòu)的重要性,讓我再來粗略地介紹這個重構(gòu)過程。
我們的測試程序主要針對Message的發(fā)送、接收與驗證。業(yè)務(wù)的處理則由部署在JBoss上的應(yīng)用來處理。我們需要設(shè)定期望的Message,在發(fā)送請求到遠(yuǎn)程系統(tǒng)后,接收返回的消息,并驗證消息以及數(shù)據(jù)庫是否符合我們的期望。重構(gòu)的起因在于我們需要編寫新的測試覆蓋之前從未測試過的消息,其類型為SO08。
如果沿用之前的實現(xiàn),我們就需要在測試步驟中增加MessageType的分支,根據(jù)消息類型對返回的消息進(jìn)行檢查。
檢查的邏輯事實上已經(jīng)被抽象為MessageChecker接口,并為各種類型的消息建立了不同的MessageChecker子類。MessageCheckFactory則是這些子類的工廠,負(fù)責(zé)根據(jù)類型創(chuàng)建對應(yīng)的子類對象。這樣的設(shè)計是完全合理的。
然而,問題出現(xiàn)在MessageReceiver,它提供了接收消息的方法,通過傳入的消息類型、隊列名稱等參數(shù),返回消息。這個返回值被定義為MessageReader。MessageReader正是問題的癥結(jié)。
我一直強(qiáng)調(diào)的面向?qū)ο笤O(shè)計中一個重要概念就是所謂”對象的自治“,即對象的職責(zé)是自我完備的,它能夠?qū)ψ约簱碛械臄?shù)據(jù)負(fù)責(zé),具備了“智能”處理的行為特征。
MessageReader違背了這一原則,它是愚笨的對象,仿佛“坐擁寶山而不知”的笨伯,雖然擁有消息的值,卻不知道該如何處理這些消息。簡而言之,它提供的方法只能對XML格式的消息進(jìn)行讀取,卻不具有真正的業(yè)務(wù)行為。于是在測試步驟中,就產(chǎn)生了這樣的代碼(省略了部分實現(xiàn)代碼):
private void checkPropagationQueueByName(String name, Queue queue, MessageType messageType) {
MessageReader reader = messageReceiver.getMessageFor(messageType, identifier, queue);
String messageText = reader.toString();
if (messageType == SO05) {
messageCheckFactory.checkerFor(messageType, getExpectedSO05ResponseFor(name), messageText);
}
if (messageType == SO07) {
checkSO07Response(name, messageType, messageText)
}
if (messageType == SO08) {
messageCheckFactory.checkerFor(messageType, getExpectedSO08ResponseFor(name), messageText)
.checkResponse();
}
}
不幸的是,這樣的邏輯處理在其他測試步驟中同樣存在。
我注意到,之所以要處理分支,是因為系統(tǒng)需要判斷返回的消息是否符合期望,以實現(xiàn)測試目標(biāo)。這個檢查的邏輯根據(jù)不同的消息類型會有不同的處理邏輯(其中,主要邏輯則委派給由MessageCheckFactory創(chuàng)建的MessageChecker對象)。
從接口看,它們都需要接收返回的消息與期望的消息。以SO05為例,它需要返回的消息messageText,以及由getExpectedSO05ResponseFor(name)方法返回的期望的消息。對于SO07而言,實現(xiàn)方法稍顯復(fù)雜,所以提取了一個私有方法checkSO07Response()來處理。
毫無疑問,我清楚地嗅到了代碼的壞味道。重構(gòu)勢在必行。一方面,這個分支的處理是不合理的,隨著消息類型的增多,這條分支語句會越來越長。關(guān)鍵是這種處理接收消息的邏輯不止存在這一處,這種沒有封裝的實現(xiàn)方式可能導(dǎo)致出現(xiàn)重復(fù)代碼,違背了DRY原則。另一方面,則是對ExpectedMessage的處理。它分散在多個測試步驟中,有的放在AddUpdateCustomerSteps,有的則被提取到AbstractSteps類。從職責(zé)分配的角度看,測試步驟本身并不應(yīng)該承擔(dān)創(chuàng)建或獲取ExpectedMessage的職責(zé)。
重構(gòu)的目標(biāo)就是MessageReceiver接口。我首先查看了MessageReceiver的實現(xiàn)類與調(diào)用者,發(fā)現(xiàn)其實現(xiàn)類只有一個,即DefaultMessageReceiver。調(diào)用者則非常多,調(diào)用的方法為getMessageFor()。
這個方法正是我要操刀手術(shù)的目標(biāo)方法。我希望它能夠返回ResponseMessage自治對象,而非MessageReader。這意味著我們既需要修改方法的簽名,同時還需要修改實現(xiàn)。修改方法簽名會影響到調(diào)用的依賴點。在依賴點較多的情況下,這種重構(gòu)需要謹(jǐn)慎處理。
4
我認(rèn)為,在重構(gòu)時首先需要明確重構(gòu)的目的是什么,然后還需要理清楚整個重構(gòu)的步驟,最后有條不紊地實施重構(gòu)。
顯然,我們的目的是希望消除分支語句,并以統(tǒng)一的方式對各種類型的返回消息進(jìn)行處理。根據(jù)對自治對象的分析,這意味著需要賦予ResponseMessage以行為,使得它自身就能夠處理對ExpectedMessage的驗證。
由于創(chuàng)建ExpectedMessage的邏輯是分散的,因此,我們需要首先對這部分功能進(jìn)行重構(gòu)。以getExpectedSO05ResponseFor(name)方法的重構(gòu)為例。該方法的實現(xiàn)如下所示:
private MessageReader getExpectedSO05ResponseFor(String name) {
MessageWriter writer;
if (scenarioContext.hasExpectedMessage() &&
scenarioContext.getExpectedMessage().getMessageType() == SO05) {
writer = scenarioContext.getExpectedMessage();
} else {
writer = transformerFactory.transformerFor(scenarioContext.getRequestMessage(), SO05)
.forCustomer(name)
.transform();
}
writer.selectBlock(SO05_MESSAGE_HEADER);
writer.setFieldValue(MESSAGE_HEADER.USER_ID, null);
writer.selectBlock(SO05_PROFILE);
String identifier = storyContext.getCustomerIdentifier(name);
writer.setFieldValue(PROFILE.PROFILE_ID, identifier);
String customerVersion = scenarioContext.getCustomerVersion();
writer.setFieldValue(PROFILE.USER_COUNT, customerVersion);
if (writer.selectBlockIfExists(SO05_INDIVIDUAL)) {
writer.setFieldValue(INDIVIDUAL_CUSTOMER_TYPE, null);
}
return messageFactory.readFor(SO05, writer.toString());
}
我們需要定義一個專門的對象來承擔(dān)這一職責(zé),因此,我引入了一個新對象ExpectedMessageFactory。通過采用Move Method手法(快捷鍵為F6,指IntelliJ下的快捷鍵,下同)可以完成這一重構(gòu)。
若要通過IDE自動幫助我們完成這一重構(gòu),就首先需要將該方法定義為static方法。然而,觀察該方法的實現(xiàn),它調(diào)用了許多字段值,例如scenarioContext,transformFactory等。由于這些字段并非static的,一旦將方法設(shè)置為static,使用這些字段就會提示錯誤。因此,在進(jìn)行Move Method重構(gòu)之前,需要首先將該方法調(diào)用的字段提取為參數(shù),即運(yùn)用Extract Parameter重構(gòu)手法(快捷鍵為Ctrl+Alt+P)。如果該方法還調(diào)用了其他方法,則需要分析了解這些方法存在多少依賴,從職責(zé)上看是否也需要轉(zhuǎn)移?如果只有重構(gòu)的目標(biāo)方法調(diào)用了它,則可以將方法內(nèi)聯(lián)(快捷鍵位Ctrl+ALT+N)。
做好這些準(zhǔn)備工作后,就可以移動方法了。所有的這些手法,IDE都提供了自動重構(gòu)的工具,所以并不須要擔(dān)心這樣的重構(gòu)會引入新的問題。轉(zhuǎn)移了方法后,原來的依賴點就自動改為對靜態(tài)方法的調(diào)用。由于我們還需要再將其修改為非靜態(tài)方法,此時,我們需要手動地修改所有原來對靜態(tài)方法的調(diào)用。同時,對于當(dāng)初為了移動方便而提取出來的參數(shù),在移動到新類后,還需要恢復(fù)其原有地位,即將這些參數(shù)再提取為字段(快捷鍵為Ctrl+ALT+F)。之所以要這樣做,一方面可以減少方法的參數(shù),使得方法變得更為簡潔,另一方面也可以提高類的內(nèi)聚性。在轉(zhuǎn)移了方法后,我還對原方法進(jìn)行了Extract Method重構(gòu)(快捷鍵為Ctrl+ALT+M):
private MessageReader getExpectedSO05ResponseFor(String name) {
MessageWriter writer = initializeExpectedMessage(name, SO05);
setSO05MessageHeader(writer);
setSO05Profile(name, writer);
setSO05Individual(writer);
return messageFactory.readFor(SO05, writer.toString());
}
private MessageWriter initializeExpectedMessage(String name, MessageType messageType) {
MessageWriter messageWriter;
if (scenarioContext.hasExpectedMessage() &&
scenarioContext.getExpectedMessage().getMessageType() == messageType) {
writer = scenarioContext.getExpectedMessage();
} else {
writer = transformerFactory.transformerFor(scenarioContext.getRequestMessage(), messageType)
.forCustomer(name)
.transform();
}
return messageWriter;
}
private void setSO05MessageHeader(MessageWriter writer) {
writer.selectBlock(SO05_MESSAGE_HEADER);
writer.setFieldValue(MESSAGE_HEADER.USER_ID, null);
}
private void setSO05Profile(String name, MessageWriter writer) {
writer.selectBlock(SO05_PROFILE);
String identifier = storyContext.getCustomerIdentifier(name);
writer.setFieldValue(PROFILE.PROFILE_ID, identifier);
String customerVersion = scenarioContext.getCustomerVersion();
writer.setFieldValue(PROFILE.USER_COUNT, customerVersion);
}
private void setSO05Individual(MessageWriter writer) {
if (writer.selectBlockIfExists(SO05_INDIVIDUAL)) {
writer.setFieldValue(INDIVIDUAL_CUSTOMER_TYPE, null);
}
}
通過對方法的提取,一方面以能表達(dá)功能意圖的方法名提高代碼的可讀性,另一方面還能通過這種重構(gòu)發(fā)現(xiàn)可能重用的方法,例如上面代碼片段中的initializeExpectedMessage(),就是在經(jīng)過提取方法的重構(gòu)后,才發(fā)現(xiàn)其實對于SO07消息而言,同樣存在相同的初始化邏輯。
private MessageWriter getExpectedSO07WriterFor(String name) {
MessageWriter writer = initializeExpectedMessage(name, SO07);
setSO07Details(name, writer);
setSO07Blocks(name, writer);
return writer;
}
5
在完成對ExpectedMessage創(chuàng)建功能的重構(gòu)后,接下來就可以考慮處理MessageReceiver了。
看起來,我們必須修改getMessageFor()方法的簽名。一種穩(wěn)妥的做法是暫時保留該方法,然后引入一個新方法,并在新方法中調(diào)用getMessageFor()方法。不過,這種方式需要我們手動地去修改所有依賴點;另一種做法則是先通過提取方法的方式,將原有g(shù)etMessageFor()的所有實現(xiàn)提取到一個私有方法中,然后再直接利用修改方法簽名的重構(gòu)手法(快捷鍵為Ctrl+F6),直接修改getMessageFor()。這樣做的好處是IDE工具可以直接幫助你修改所有的依賴點,同時還能夠保留原有的實現(xiàn)。
為了更好地表達(dá)方法的意圖,我還對該方法進(jìn)行了更名重構(gòu)(快捷鍵為Shift+F6),將其重命名為getResponseMessage()。由于方法的返回值發(fā)生了變化,所以依賴該方法的地方都會出現(xiàn)返回值類型不吻合的提示。在IntelliJ中,我們可以很容易地找到這些提示位置,并直接通過Alt+Enter根據(jù)工具給出的提示,來改變返回值類型。
改變了返回值類型并不意味著完事大吉,因為后面對該返回類型的調(diào)用,即前面提到的那段分支語句,仍然是不一致的。原來使用的是MessageReader對象,現(xiàn)在變成ResponseMessage對象了。這就需要我們手動地修改這些調(diào)用。當(dāng)然,也有一種取巧的辦法,就是將這些代碼結(jié)合Extract Method與Move Method重構(gòu)手法,再轉(zhuǎn)移到我們引入的ResponseMessage中,因為在我們之前的分析中,已經(jīng)明確這些分支判斷邏輯應(yīng)該封裝到ResponseMessage對象。最終的重構(gòu)結(jié)果為:
public abstract class ResponseMessage {
public ResponseMessage(MessageReader messageReader) {
this.messageReader = messageReader;
}
public void check(MessageReader expectedMessage) {
messageCheckFactory.checkerFor(getMessageType(), expectedMessage, getMessageText()).checkResponse();
}
protected abstract MessageType getMessageType();
}
public class SO05ResponseMessage extends ResponseMessage {
public SO05ResponseMessage(MessageReader messageReader) {
super(messageReader);
}
@Override
protected MessageType getMessageType() {
return MessageType.SO05;
}
}
public class DefaultMessageReceiver {
public ResponseMessage getResponseMessage(MessageType type, String identifier, GCISQueue queue) {
MessageReader messageReader = getMessageFor(type, identifier, queue);
return createResponseMessage(type, messageReader, identifer);
}
private MessageReader getMessageFor(MessageType type, String identifier, GCISQueue queue) {
MessageReader reader = getCachedMessageFor(type, identifier, queue);
while (reader == null) {
reader = getMessageFromQueueFor(type, identifer, queue);
}
return reader;
}
private ResponseMessage createResponseMessage(MessageType messageType, MessageReader messageReader, String identifer) {
ResponseMessage message = null;
switch (messageType) {
case SO05:
message = new SO05ResponseMessage(messageReader);
break;
case SO07:
message = new SO07ResponseMessage(messageReader);
break;
case SO08:
message = new SO08ResponseMessage(messageReader);
break;
}
message.setMessageCheckFactory(messageCheckFactory);
return message;
}
}
//invoker
public class AddUpdateProductSystemCustomerSteps extends AbstractCustomerExpectationSteps {
private void checkPropagationQueueByName(String name, Queue queue, MessageType messageType) {
ResponseMessage responseMessage = messageReceiver.getMessageFor(messageType, identifier, queue);
}
}
6
掌握重構(gòu)的技巧并不難,關(guān)于在于你必須要有好的嗅覺,能夠及時發(fā)現(xiàn)代碼的壞味道。
然而,即使你擁有高超的重構(gòu)技藝,如果未能養(yǎng)成隨時重構(gòu)的好習(xí)慣,又能如何?換言之,重構(gòu)能力體現(xiàn)的是你的專業(yè)技能,而重構(gòu)習(xí)慣體現(xiàn)的則是你的職業(yè)素養(yǎng)。你是否愿意為追求高質(zhì)量的卓越代碼而為之付出時間和精力呢?你能否以好的結(jié)果來說服客戶尊重你的重構(gòu)成果呢?我覺得,對卓越軟件的追求,不僅限于自己,同時也需要將此理念灌輸給客戶,并使得客戶愿意為之付費(fèi)。從軟件成本來看,這種對高質(zhì)量軟件的追求或許違背了短期利益,但絕對符合軟件開發(fā)的長期利益。
所以,在下決心打磨代碼質(zhì)量之前,還是先找好重構(gòu)這塊磨刀石,并放到自己隨時伸手可及的工具箱中吧。
新聞標(biāo)題:重構(gòu):勿以善小而不為
轉(zhuǎn)載來于:http://www.5511xx.com/article/djghcgs.html


咨詢
建站咨詢
