作者:京東保險(xiǎn) 王奕龍
到本節(jié) Mybatis 源碼中核心邏輯基本已經(jīng)介紹完了,在這里我想借助 Mybatis 其他部分源碼來介紹一些我認(rèn)為在編程中能 最快提高編碼質(zhì)量的小方法,它們可能比較細(xì)碎,希望能對大家有所啟發(fā)。
關(guān)于方法的長度和方法拆分
之前我在讀完《代碼整潔之道》時(shí),非常癡迷于寫小方法這件事,它強(qiáng)調(diào)“每個(gè)方法只做一件事,方法的長度不能超過 5 行”等觀點(diǎn)。
記得某次代碼評審時(shí),有同事對將一個(gè)大方法拆分成多個(gè)小方法提出了異議:拆分出的小方法不能算作做了一件事,它們都只是大方法中的一個(gè)“動(dòng)作”而已,所以不應(yīng)該拆分巴拉巴拉。
這個(gè)觀點(diǎn)讓我說不出什么,后來我也在想:如果按照這個(gè)觀點(diǎn),多大的方法都可以概括成只做了一件事,那么我們就需要將所有的邏輯都“攤”到一個(gè)方法中嗎?我覺得拆分方法目的不是在界定一件事還是一個(gè)動(dòng)作上,而是 關(guān)注方法的可讀性,拆分方法太多確實(shí)讓代碼變得不好讀,需要輾轉(zhuǎn)在多個(gè)方法之間,但是不拆的可讀性也會(huì)差,所以接下來我想根據(jù) Mybatis 這段代碼來簡單談?wù)勎覍懛椒ǖ挠^點(diǎn):
public class XMLConfigBuilder extends BaseBuilder { private void parseConfiguration(XNode root) { try { propertiesElement(root.evalNode("properties")); Properties settings = settingsAsProperties(root.evalNode("settings")); loadCustomVfsImpl(settings); loadCustomLogImpl(settings); typeAliasesElement(root.evalNode("typeAliases")); pluginsElement(root.evalNode("plugins")); objectFactoryElement(root.evalNode("objectFactory")); objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); reflectorFactoryElement(root.evalNode("reflectorFactory")); settingsElement(settings); environmentsElement(root.evalNode("environments")); databaseIdProviderElement(root.evalNode("databaseIdProvider")); typeHandlersElement(root.evalNode("typeHandlers")); mappersElement(root.evalNode("mappers")); } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); } } }
如上是 Mybatis 解析配置文件中各個(gè)標(biāo)簽的方法,它將每個(gè)標(biāo)簽的解析都單獨(dú)定義出了一個(gè)方法,這也是我一直遵循的寫方法的觀點(diǎn):最頂層的入口方法應(yīng)該是短小清晰的步驟,在主方法中編排好方法的執(zhí)行內(nèi)容,這樣主方法便是清晰明了的執(zhí)行流程,我們便能一眼清晰的知道該方法做了什么事情,而針對各個(gè)具體的環(huán)節(jié)或者要改動(dòng)哪些邏輯,直接跳轉(zhuǎn)到對應(yīng)的方法即可。
至于該不該將某段邏輯抽象成一個(gè)方法,我的觀點(diǎn)是 能不能一眼看明白這段邏輯在干什么,如果不能,那么就應(yīng)該被抽象到一個(gè)方法中,否則將其保留在原方法中也是沒有問題的,對方法的抽象從來都不在于方法的長度,可讀性 應(yīng)得到更多的關(guān)注。
此外,還有一個(gè)能提高代碼可讀性的方法是: “合理使用換行符” ,如下代碼所示:
public class Configuration { // ... public Configuration() { typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class); typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class); typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class); typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class); typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class); typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class); typeAliasRegistry.registerAlias("FIFO", FifoCache.class); typeAliasRegistry.registerAlias("LRU", LruCache.class); typeAliasRegistry.registerAlias("SOFT", SoftCache.class); typeAliasRegistry.registerAlias("WEAK", WeakCache.class); typeAliasRegistry.registerAlias("DB_VENDOR", VendorDatabaseIdProvider.class); typeAliasRegistry.registerAlias("XML", XMLLanguageDriver.class); typeAliasRegistry.registerAlias("RAW", RawLanguageDriver.class); typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class); typeAliasRegistry.registerAlias("COMMONS_LOGGING", JakartaCommonsLoggingImpl.class); typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class); typeAliasRegistry.registerAlias("LOG4J2", Log4j2Impl.class); typeAliasRegistry.registerAlias("JDK_LOGGING", Jdk14LoggingImpl.class); typeAliasRegistry.registerAlias("STDOUT_LOGGING", StdOutImpl.class); typeAliasRegistry.registerAlias("NO_LOGGING", NoLoggingImpl.class); typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class); typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class); languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class); languageRegistry.register(RawLanguageDriver.class); } }
在 Configuration 的構(gòu)造方法中,進(jìn)行注冊別名操作時(shí)使用了換行符進(jìn)行分割,它將 TransactionFactory 相關(guān)的緊挨在一起作為一組,再將 DataSourceFactory 相關(guān)的緊挨在一起等等,這樣在分門別類查看這段代碼便是清晰的,即使它們都在一個(gè)方法中。
方法的編排
在《代碼整潔之道》中提出了代碼中 方法要從上到下排列,讀方法就像讀報(bào)紙一樣,因?yàn)榉椒ū怀橄筇釤挸鰜恚喿x時(shí)必然會(huì)造成在多個(gè)方法間切換的問題,那么如果我們將方法從上到下依次排列,能夠在屏幕中同時(shí)看到所有相關(guān)方法的話,那么這樣的確方便了閱讀,比如 methodA 依賴 commonMethod 方法的排列:
@Override public void methodA() { commonMethod(); } private void commonMethod() { // ... }
此時(shí)如果增加 methodB() 也要復(fù)用 commonMethod() 的話,那么我并不會(huì)像下面這樣排列方法:
@Override public void methodA() { commonMethod(); } private void commonMethod() { // ... } @Override public void methodB() { commonMethod(); }
因?yàn)槲覀冊诳匆粋€(gè)方法時(shí),始終要堅(jiān)持 自上往下讀 的原則,不能在看 methodB() 的時(shí)候,再跳回到上面去,而是需要像這樣:
@Override public void methodA() { commonMethod(); } @Override public void methodB() { commonMethod(); } private void commonMethod() { // ... }
那么這也就意味著:如果 某個(gè)方法被復(fù)用的次數(shù)過多,它的位置則越靠近類的下方。在《軟件設(shè)計(jì)哲學(xué)》中也提到過 專用方法上移,通用方法下移 的觀點(diǎn),這也是在提醒開發(fā)者,當(dāng)看見某個(gè)私有方法在類的尾部時(shí),它可能是一個(gè)非常通用的方法,對它的修改就需要特別謹(jǐn)慎。
方法的聲明
在業(yè)務(wù)代碼中經(jīng)常會(huì)看到接口中某方法聲明拋出異常:
public interface Demo { void method(Object parameter) throws Exception; }
但是對要拋出的異常類型并沒有明確的聲明,只知道會(huì)拋出 Exception,對于具體的原因一無所知。如果想清楚的了解,可以借助注釋(如果有的話),否則就需要去探究它的具體實(shí)現(xiàn),這對想直接調(diào)用該方法的研發(fā)人員來說非常不友好,增加了 “認(rèn)知負(fù)荷” ,那該怎么辦呢?
《圖解Java多線程設(shè)計(jì)模式》中提到過一個(gè)例子非常有啟發(fā)性,它說方法簽名中標(biāo)記 throws InterruptedException 能表示兩種含義:第一種比較容易被想到,表示該方法可以被打斷/取消;第二種含義是,這個(gè)方法耗時(shí)可能比較長。
比如 Thread.join() 方法,它聲明了 throws InterruptedException,它的作用是讓當(dāng)前執(zhí)行的線程暫停運(yùn)行,直到調(diào)用 join() 方法的線程執(zhí)行完畢。當(dāng)我們在一個(gè)線程實(shí)例上調(diào)用 join() 方法時(shí),當(dāng)前執(zhí)行的線程將被阻塞,阻塞時(shí)間可能會(huì)很長,如果在阻塞期間如果另一個(gè)線程中斷(interrupt)了它,那么它將拋出一個(gè) InterruptedException。所以,我們能夠在 throws 聲明中,獲取某方法關(guān)于某異常的信息。
在 Mybatis 源碼中也有類似的例子,如下:
public interface Executor { int update(MappedStatement ms, Object parameter) throws SQLException; }
它聲明出 throws SQLException 表示 SQL 執(zhí)行的異常,它被拋出了我們便能知道是 SQL 寫的有問題。我認(rèn)為直接將方法上聲明 throws Exception 的簽名并不添加任何注釋是一種懶惰。異常精細(xì)化能給我們帶來很多好處,比如日常報(bào)警容易看,增加方法可讀性,能夠通過聲明知道這個(gè)方法會(huì)拋出關(guān)于什么類型的異常,便能讓接口的調(diào)用者判斷是處理異常還是拋出異常。
方法的參數(shù)聲明也很重要,我認(rèn)為在業(yè)務(wù)代碼中除了要遵循方法入?yún)⒉灰^多以外,還需要遵循 隨著重要程度向后排序 的原則,以 Mybatis 中如下方法為反例:
public class DefaultResultSetHandler implements ResultSetHandler { // ... private final Map ancestorObjects = new HashMap?>(); private void putAncestor(Object resultObject, String resultMapId) { ancestorObjects.put(resultMapId, resultObject); } }
向緩存中添加元素的方法 putAncestor 將入?yún)?String resultMapId 放在第一位更合適。
關(guān)于代碼自解釋
每次提到命名或者在為接口命名時(shí),之前我都會(huì)有一種非常強(qiáng)烈的讓它自解釋的想法,但是隨著對軟件開發(fā)理解的變化,這種想法的欲望在逐漸降低,原因有二:
閱讀習(xí)慣:對國人來說,可能大多數(shù)人沒有先去讀英文的習(xí)慣,更傾向于讀中文相關(guān)的內(nèi)容,比如注釋
英語水平參差:可能有時(shí)候想要自解釋的初心是好的,但是如果使接口名變成了長難句,可讀性將降低
當(dāng)然,花時(shí)間來好好為變量和方法命名,是非常值得的,它能大大的提高可讀性,最好的情況是:當(dāng)讀者看到它時(shí),就已經(jīng)基本領(lǐng)會(huì)了它的作用。盡可能的讓它們明確、直觀且不太長。如果很難為變量或方法找到一個(gè)簡單的名稱,這可能暗示底層對象的設(shè)計(jì)不夠簡潔,《軟件設(shè)計(jì)哲學(xué)》提出了一種觀點(diǎn):考慮 拆分成多個(gè)分別定義 或者為其 添加上必要的注釋。此外,我覺得命名保持一致性也非常重要,比如在項(xiàng)目中對于補(bǔ)購已經(jīng)命名為 AddBuy,那么便不要再引入 SupplementaryPurchase 和 Replenishment 等命名,團(tuán)隊(duì)內(nèi)成員將知識統(tǒng)一才是最好的,并不在于它在英文語境下是否表達(dá)準(zhǔn)確。
但是,Mybatis 為什么能夠在很少注釋的情況下又保證了它的源碼自解釋呢?而且在《代碼整潔之道》中也持有對注釋的消極觀點(diǎn):
... 注釋最多只能算是一種不得已而為之的手段。若編程語言有足夠的表達(dá)力,或者我們長于用這些語言來表達(dá)意圖,就不那么需要注釋——也許根本不需要。 注釋的恰當(dāng)用法是彌補(bǔ)我們在代碼中未能表達(dá)清楚的內(nèi)容... 注釋總是代表著失敗,我們總有不用注釋便很難表達(dá)代碼意圖的時(shí)候,所以總要有注釋,這并不值得慶賀。
因?yàn)?Mybatis 中方法做的事情足夠簡單,像簡單的 query 和 doQuery 方法,或者再復(fù)雜一些的 handleRowValuesForNestedResultMap 也能知道它是在處理循環(huán)引用的結(jié)果映射集。而在業(yè)務(wù)代碼中就不太一樣了,僅靠幾個(gè)簡短的詞語并不能將方法的作用解釋清楚,想讓它自解釋就會(huì)導(dǎo)致方法名寫的很長,而且多數(shù)情況下,研發(fā)同事并不愿意花精力去翻譯那冗長又蹩腳的方法名,給人更多的感受是:“這寫的都是什么?”。如果想在業(yè)務(wù)代碼中保證“代碼自解釋”的話,還是需要認(rèn)真的去寫注釋。因?yàn)闃I(yè)務(wù)功能相對復(fù)雜,而方法名本身所能表現(xiàn)的東西又非常有限,通常并不能僅通過方法名來表達(dá)其含義,注釋能夠在此處為方法表達(dá)帶來增益。但因此認(rèn)為注釋是彌補(bǔ)方法名表達(dá)能力欠佳的補(bǔ)丁,就有些偏頗了,因?yàn)殡S著注釋寫的越來越多,你會(huì)發(fā)現(xiàn):注釋其實(shí)是代碼的一部分,它不光提供代碼之外的重要信息,還能隱藏復(fù)雜性,提高抽象程度,這還反映了開發(fā)者對代碼的設(shè)計(jì)和重視,隨著時(shí)間的推移,有新的開發(fā)者加入時(shí),也能讓他快速理解代碼,降低出現(xiàn) Bug 的概率。
不過,也有一些命名方法能夠幫我們提高方法的可讀性,比如 instantiateXxx 表示創(chuàng)建某對象,initialXxx 表示為某對象中字段賦值。
還有一點(diǎn)值得學(xué)習(xí),Mybatis 源碼中會(huì)在目錄下創(chuàng)建 package-info.java 來注釋包路徑,以 src/main/java/org/apache/ibatis/cache/decorators/package-info.java 為例,它注釋了該目錄都是緩存的裝飾器:
/** * Contains cache decorators. */ package org.apache.ibatis.cache.decorators;
這樣我們就能夠知道該路徑下的定義是與什么有關(guān)了。不過,這會(huì)使得該文件夾雜在各個(gè)類之中,如果能在命名前加上 a- 成為 a-package-info.java 被置于頂部的話,會(huì)更整潔一些:
“能用就行” 其實(shí)遠(yuǎn)遠(yuǎn)不夠
“代碼整潔與否不是一件主觀的事情,這需要始終站在閱讀者的角度考慮”是學(xué)習(xí)軟件設(shè)計(jì)帶給我最大的啟發(fā),“該如何設(shè)計(jì)能讓開發(fā)者更輕松得讀懂”也成了在寫代碼時(shí)常常考慮的問題。《軟件設(shè)計(jì)哲學(xué)》中提到過“永遠(yuǎn)不要反駁他人對代碼可讀性的評價(jià)”的觀點(diǎn)也正是在強(qiáng)調(diào)這些。
到現(xiàn)在回看本專欄,發(fā)現(xiàn)真正的講好設(shè)計(jì)原則和代碼的寫法并不是一件很容易的事情,因?yàn)槲也幌胫恢v理論,而想結(jié)合實(shí)踐又需要結(jié)合大部分 Mybatis 源碼,所以它們在內(nèi)容上,源碼介紹會(huì)占得更多一些,當(dāng)然這也是我覺得稍有遺憾的點(diǎn),如果這都能給大家?guī)硪恍﹩l(fā)的話,實(shí)在感激涕零。
雖然本專欄始終圍繞著如何將代碼寫得更整潔和優(yōu)雅做討論,但是我們還是需要學(xué)會(huì)“負(fù)重前行”:和凌亂的代碼相處。一些凌亂的代碼可能寫過一次后便不再變更,所以有時(shí)候沒有必要為了優(yōu)雅強(qiáng)迫癥而去重構(gòu)它們,它們可能始終會(huì)被隱藏在某個(gè)方法后面,默默地提供著穩(wěn)定的功能,如果你深受其擾,可以考慮在你讀過之后為這段代碼添加注釋,之后看這段代碼的開發(fā)者也能理解和感謝你的用心,否則因?yàn)閮?yōu)雅的重構(gòu)導(dǎo)致線上生產(chǎn)事故,可就得不償失了。
實(shí)際上,能寫好代碼對于程序員來說并不是一件特別厲害的事情,它只能算是一項(xiàng)基本要求,而且隨著 AI 的不斷發(fā)展,它在未來可能會(huì)幫我們生成很好的設(shè)計(jì)。當(dāng)然,這也不是放任的理由,寫爛代碼的行為還是需要被摒棄的。在最后我想借先前讀過的雷軍的博客《我十年的程序員生涯》的節(jié)選來結(jié)束本專欄:
有的人學(xué)習(xí)編程技術(shù),是把高級程序員做為追求的目標(biāo),甚至是終身的奮斗目標(biāo)。后來參與了真正的商品化軟件開發(fā)后,反而困惑了,茫然了。
一個(gè)人只要有韌性和靈性,有機(jī)會(huì)接觸并學(xué)習(xí)電腦的編程技術(shù),就會(huì)成為一個(gè)不錯(cuò)的程序員。剛開始寫程序,這時(shí)候?qū)W得多的人寫的好,到了后來,大家都上了一個(gè)層次,誰寫的好只取決于這個(gè)人是否細(xì)心、有韌性、有靈性。掌握多一點(diǎn)或少一點(diǎn),很快就能補(bǔ)上。成為一個(gè)高級程序員并不是件困難的事。
當(dāng)我上學(xué)的時(shí)候,高級程序員也曾是我的目標(biāo),我希望我的技術(shù)能得到別人的承認(rèn)。后來發(fā)現(xiàn)無論多么高級的程序員都沒用,關(guān)鍵是你是否能夠出想法出產(chǎn)品,你的勞動(dòng)是否能被社會(huì)承認(rèn),能為社會(huì)創(chuàng)造財(cái)富。成為高級程序員絕對不是追求的目標(biāo)。
希望大家不僅能寫出好代碼,還能做出屬于自己的產(chǎn)品,為生活乃至世界添一份彩。
審核編輯 黃宇
-
源碼
+關(guān)注
關(guān)注
8文章
667瀏覽量
30136 -
mybatis
+關(guān)注
關(guān)注
0文章
63瀏覽量
6867
發(fā)布評論請先 登錄
一文了解MyBatis的查詢原理
史上最專業(yè)的電容選型資料
求一款可以與51單片機(jī)通訊的LCD顯示屏,12864遠(yuǎn)遠(yuǎn)不夠大
傳感器是什么由什么組成
軟件設(shè)計(jì)師全書

