您好,登錄后才能下訂單哦!
這篇文章給大家介紹Java代碼重構(gòu)的方法到底多長(zhǎng)才算長(zhǎng),內(nèi)容非常詳細(xì),感興趣的小伙伴們可以參考借鑒,希望對(duì)大家能有所幫助。
100 行?對(duì)于函數(shù)長(zhǎng)度容忍度太高了!這是導(dǎo)致長(zhǎng)函數(shù)產(chǎn)生的關(guān)鍵點(diǎn)。
看具體代碼時(shí),一定要能夠看到細(xì)微之處。關(guān)鍵點(diǎn)就是將任務(wù)拆解得越小越好,這個(gè)觀點(diǎn)對(duì)代碼同樣適用。隨著對(duì)代碼長(zhǎng)度容忍度的降低,對(duì)代碼細(xì)節(jié)的感知力就會(huì)逐漸提升,你才能看到那些原本所謂細(xì)枝末節(jié)的地方隱藏的各種問(wèn)題。
“越小越好”是一個(gè)追求的目標(biāo),不過(guò),沒(méi)有一個(gè)具體的數(shù)字,就沒(méi)辦法約束所有人的行為。所以,通常情況下,我們還是要定義出一個(gè)代碼行數(shù)的上限,以保證所有人都可以按照這個(gè)標(biāo)準(zhǔn)執(zhí)行。
像 Java 這樣表達(dá)能力稍弱的靜態(tài)類型語(yǔ)言,爭(zhēng)取 20 行代碼解決問(wèn)題。
這不是一個(gè)說(shuō)說(shuō)就算的標(biāo)準(zhǔn),我們應(yīng)該把它變成一個(gè)可執(zhí)行的標(biāo)準(zhǔn)。比如,在 Java 中,我們就可以把代碼行的約束加到 CheckStyle 的配置文件:
<module name="MethodLength"> <property name="tokens" value="METHOD_DEF"/> <property name="max" value="20"/> <property name="countEmpty" value="false"/> </module>
這樣,在我們提交代碼之前,執(zhí)行本地的構(gòu)建腳本,就可以把長(zhǎng)函數(shù)檢測(cè)出來(lái)。
即便以 20 行上限,這也已經(jīng)超過(guò)很多人的認(rèn)知,具體的函數(shù)行數(shù)可以結(jié)合團(tuán)隊(duì)的實(shí)際情況來(lái)制定。
非常不建議把這個(gè)數(shù)字放得很大,就像我前面說(shuō)的那樣,如果你放到 100 行,這個(gè)數(shù)字基本上是沒(méi)有太多意義的,對(duì)團(tuán)隊(duì)也起不到什么約束作用。
如果函數(shù)里面的行寫(xiě)得很長(zhǎng)呢?還應(yīng)不應(yīng)該插入換行?如果插入換行的話就會(huì)增加行數(shù),如果不差入換行,在看代碼時(shí)就要經(jīng)常移動(dòng)水平滾動(dòng)條,按代碼行而非物理行計(jì)數(shù)。
限制函數(shù)長(zhǎng)度,是一種簡(jiǎn)單粗暴的解決方案。最重要的是你要知道,長(zhǎng)函數(shù)本身是一個(gè)結(jié)果,如果不理解長(zhǎng)函數(shù)產(chǎn)生的原因,還是很難寫(xiě)出整潔的代碼。
像 C 語(yǔ)言這種在今天已經(jīng)是高性能的程序設(shè)計(jì)語(yǔ)言,在問(wèn)世之初,也曾被人質(zhì)疑性能不彰,尤其是函數(shù)調(diào)用。
在一些寫(xiě)匯編語(yǔ)言的人看來(lái),調(diào)用函數(shù)涉及到入棧出棧的過(guò)程,顯然不如直接執(zhí)行來(lái)得性能高。這種想法經(jīng)過(guò)各種演變流傳到今天,任何一門(mén)新語(yǔ)言出現(xiàn),還是會(huì)以同樣的理由被質(zhì)疑。
所以,在很多人看來(lái),把函數(shù)寫(xiě)長(zhǎng)是為了所謂性能。不過(guò),這個(gè)觀點(diǎn)在今天是站不住的。性能優(yōu)化不該是寫(xiě)代碼的第一考量:
有活力的程序設(shè)計(jì)語(yǔ)言本身是不斷優(yōu)化的,無(wú)論是編譯器,還是運(yùn)行時(shí),性能都會(huì)越來(lái)越好
可維護(hù)性比性能優(yōu)化要優(yōu)先考慮,當(dāng)性能不足以滿足需要時(shí),我們?cè)賮?lái)做相應(yīng)的測(cè)量,找到焦點(diǎn),進(jìn)行特定的優(yōu)化。這比在寫(xiě)代碼時(shí)就考慮所謂性能要更能鎖定焦點(diǎn),優(yōu)化才有意義。
寫(xiě)代碼平鋪直敘,把自己想到的一點(diǎn)點(diǎn)羅列出來(lái)。比如下面這段代碼(如果你不想仔細(xì)閱讀,可以直接跳到后面):
public void executeTask() { ObjectMapper mapper = new ObjectMapper(); CloseableHttpClient client = HttpClients.createDefault(); List<Chapter> chapters = this.chapterService.getUntranslatedChapters(); for (Chapter chapter : chapters) { // Send Chapter SendChapterRequest sendChapterRequest = new SendChapterRequest(); sendChapterRequest.setTitle(chapter.getTitle()); sendChapterRequest.setContent(chapter.getContent()); HttpPost sendChapterPost = new HttpPost(sendChapterUrl); CloseableHttpResponse sendChapterHttpResponse = null; String chapterId = null; try { String sendChapterRequestText = mapper.writeValueAsString(sendChapterRequest); sendChapterPost.setEntity(new StringEntity(sendChapterRequestText)); sendChapterHttpResponse = client.execute(sendChapterPost); HttpEntity sendChapterEntity = sendChapterPost.getEntity(); SendChapterResponse sendChapterResponse = mapper.readValue(sendChapterEntity.getContent(), SendChapterResponse.class); chapterId = sendChapterResponse.getChapterId(); } catch (IOException e) { throw new RuntimeException(e); } finally { try { if (sendChapterHttpResponse != null) { sendChapterHttpResponse.close(); } } catch (IOException e) { // ignore } } // Translate Chapter HttpPost translateChapterPost = new HttpPost(translateChapterUrl); CloseableHttpResponse translateChapterHttpResponse = null; try { TranslateChapterRequest translateChapterRequest = new TranslateChapterRequest(); translateChapterRequest.setChapterId(chapterId); String translateChapterRequestText = mapper.writeValueAsString(translateChapterRequest); translateChapterPost.setEntity(new StringEntity(translateChapterRequestText)); translateChapterHttpResponse = client.execute(translateChapterPost); HttpEntity translateChapterEntity = translateChapterHttpResponse.getEntity(); TranslateChapterResponse translateChapterResponse = mapper.readValue(translateChapterEntity.getContent(), TranslateChapterResponse.class); if (!translateChapterResponse.isSuccess()) { logger.warn("Fail to start translate: {}", chapterId); } } catch (IOException e) { throw new RuntimeException(e); } finally { if (translateChapterHttpResponse != null) { try { translateChapterHttpResponse.close(); } catch (IOException e) { // ignore } } } }
把沒(méi)有翻譯過(guò)的章節(jié)發(fā)到翻譯引擎,然后,啟動(dòng)翻譯過(guò)程。
翻譯引擎是另外一個(gè)服務(wù),需通過(guò) HTTP 的形式向它發(fā)送請(qǐng)求。相對(duì)而言,這段代碼還算直白,當(dāng)你知道了我上面所說(shuō)的邏輯,你是很容易看懂這段代碼。
這段代碼之所以很長(zhǎng),主要原因就是把前面所說(shuō)的邏輯全部平鋪直敘地?cái)[在那里了,這里既有業(yè)務(wù)處理的邏輯,比如,把章節(jié)發(fā)送給翻譯引擎,然后,啟動(dòng)翻譯過(guò)程;又有處理的細(xì)節(jié),比如,把對(duì)象轉(zhuǎn)成 JSON,然后,通過(guò) HTTP 客戶端發(fā)送出去。
從這段代碼中,可看到平鋪直敘的代碼存在的兩個(gè)典型問(wèn)題:
把多個(gè)業(yè)務(wù)處理流程放在一個(gè)函數(shù)里實(shí)現(xiàn)
把不同層面的細(xì)節(jié)放到一個(gè)函數(shù)里實(shí)現(xiàn)
這里發(fā)送章節(jié)和啟動(dòng)翻譯是兩個(gè)過(guò)程,顯然,這是可以放到兩個(gè)不同的函數(shù)中去實(shí)現(xiàn)的,所以,我們只要做一下提取函數(shù),就可以把這個(gè)看似龐大的函數(shù)拆開(kāi),而拆出來(lái)的幾個(gè)函數(shù)規(guī)模都會(huì)小很多,像下面這樣:
public void executeTask() { ObjectMapper mapper = new ObjectMapper(); CloseableHttpClient client = HttpClients.createDefault(); List<Chapter> chapters = this.chapterService.getUntranslatedChapters(); for (Chapter chapter : chapters) { String chapterId = sendChapter(mapper, client, chapter); translateChapter(mapper, client, chapterId); } }
拆出來(lái)的部分,實(shí)際上就是把對(duì)象打包發(fā)送的過(guò)程,我們以發(fā)送章節(jié)為例,先來(lái)看拆出來(lái)的發(fā)送章節(jié)部分:
private String sendChapter(final ObjectMapper mapper, final CloseableHttpClient client, final Chapter chapter) { SendChapterRequest request = asSendChapterRequest(chapter); CloseableHttpResponse response = null; String chapterId = null; try { HttpPost post = sendChapterRequest(mapper, request); response = client.execute(post); chapterId = asChapterId(mapper, post); } catch (IOException e) { throw new RuntimeException(e); } finally { try { if (response != null) { response.close(); } } catch (IOException e) { // ignore } } return chapterId; } private HttpPost sendChapterRequest(final ObjectMapper mapper, final SendChapterRequest sendChapterRequest) throws JsonProcessingException, UnsupportedEncodingException { HttpPost post = new HttpPost(sendChapterUrl); String requestText = mapper.writeValueAsString(sendChapterRequest); post.setEntity(new StringEntity(requestText)); return post; } private String asChapterId(final ObjectMapper mapper, final HttpPost sendChapterPost) throws IOException { String chapterId; HttpEntity entity = sendChapterPost.getEntity(); SendChapterResponse response = mapper.readValue(entity.getContent(), SendChapterResponse.class); chapterId = response.getChapterId(); return chapterId; } private SendChapterRequest asSendChapterRequest(final Chapter chapter) { SendChapterRequest request = new SendChapterRequest(); request.setTitle(chapter.getTitle()); request.setContent(chapter.getContent()); return request
這個(gè)代碼還算不上已經(jīng)處理得很整潔了,但至少同之前相比,已經(jīng)簡(jiǎn)潔了一些。我們只用了最簡(jiǎn)單的提取函數(shù)這個(gè)重構(gòu)手法,就把一個(gè)大函數(shù)拆分成了若干的小函數(shù)。
長(zhǎng)函數(shù)往往還隱含著一個(gè)命名問(wèn)題。如果你看修改后的sendChapter,其中的變量命名明顯比之前要短,理解的成本也相應(yīng)地會(huì)降低。因?yàn)樽兞慷际窃谶@個(gè)短小的上下文里,也就不會(huì)產(chǎn)生那么多的命名沖突,變量名當(dāng)然就可以寫(xiě)短一些。
平鋪直敘的代碼,一個(gè)關(guān)鍵點(diǎn)就是沒(méi)有把不同的東西分解出來(lái)。如果我們用設(shè)計(jì)的眼光衡量這段代碼,這就是“分離關(guān)注點(diǎn)”沒(méi)有做好,把不同層面的東西混在了一起,既有不同業(yè)務(wù)混在一起,也有不同層次的處理混在了一起。我在《軟件設(shè)計(jì)之美》專欄中,也曾說(shuō)過(guò),關(guān)注點(diǎn)越多越好,粒度越小越好。
有時(shí),一段代碼一開(kāi)始的時(shí)候并不長(zhǎng),就像下面這段代碼,它根據(jù)返回的錯(cuò)誤進(jìn)行相應(yīng)地錯(cuò)誤處理:
if (code == 400 || code == 401) { // 做一些錯(cuò)誤處理 }
然后,新的需求來(lái)了,增加了新的錯(cuò)誤碼,它就變成了這個(gè)樣子:
if (code == 400 || code == 401 || code == 402) { // 做一些錯(cuò)誤處理 }
這段代碼有很多次被修改的機(jī)會(huì),日積月累:
if (code == 400 || code == 401 || code == 402 || ... || code == 500 || ... || ... || code == 10000 || ...) { }
后人看到就想罵人。任何代碼都經(jīng)不起這種無(wú)意識(shí)的累積,每個(gè)人都沒(méi)做錯(cuò),但最終的結(jié)果很糟糕。對(duì)抗這種逐漸糟糕腐壞的代碼,需要知道“童子軍軍規(guī)”:
讓營(yíng)地比你來(lái)時(shí)更干凈。
Robert Martin 把它借鑒到了編程領(lǐng)域,我們應(yīng)該看看自己對(duì)于代碼的改動(dòng)是不是讓原有的代碼變得更糟糕了,如果是,那就改進(jìn)它。
但這一切的前提是,你要能看出自己的代碼是不是讓原有的代碼變得糟糕了,所以,學(xué)習(xí)代碼的壞味道還是很有必要的。
至此,我們看到了代碼變長(zhǎng)的幾種常見(jiàn)原因:
以性能為由
平鋪直敘
一次加一點(diǎn)
代碼變長(zhǎng)根本是一個(gè)無(wú)意識(shí)的問(wèn)題,寫(xiě)代碼的人沒(méi)有覺(jué)得自己把代碼破壞了。但只要你認(rèn)識(shí)到長(zhǎng)函數(shù)是一個(gè)壞味道,后面的許多問(wèn)題就自然而然地會(huì)被發(fā)掘出來(lái),至于解決方案,你已經(jīng)看到了,大部分情況下,就是拆分成各種小函數(shù)。
沒(méi)有人愿意去閱讀長(zhǎng)函數(shù),但許多人又會(huì)不經(jīng)意間寫(xiě)出長(zhǎng)函數(shù)。
對(duì)于團(tuán)隊(duì),一個(gè)關(guān)鍵點(diǎn)是要定義出長(zhǎng)函數(shù)的標(biāo)準(zhǔn)。
過(guò)于寬泛的標(biāo)準(zhǔn)沒(méi)有意義,想要有效地控制函數(shù)規(guī)模,幾十行已經(jīng)是標(biāo)準(zhǔn)上限,這個(gè)標(biāo)準(zhǔn)越低越好。
長(zhǎng)函數(shù)產(chǎn)生的原因:
性能為借口
代碼平鋪直敘
函數(shù)寫(xiě)長(zhǎng)最常見(jiàn)的原因。之所以會(huì)把代碼平攤在那里:
- 把多個(gè)業(yè)務(wù)寫(xiě)到了一起
- 把不同層次的代碼寫(xiě)到了一起。究其根因,那是“分離關(guān)注點(diǎn)”沒(méi)有做好
每人每次加一點(diǎn)點(diǎn)
應(yīng)對(duì)主要辦法就是要堅(jiān)守“童子軍軍規(guī)”,但其背后更深層次的支撐就是要對(duì)壞味道有著深刻的認(rèn)識(shí)
把函數(shù)寫(xiě)短,越短越好。
關(guān)于Java代碼重構(gòu)的方法到底多長(zhǎng)才算長(zhǎng)就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,可以學(xué)到更多知識(shí)。如果覺(jué)得文章不錯(cuò),可以把它分享出去讓更多的人看到。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。