常見(jiàn)問(wèn)題匯編">Spring MVC DispatcherServlet改造為 CSE RestServlet 常見(jiàn)問(wèn)題匯編
789
2025-04-01
有讀者看到標(biāo)題就開(kāi)始敲鍵盤(pán)了,我知道,命名不就是不能用 abc、123 命名,名字要有意義嘛,這有什么好講的?
然而,即便懂得了名字要有意義,很多程序員依然無(wú)法逃離命名沼澤。
不精準(zhǔn)的命名
什么叫精準(zhǔn)?
廢話不多說(shuō),CR 一段代碼:
public void processChapter(long chapterId) { Chapter chapter = this.repository.findByChapterId(chapterId); if (chapter == null) { throw new IllegalArgumentException("Unknown chapter [" + chapterId + "]"); t } chapter.setTranslationState(TranslationState.TRANSLATING); this.repository.save(chapter); }
1
2
3
4
5
6
7
8
9
10
看上去挺正常。
但我問(wèn)你,這段代碼在干嘛?你就需要調(diào)動(dòng)全部注意力,去認(rèn)真閱讀這段代碼,找出其中邏輯。經(jīng)過(guò)閱讀發(fā)現(xiàn),這段代碼做的就是把一個(gè)章節(jié)的翻譯狀態(tài)改成翻譯中。
為什么你需要閱讀這段代碼細(xì)節(jié),才知道這段代碼在干嘛?
問(wèn)題就在函數(shù)名,processChapter,這個(gè)函數(shù)確實(shí)是在處理章節(jié),但這個(gè)名字太寬泛。如果說(shuō)“將章節(jié)的翻譯狀態(tài)改成翻譯中”叫做處理章節(jié),那么:
“將章節(jié)的翻譯狀態(tài)改成翻譯完”
“修改章節(jié)內(nèi)容”
是不是也能叫處理章節(jié)?
所以,如果各種場(chǎng)景都能叫處理章節(jié),那么處理章節(jié)就是個(gè)寬泛名,沒(méi)有錯(cuò),但不精準(zhǔn)!
表面看,這個(gè)名字是有含義,但實(shí)際上,并不能有效反映這段代碼含義。
如果我在做的是一個(gè)信息處理系統(tǒng),你根本無(wú)法判斷,是一個(gè)電商平臺(tái),還是一個(gè)圖書(shū)管理系統(tǒng),從溝通的角度看,這就不是一個(gè)有效的溝通。要想理解它,你需要消耗大量認(rèn)知成本,無(wú)論是時(shí)間,還是精力。
命名過(guò)于寬泛,不能精準(zhǔn)描述,這是很多代碼在命名上存在的嚴(yán)重問(wèn)題,也是代碼難以理解的根源所在。
或許這么說(shuō)你的印象還是不深刻,看看下面這些詞是不是經(jīng)常出現(xiàn)在你的代碼里:data、info、flag、process、handle、build、maintain、manage、modify 等等。這些名字都屬于典型的過(guò)寬泛名字,當(dāng)這些名字出現(xiàn)在你的代碼里,多半是寫(xiě)代碼的人當(dāng)時(shí)沒(méi)有想好用什么名字,就開(kāi)始寫(xiě)代碼了。
回到前面那段代碼上,如果它不叫“處理章節(jié)”,那應(yīng)該叫什么?首先,命名要能夠描述出這段代碼在做的事情。這段代碼在做的事情就是“將章節(jié)修改為翻譯中”。那是不是它就應(yīng)該叫 changeChapterToTranlsating呢?
相比于“處理章節(jié)”,changeChapterToTranlsating這個(gè)名字已經(jīng)進(jìn)了一步,然而,它也不算是一個(gè)好名字,因?yàn)樗嗟氖窃诿枋鲞@段代碼在做的細(xì)節(jié)。
之所以要將一段代碼封裝起來(lái),是我們不想知道那么多細(xì)節(jié)。如果把細(xì)節(jié)平鋪開(kāi)來(lái),那本質(zhì)上和直接閱讀代碼細(xì)節(jié)差別不大。
所以,一個(gè)好的名字應(yīng)該描述意圖,而非細(xì)節(jié)。
就這段代碼而言, 我們?yōu)槭裁匆逊g狀態(tài)修改成翻譯中,這一定是有原因,也就是意圖。
我們把翻譯狀態(tài)修改成翻譯中,是因?yàn)槲覀冊(cè)谶@里開(kāi)啟了一個(gè)翻譯的過(guò)程。所以,這段函數(shù)應(yīng)該命名 startTranslation。
public void startTranslation(long chapterId) { Chapter chapter = this.repository.findByChapterId(chapterId); if (chapter == null) { throw new IllegalArgumentException("Unknown chapter [" + chapterId + "]"); t } chapter.setTranslationState(TranslationState.TRANSLATING); this.repository.save(chapter); }
1
2
3
4
5
6
7
8
9
10
用技術(shù)術(shù)語(yǔ)命名
我們?cè)賮?lái)看一段代碼:
List
1
常見(jiàn)得不能再常見(jiàn)的代碼,但卻隱藏另外一個(gè)典型得不能再典型的問(wèn)題:用技術(shù)術(shù)語(yǔ)命名。
這個(gè) bookList 變量之所以叫 bookList,原因就是它聲明的類型是 List。這種命名在代碼中幾乎是隨處可見(jiàn)的,比如 xxxMap、xxxSet。
這是一種不費(fèi)腦子的命名方式,但這種命名卻會(huì)帶來(lái)很多問(wèn)題,因?yàn)樗且环N基于實(shí)現(xiàn)細(xì)節(jié)的命名方式。
面向接口編程,從另外一個(gè)角度理解,就是不要面向?qū)崿F(xiàn)編程,因?yàn)榻涌谑欠€(wěn)定的,而實(shí)現(xiàn)易變。雖然在大多數(shù)人的理解里,這個(gè)原則是針對(duì)類型的,但在命名上,我們也應(yīng)該遵循同樣的原則。為什么?我舉個(gè)例子你就知道了。
比如,如果我發(fā)現(xiàn),我現(xiàn)在需要的是一個(gè)不重復(fù)的作品集合,也就是說(shuō),我需要把這個(gè)變量的類型從 List 改成 Set。變量類型你一定會(huì)改,但變量名你會(huì)改嗎?這還真不一定,一旦出現(xiàn)遺忘,就會(huì)出現(xiàn)一個(gè)奇特的現(xiàn)象,一個(gè)叫 bookList 的變量,它的類型是一個(gè) Set。這樣,一個(gè)新的混淆產(chǎn)生了。
有什么更好的名字嗎?我們需要一個(gè)更面向意圖的名字。其實(shí),我們?cè)谶@段代碼里真正要表達(dá)的是拿到了一堆書(shū),所以,這個(gè)名字可以命名成 books。
List books = service.getBooks();
這個(gè)名字其實(shí)更簡(jiǎn)單,但從表意的程度上來(lái)說(shuō),它卻是一個(gè)更有效的名字。
雖然這里我們只是以變量為例說(shuō)明了以技術(shù)術(shù)語(yǔ)命名存在的問(wèn)題,事實(shí)上,在實(shí)際的代碼中,技術(shù)名詞的出現(xiàn),往往就代表著它缺少了一個(gè)應(yīng)有的模型。
比如,在業(yè)務(wù)代碼里如果直接出現(xiàn)了 Redis:
public Book getByIsbn(String isbn) { Book cachedBook = redisBookStore.get(isbn); if (cachedBook != null) { return cachedBook; } Book book = doGetByIsbn(isbn); redisBookStore.put(isbn, book); return book; }
1
2
3
4
5
6
7
8
9
10
通常來(lái)說(shuō),這里真正需要的是一個(gè)緩存。Redis 是緩存這個(gè)模型的一個(gè)實(shí)現(xiàn):
public Book getByIsbn(String isbn) { Book cachedBook = cache.get(isbn); if (cachedBook != null) { return cachedBook; } Book book = doGetByIsbn(isbn); cache.put(isbn, book); return book; }
1
2
3
4
5
6
7
8
9
10
再進(jìn)一步,緩存這個(gè)概念其實(shí)也是一個(gè)技術(shù)術(shù)語(yǔ),從某種意義上說(shuō),它也不應(yīng)該出現(xiàn)在業(yè)務(wù)代碼。
這方面做得比較好的是 Spring。使用 Spring 框架時(shí),如果需要緩存,我們通常是加上一個(gè) Annotation(注解):
@Cacheable("books") public Book getByIsbn(String isbn) { ... }
1
2
3
4
之所以喜歡用技術(shù)名詞去命名,一方面是因?yàn)椋@是習(xí)慣的語(yǔ)言,另一方面也是因?yàn)閷W(xué)寫(xiě)代碼,很大程度上是參考別人代碼,而行業(yè)里面優(yōu)秀的代碼常常是一些開(kāi)源項(xiàng)目,而這些開(kāi)源項(xiàng)目往往是技術(shù)類項(xiàng)目。在一個(gè)技術(shù)類的項(xiàng)目中,這些技術(shù)術(shù)語(yǔ)其實(shí)就是它的業(yè)務(wù)語(yǔ)言。但對(duì)于業(yè)務(wù)項(xiàng)目,這個(gè)說(shuō)法就必須重新審視了。
如果這個(gè)部分的代碼確實(shí)就是處理一些技術(shù),使用技術(shù)術(shù)語(yǔ)無(wú)可厚非,但如果是在處理業(yè)務(wù),就要盡可能把技術(shù)術(shù)語(yǔ)隔離開(kāi)來(lái)。
xxxMap這種命名表示映射關(guān)系,比如:書(shū)id與書(shū)的映射關(guān)系,不能命名為bookIdMap么?
Map 表示的是一個(gè)數(shù)據(jù)結(jié)構(gòu),而映射關(guān)系我會(huì)寫(xiě)成 Mapping
用業(yè)務(wù)語(yǔ)言寫(xiě)代碼
無(wú)論是不精準(zhǔn)的命名也好,技術(shù)名詞也罷,歸根結(jié)底,體現(xiàn)的是同一個(gè)問(wèn)題:對(duì)業(yè)務(wù)理解不到位。
編寫(xiě)可維護(hù)的代碼要使用業(yè)務(wù)語(yǔ)言。怎么才知道自己的命名是否用的是業(yè)務(wù)語(yǔ)言呢?
把這個(gè)詞講給產(chǎn)品經(jīng)理,看他知不知道是怎么回事。
從團(tuán)隊(duì)的角度看,讓每個(gè)人根據(jù)自己的理解來(lái)命名,確實(shí)就有可能出現(xiàn)千奇百怪的名字,所以,一個(gè)良好的團(tuán)隊(duì)實(shí)踐是,建立團(tuán)隊(duì)的詞匯表,讓團(tuán)隊(duì)成員有信息可以參考。
團(tuán)隊(duì)對(duì)于業(yè)務(wù)有了共同理解,我們也許就可以發(fā)現(xiàn)一些更高級(jí)的壞味道,比如說(shuō)下面這個(gè)函數(shù)聲明:
public void approveChapter(long chapterId, long userId) { ... }
1
2
3
確認(rèn)章節(jié)內(nèi)容審核通過(guò)。這里有一個(gè)問(wèn)題,chapterId 是審核章節(jié)的 ID,這個(gè)沒(méi)問(wèn)題,但 userId 是什么呢?了解了一下背景,我們才知道,之所以這里要有一個(gè) userId,是因?yàn)檫@里需要記錄一下審核人的信息,這個(gè) userId 就是審核人的 userId。
你看,通過(guò)業(yè)務(wù)的分析,我們會(huì)發(fā)現(xiàn),這個(gè) userId 并不是一個(gè)好的命名,因?yàn)樗€需要更多的解釋,更好的命名是 reviewerUserId,之所以起這個(gè)名字,因?yàn)檫@個(gè)用戶在這個(gè)場(chǎng)景下扮演的角色是審核人(Reviewer)。
public void approveChapter(long chapterId, long reviewerUserId) { ... }
1
2
3
這個(gè)壞味道也是一種不精準(zhǔn)的命名,但它不是那種一眼可見(jiàn)的壞味道,而是需要在業(yè)務(wù)層面上再進(jìn)行討論,所以,它是一種更高級(jí)的壞味道。
能夠意識(shí)到自己的命名有問(wèn)題,是程序員進(jìn)階的第一步。
@GetMapping("getTotalSettlementInfoByYear") @ApiOperation("公司結(jié)算信息按年求和") public Result> getTotalSettlementInfoByYear(@RequestParam String year) { List
>().ok(list); }
1
2
3
4
5
6
名字長(zhǎng)不是問(wèn)題,問(wèn)題是表達(dá)是否清晰,像repMonthCompanyService這個(gè)名字,是不太容易一眼看出來(lái)含義的。
另外,傳給 service 的參數(shù)是一個(gè)字符串,這個(gè)從邏輯上是有問(wèn)題的,沒(méi)有進(jìn)行參數(shù)的校驗(yàn)。后面的內(nèi)容也會(huì)講到,這個(gè)做法是一種缺乏封裝的表現(xiàn)。
變量名是 list,按照這一講的說(shuō)法是用技術(shù)術(shù)語(yǔ)在命名。
再有,這個(gè) URI 是 getTotalSettlementInfoByYear,這是不符合 REST 的命名規(guī)范的,比如,動(dòng)詞不應(yīng)該出現(xiàn)在 URI 里,分詞應(yīng)該是“-”,byYear 實(shí)際上是一個(gè)過(guò)濾條件等等。
總結(jié)
兩個(gè)典型的命名壞味道:
不精準(zhǔn)的命名;
用技術(shù)術(shù)語(yǔ)命名。
命名是軟件開(kāi)發(fā)中兩件難事之一(另一個(gè)難事是緩存失效),不好的命名本質(zhì)上是增加我們的認(rèn)知成本,同樣也增加了后來(lái)人(包括我們自己)維護(hù)代碼的成本。
好的命名要體現(xiàn)出這段代碼在做的事情,而無(wú)需展開(kāi)代碼了解其中的細(xì)節(jié)
再進(jìn)一步,好的命名要準(zhǔn)確地體現(xiàn)意圖,而不是實(shí)現(xiàn)細(xì)節(jié)
更高的要求是,用業(yè)務(wù)語(yǔ)言寫(xiě)代碼
好的命名,是體現(xiàn)業(yè)務(wù)含義的命名。
機(jī)器翻譯
版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實(shí)的內(nèi)容,請(qǐng)聯(lián)系我們jiasou666@gmail.com 處理,核實(shí)后本網(wǎng)站將在24小時(shí)內(nèi)刪除侵權(quán)內(nèi)容。
版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實(shí)的內(nèi)容,請(qǐng)聯(lián)系我們jiasou666@gmail.com 處理,核實(shí)后本網(wǎng)站將在24小時(shí)內(nèi)刪除侵權(quán)內(nèi)容。