中國彩電市場銷量遠(yuǎn)遠(yuǎn)不夠?我們的市場還遠(yuǎn)遠(yuǎn)沒有到消費(fèi)的天花板
人工智能技術(shù)感知層面 認(rèn)知智能發(fā)展遠(yuǎn)遠(yuǎn)不夠

基于單片機(jī)的避障小車及自動(dòng)循跡的設(shè)計(jì)(proteus仿真+源碼+原理圖+軟件設(shè)計(jì)流程+硬件清單+視頻講解)

【畢設(shè)狗】【單片機(jī)畢業(yè)設(shè)計(jì)】基于單片機(jī)的控制窗簾電路的設(shè)計(jì)(proteus仿真+源碼+原理圖+軟件設(shè)計(jì)流程+硬件

【畢設(shè)狗】【單片機(jī)畢業(yè)設(shè)計(jì)】基于單片機(jī)的溫控水杯的設(shè)計(jì)(實(shí)物+proteus仿真+源碼+原理圖+PCB+軟件設(shè)計(jì)流程

【畢設(shè)狗】【單片機(jī)畢業(yè)設(shè)計(jì)】基于單片機(jī)的教室人數(shù)實(shí)時(shí)檢測系統(tǒng)的設(shè)計(jì)(proteus仿真+源碼+原理圖+軟件設(shè)計(jì)

【畢設(shè)狗】【單片機(jī)畢業(yè)設(shè)計(jì)】基于單片機(jī)的智能密碼鎖的設(shè)計(jì)(實(shí)物+proteus仿真+源碼+原理圖+軟件設(shè)計(jì)流程

評論