作者:京東保險 王奕龍
到本節(jié) Mybatis 源碼中核心邏輯基本已經(jīng)介紹完了,在這里我想借助 Mybatis 其他部分源碼來介紹一些我認為在編程中能 最快提高編碼質(zhì)量的小方法,它們可能比較細碎,希望能對大家有所啟發(fā)。
關(guān)于方法的長度和方法拆分
之前我在讀完《代碼整潔之道》時,非常癡迷于寫小方法這件事,它強調(diào)“每個方法只做一件事,方法的長度不能超過 5 行”等觀點。
記得某次代碼評審時,有同事對將一個大方法拆分成多個小方法提出了異議:拆分出的小方法不能算作做了一件事,它們都只是大方法中的一個“動作”而已,所以不應(yīng)該拆分巴拉巴拉。
這個觀點讓我說不出什么,后來我也在想:如果按照這個觀點,多大的方法都可以概括成只做了一件事,那么我們就需要將所有的邏輯都“攤”到一個方法中嗎?我覺得拆分方法目的不是在界定一件事還是一個動作上,而是 關(guān)注方法的可讀性,拆分方法太多確實讓代碼變得不好讀,需要輾轉(zhuǎn)在多個方法之間,但是不拆的可讀性也會差,所以接下來我想根據(jù) Mybatis 這段代碼來簡單談?wù)勎覍懛椒ǖ挠^點:
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 解析配置文件中各個標(biāo)簽的方法,它將每個標(biāo)簽的解析都單獨定義出了一個方法,這也是我一直遵循的寫方法的觀點:最頂層的入口方法應(yīng)該是短小清晰的步驟,在主方法中編排好方法的執(zhí)行內(nèi)容,這樣主方法便是清晰明了的執(zhí)行流程,我們便能一眼清晰的知道該方法做了什么事情,而針對各個具體的環(huán)節(jié)或者要改動哪些邏輯,直接跳轉(zhuǎn)到對應(yīng)的方法即可。
至于該不該將某段邏輯抽象成一個方法,我的觀點是 能不能一眼看明白這段邏輯在干什么,如果不能,那么就應(yīng)該被抽象到一個方法中,否則將其保留在原方法中也是沒有問題的,對方法的抽象從來都不在于方法的長度,可讀性 應(yīng)得到更多的關(guān)注。
此外,還有一個能提高代碼可讀性的方法是: “合理使用換行符” ,如下代碼所示:
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)造方法中,進行注冊別名操作時使用了換行符進行分割,它將 TransactionFactory 相關(guān)的緊挨在一起作為一組,再將 DataSourceFactory 相關(guān)的緊挨在一起等等,這樣在分門別類查看這段代碼便是清晰的,即使它們都在一個方法中。
方法的編排
在《代碼整潔之道》中提出了代碼中 方法要從上到下排列,讀方法就像讀報紙一樣,因為方法被抽象提煉出來,閱讀時必然會造成在多個方法間切換的問題,那么如果我們將方法從上到下依次排列,能夠在屏幕中同時看到所有相關(guān)方法的話,那么這樣的確方便了閱讀,比如 methodA 依賴 commonMethod 方法的排列:
@Override public void methodA() { commonMethod(); } private void commonMethod() { // ... }
此時如果增加 methodB() 也要復(fù)用 commonMethod() 的話,那么我并不會像下面這樣排列方法:
@Override public void methodA() { commonMethod(); } private void commonMethod() { // ... } @Override public void methodB() { commonMethod(); }
因為我們在看一個方法時,始終要堅持 自上往下讀 的原則,不能在看 methodB() 的時候,再跳回到上面去,而是需要像這樣:
@Override public void methodA() { commonMethod(); } @Override public void methodB() { commonMethod(); } private void commonMethod() { // ... }
那么這也就意味著:如果 某個方法被復(fù)用的次數(shù)過多,它的位置則越靠近類的下方。在《軟件設(shè)計哲學(xué)》中也提到過 專用方法上移,通用方法下移 的觀點,這也是在提醒開發(fā)者,當(dāng)看見某個私有方法在類的尾部時,它可能是一個非常通用的方法,對它的修改就需要特別謹慎。
方法的聲明
在業(yè)務(wù)代碼中經(jīng)常會看到接口中某方法聲明拋出異常:
public interface Demo { void method(Object parameter) throws Exception; }
但是對要拋出的異常類型并沒有明確的聲明,只知道會拋出 Exception,對于具體的原因一無所知。如果想清楚的了解,可以借助注釋(如果有的話),否則就需要去探究它的具體實現(xiàn),這對想直接調(diào)用該方法的研發(fā)人員來說非常不友好,增加了 “認知負荷” ,那該怎么辦呢?
《圖解Java多線程設(shè)計模式》中提到過一個例子非常有啟發(fā)性,它說方法簽名中標(biāo)記 throws InterruptedException 能表示兩種含義:第一種比較容易被想到,表示該方法可以被打斷/取消;第二種含義是,這個方法耗時可能比較長。
比如 Thread.join() 方法,它聲明了 throws InterruptedException,它的作用是讓當(dāng)前執(zhí)行的線程暫停運行,直到調(diào)用 join() 方法的線程執(zhí)行完畢。當(dāng)我們在一個線程實例上調(diào)用 join() 方法時,當(dāng)前執(zhí)行的線程將被阻塞,阻塞時間可能會很長,如果在阻塞期間如果另一個線程中斷(interrupt)了它,那么它將拋出一個 InterruptedException。所以,我們能夠在 throws 聲明中,獲取某方法關(guān)于某異常的信息。
在 Mybatis 源碼中也有類似的例子,如下:
public interface Executor { int update(MappedStatement ms, Object parameter) throws SQLException; }
它聲明出 throws SQLException 表示 SQL 執(zhí)行的異常,它被拋出了我們便能知道是 SQL 寫的有問題。我認為直接將方法上聲明 throws Exception 的簽名并不添加任何注釋是一種懶惰。異常精細化能給我們帶來很多好處,比如日常報警容易看,增加方法可讀性,能夠通過聲明知道這個方法會拋出關(guān)于什么類型的異常,便能讓接口的調(diào)用者判斷是處理異常還是拋出異常。
方法的參數(shù)聲明也很重要,我認為在業(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)于代碼自解釋
每次提到命名或者在為接口命名時,之前我都會有一種非常強烈的讓它自解釋的想法,但是隨著對軟件開發(fā)理解的變化,這種想法的欲望在逐漸降低,原因有二:
閱讀習(xí)慣:對國人來說,可能大多數(shù)人沒有先去讀英文的習(xí)慣,更傾向于讀中文相關(guān)的內(nèi)容,比如注釋
英語水平參差:可能有時候想要自解釋的初心是好的,但是如果使接口名變成了長難句,可讀性將降低
當(dāng)然,花時間來好好為變量和方法命名,是非常值得的,它能大大的提高可讀性,最好的情況是:當(dāng)讀者看到它時,就已經(jīng)基本領(lǐng)會了它的作用。盡可能的讓它們明確、直觀且不太長。如果很難為變量或方法找到一個簡單的名稱,這可能暗示底層對象的設(shè)計不夠簡潔,《軟件設(shè)計哲學(xué)》提出了一種觀點:考慮 拆分成多個分別定義 或者為其 添加上必要的注釋。此外,我覺得命名保持一致性也非常重要,比如在項目中對于補購已經(jīng)命名為 AddBuy,那么便不要再引入 SupplementaryPurchase 和 Replenishment 等命名,團隊內(nèi)成員將知識統(tǒng)一才是最好的,并不在于它在英文語境下是否表達準(zhǔn)確。
但是,Mybatis 為什么能夠在很少注釋的情況下又保證了它的源碼自解釋呢?而且在《代碼整潔之道》中也持有對注釋的消極觀點:
... 注釋最多只能算是一種不得已而為之的手段。若編程語言有足夠的表達力,或者我們長于用這些語言來表達意圖,就不那么需要注釋——也許根本不需要。 注釋的恰當(dāng)用法是彌補我們在代碼中未能表達清楚的內(nèi)容... 注釋總是代表著失敗,我們總有不用注釋便很難表達代碼意圖的時候,所以總要有注釋,這并不值得慶賀。
因為 Mybatis 中方法做的事情足夠簡單,像簡單的 query 和 doQuery 方法,或者再復(fù)雜一些的 handleRowValuesForNestedResultMap 也能知道它是在處理循環(huán)引用的結(jié)果映射集。而在業(yè)務(wù)代碼中就不太一樣了,僅靠幾個簡短的詞語并不能將方法的作用解釋清楚,想讓它自解釋就會導(dǎo)致方法名寫的很長,而且多數(shù)情況下,研發(fā)同事并不愿意花精力去翻譯那冗長又蹩腳的方法名,給人更多的感受是:“這寫的都是什么?”。如果想在業(yè)務(wù)代碼中保證“代碼自解釋”的話,還是需要認真的去寫注釋。因為業(yè)務(wù)功能相對復(fù)雜,而方法名本身所能表現(xiàn)的東西又非常有限,通常并不能僅通過方法名來表達其含義,注釋能夠在此處為方法表達帶來增益。但因此認為注釋是彌補方法名表達能力欠佳的補丁,就有些偏頗了,因為隨著注釋寫的越來越多,你會發(fā)現(xiàn):注釋其實是代碼的一部分,它不光提供代碼之外的重要信息,還能隱藏復(fù)雜性,提高抽象程度,這還反映了開發(fā)者對代碼的設(shè)計和重視,隨著時間的推移,有新的開發(fā)者加入時,也能讓他快速理解代碼,降低出現(xiàn) Bug 的概率。
不過,也有一些命名方法能夠幫我們提高方法的可讀性,比如 instantiateXxx 表示創(chuàng)建某對象,initialXxx 表示為某對象中字段賦值。
還有一點值得學(xué)習(xí),Mybatis 源碼中會在目錄下創(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)了。不過,這會使得該文件夾雜在各個類之中,如果能在命名前加上 a- 成為 a-package-info.java 被置于頂部的話,會更整潔一些:
“能用就行” 其實遠遠不夠
“代碼整潔與否不是一件主觀的事情,這需要始終站在閱讀者的角度考慮”是學(xué)習(xí)軟件設(shè)計帶給我最大的啟發(fā),“該如何設(shè)計能讓開發(fā)者更輕松得讀懂”也成了在寫代碼時常??紤]的問題?!盾浖O(shè)計哲學(xué)》中提到過“永遠不要反駁他人對代碼可讀性的評價”的觀點也正是在強調(diào)這些。
到現(xiàn)在回看本專欄,發(fā)現(xiàn)真正的講好設(shè)計原則和代碼的寫法并不是一件很容易的事情,因為我不想只講理論,而想結(jié)合實踐又需要結(jié)合大部分 Mybatis 源碼,所以它們在內(nèi)容上,源碼介紹會占得更多一些,當(dāng)然這也是我覺得稍有遺憾的點,如果這都能給大家?guī)硪恍﹩l(fā)的話,實在感激涕零。
雖然本專欄始終圍繞著如何將代碼寫得更整潔和優(yōu)雅做討論,但是我們還是需要學(xué)會“負重前行”:和凌亂的代碼相處。一些凌亂的代碼可能寫過一次后便不再變更,所以有時候沒有必要為了優(yōu)雅強迫癥而去重構(gòu)它們,它們可能始終會被隱藏在某個方法后面,默默地提供著穩(wěn)定的功能,如果你深受其擾,可以考慮在你讀過之后為這段代碼添加注釋,之后看這段代碼的開發(fā)者也能理解和感謝你的用心,否則因為優(yōu)雅的重構(gòu)導(dǎo)致線上生產(chǎn)事故,可就得不償失了。
實際上,能寫好代碼對于程序員來說并不是一件特別厲害的事情,它只能算是一項基本要求,而且隨著 AI 的不斷發(fā)展,它在未來可能會幫我們生成很好的設(shè)計。當(dāng)然,這也不是放任的理由,寫爛代碼的行為還是需要被摒棄的。在最后我想借先前讀過的雷軍的博客《我十年的程序員生涯》的節(jié)選來結(jié)束本專欄:
有的人學(xué)習(xí)編程技術(shù),是把高級程序員做為追求的目標(biāo),甚至是終身的奮斗目標(biāo)。后來參與了真正的商品化軟件開發(fā)后,反而困惑了,茫然了。
一個人只要有韌性和靈性,有機會接觸并學(xué)習(xí)電腦的編程技術(shù),就會成為一個不錯的程序員。剛開始寫程序,這時候?qū)W得多的人寫的好,到了后來,大家都上了一個層次,誰寫的好只取決于這個人是否細心、有韌性、有靈性。掌握多一點或少一點,很快就能補上。成為一個高級程序員并不是件困難的事。
當(dāng)我上學(xué)的時候,高級程序員也曾是我的目標(biāo),我希望我的技術(shù)能得到別人的承認。后來發(fā)現(xiàn)無論多么高級的程序員都沒用,關(guān)鍵是你是否能夠出想法出產(chǎn)品,你的勞動是否能被社會承認,能為社會創(chuàng)造財富。成為高級程序員絕對不是追求的目標(biāo)。
希望大家不僅能寫出好代碼,還能做出屬于自己的產(chǎn)品,為生活乃至世界添一份彩。
審核編輯 黃宇
-
源碼
+關(guān)注
關(guān)注
8文章
648瀏覽量
29306 -
mybatis
+關(guān)注
關(guān)注
0文章
61瀏覽量
6724
發(fā)布評論請先 登錄
相關(guān)推薦
評論