2020年 你打算怎樣提升寫代碼的能力?

點擊上方“小羅技術筆記”,關注公衆號

第一時間送達實用乾貨


在 9102 年年初,一位室友問我一個問題,如何才能夠提升寫代碼的能力?

可惜的是: 當時僅僅回覆了一些自己的想法,如多看開源代碼,多讀書,多學習,多關注業界的動向與實踐,同時也列了一些原則。但是這些並沒有所總結,又或者說沒有例子的語言始終是空泛的。所以在今年年底之際,對應着今年中遇到的形形色色的代碼問題來一一講解一下。

好代碼的用處

實際上本書建立在一個相當不可靠的前提之上:好的代碼是有意義的。我見過太多醜陋的代碼給他們的主人賺着大把鈔票,所以在我看來,軟件要取得商業成功或者廣泛使用,“好的代碼質量”既不必要也不充分。即使如此,我仍然相信,儘管代碼質量不能保證美好的未來,他仍然有其意義:有了質量良好的代碼以後,業務需求能夠被充滿信心的開發和交付,軟件用戶能夠及時調整方向以便應對機遇和競爭,開發團隊能夠再挑戰和挫折面前保持高昂的鬥志。總而言之,比起質量低劣,錯誤重重的代碼,好的代碼更有可能幫助用戶取得業務上的成功。

以上文字摘抄於《實現模式》的前言,距離本書翻譯已經時隔 10 年了,但是這本書仍舊有着很大的價值。同時對於上述言論,我並不持否認意見。但是我認爲,壞代碼比好代碼更加的費財(嗯,沒打錯,我確定)。對於相同的業務需求,壞代碼需要投入的精力,時間更多,產出反而會更少。同時根據破窗理論( 此理論認爲環境中的不良現象如果被放任存在,會誘使人們仿效,甚至變本加厲 ),壞代碼會產生更壞的代碼。這是一個惡性循環,如果不加以控制,完成需求的時間會慢慢失去控制。需要完成需求的人也會失落離開。

也就是說,好代碼可以實現多贏,能夠讓用戶爽,能夠讓老闆爽,能夠讓開發者爽。總之,大家爽纔是真的爽。

怎麼寫出好代碼

少即使多

利用開源出來的設計與代碼來減輕來自於業務線的時間壓力。

   
   
   
  1. The best way to write secure and reliable applications. Write nothing; deploy nowhere.

以上取自 github 上最火的項目之一 nocode懶惰是程序員的美德之一。所以學習業務,理解業務,拒絕不必要的需求也是一個程序員的必修功課。詳情可以參考如何杜絕一句話需求? 這一篇 blog,當然,在大部分場景下,我們是不具備對需求說不的能力與權力的,但是無論如何,深度的理解業務,對客戶有同理心是對程序員的更高要求。解決問題纔是一個程序員需要做的事情。能夠理解好題意才能解決問題。

對於軟件開發而言,時間一定是最寶貴,最有價值的資源。相應的,儘量把時間耗費在解決新的問題,而不是對已經存在確切解決方案的問題老調重彈。所以,儘量不要自己寫代碼,而是借用別人的設計與實現。而在事實上,你也很難在極短的時間壓力下設計並完成比開源更加合適的代碼。

當然,開源作者一定是想讓他的產品有更多的受衆,所以從設計上而言,會採用較爲通用的設計,如果你的需求較爲特殊並且你覺得不能說服作者幫你“免費打工”(或者作者拒絕了),那麼你也只需要在特定之處進行包裝與改寫,但是要比完全重寫要簡單太多了。

當然,調研新的技術方案並且使用到項目中是一種能力,但是千萬不要因爲一個小功能添加一個非常大的項目。

筆者在之前就遇到過其他小夥伴因爲無法使用數字四捨五入。說 fixed 方法有問題而使用 math.js 的小夥伴。

   
   
   
  1. (11.545).toFixed(2)

  2. // "11.54"

如果想要了解 fixed 方法爲何有問題的,可以參考 爲什麼(2.55).toFixed(1)等於2.5? 作者以 v8 源碼來解釋爲何會有這樣的問題,以及提供了部分修正 fixed 的方案。

事實上如果沒有很大的精度需求,前端完完全全利用一個函數便可以解決的問題,完全不需要複雜的math 這種高精度庫。

   
   
   
  1. function round(number, precision) {

  2.    returnMath.round(+number + 'e'+ precision) / Math.pow(10, precision);

  3. }

當然,也有小夥伴來找我詢問大量數據的表格優化,我第一反應就是 React Infinite 或者 vue-infinite-scroll 此類解決方案。但是對方能夠多提供一些信息包括上下文,採用的技術棧,當前數據量大小,未來可能需要達到的大小,當前表格是否需要修改等。得到了這些信息,結合業務來看,相比於增加一個庫,是否如下方式更爲便捷與快速。

   
   
   
  1. // 因爲 vue 模型的原因,使用 Object.freeze 性能可以有很大增益

  2. this.xxx = Object.freeze(xxx);

  3. 隨着堆積業務,代碼的增長。管理複雜度的成本與日俱增,把依賴降低。

  4. 利用開源代碼使得任務更容易實現。時間就是成本。關鍵是讓收益可以最大化。

學習更多是爲了做的更少。

統一

不同的人由於編碼經驗和編碼偏好不同,項目中同一個功能的實現代碼可能千差萬別。但是如果不加以約束,讓每一個人都按照自己的偏好寫自己的模塊,恐怕就會變成災難。

所以每次在學習一些新技術的時候,我總是想多看看作者的實例代碼,作者是如何理解的,社區又是如何理解的。以求實現起來代碼風格不至於偏離社區太多,這樣的話可以提高溝通與協作的效率。類似於 《阿里巴巴Java開發手冊》 或者 vue 風格指南 這種取自大公司或社區的經驗之談,要多讀幾遍。因爲他們所遇到的問題和業務更加複雜。

對於公司內部開發來說,寫一個組件時候,生命週期的代碼放在文件上面還是放在最下面,如何把代碼的一個功能點集中放置。通用型代碼的修改。代碼行數的限制。能夠列出統一的方案,多利而少害。

化繁爲簡(抽象)

抽象是指從具體事物抽出、概括出它們共同的方面、本質屬性與關係等,而將個別的、非本質的方面、屬性與關係捨棄的思維過程。

如果你面對一個較大的系統,你會發現重構並不能解決根本問題,它僅僅只能減少少許的代碼的複雜度以及代碼行數,只有抽象纔可以解決實質性問題。

無論是數據庫設計,架構設計,業務設計,代碼設計,但凡設計都離不開抽象。抽象能力強的所面臨的困難會比能力弱的少很多。

或者說抽象能力弱一些的小夥伴遇到一些問題甚至需要重新推翻然後再設計,這個是在時間和業務開發中是不能被接受的。

這裏就談談代碼,以下也舉個例子,如 axios 庫中有攔截器與本身業務,在沒有看到源碼之前,我一直認爲他是分 3 階段處理:

1、請求攔截

2、業務處理

3、響應攔截

但如果你去看源碼,你就會發現其實在作者看來,這 3 個階段其實都是在處理一個 Promise 隊列而已。

   
   
   
  1. // 業務處理

  2. var chain = [dispatchRequest, undefined];

  3. var promise = Promise.resolve(config);

  4. this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {

  5.    // 前置請求攔截

  6.    chain.unshift(interceptor.fulfilled, interceptor.rejected);

  7.  });

  8. this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {

  9.    // 後置響應攔截

  10.    chain.push(interceptor.fulfilled, interceptor.rejected);

  11.  });

  12.  while(chain.length) {

  13.    promise = promise.then(chain.shift(), chain.shift());

  14.  }

  15.  return promise;

這就是一種代碼抽象能力。讓自己的代碼可以適應更多的場景是程序員需要思考的。代碼不是給機器看的,是給人看的,更高的要求是: 代碼不僅僅是給人看的,更是給人用的。需要考慮到協作的人與事,靈活的配置也是必須要考慮到的。就拿前端的 虛擬 dom 來說。能夠適配更多的平臺。

當然了,抽象能力需要時間,需要經驗,需要學習大量的設計。

注意!:不要過早的抽象業務代碼,甚至不要抽象業務代碼。多寫一點代碼無所謂,千萬別給自己找事做。 在業務上儘量保持簡單和愚蠢。除非你是業務專家,確認當前業務不太會產生變化。

權責對等(拆分與合併)

責任與義務本質上就是對等的,且越對等的就越穩定。這幾年,微服務架構,中臺,微前端理論層出不窮,本質上就是爲了權責對等,對於更加基礎的服務,更有產出的業務投入更高的人力與物力以保證更穩定的運行是很正常的一件事。而不是之前的大鍋飯(單體應用)。

從代碼上來看,某個模塊是否承擔了它不應該做的事情,或者某個模塊過於簡單,徒增複雜度。

當然,事實上有些東西目前是做不到的讓所有人都覺得滿意,增一分則肥,減一分則瘦,剛剛好很難界定。就像 Dan Abramov 說的那樣:

   
   
   
  1. Flux libraries are like glasses: you’ll know when you need them.

只做一件事

Unix 哲學,這個很好理解,就像我今年想做的事情太多,反而什麼都沒有做(或者說都做了,但都不好)。

代碼上來看,不要因爲一點點性能的原因,把幾件事合在一起去做。例如在一次 for 循環中解決所有問題,或者將所有代碼寫在一個函數中,例如:

   
   
   
  1. created() {

  2.  const{a,b,c,d} = this.data

  3.  // ... 三件事情彼此有交互同時需要 a,b,c,d

  4.  // 完成之後的邏輯

  5. }

改造後:

   
   
   
  1. created() {

  2.  const axx = doA()

  3.  doB()

  4.  const cxx =  doC()

  5.  // 完成之後的邏輯

  6. }

  7. // 分離出3個函數

  8. doA() {

  9.   const{a,b,c} = this.data

  10.  // ... 三件事情彼此有交互同時需要 a,b,c,d

  11.  // 完成之後的邏輯

  12. }

  13. // 其他代碼

相比於第一個只需要一次取數,一次setData,第二個性能無疑更低,但是可維護性變高了,3 件事情都被拆分出來,後面修改代碼時候,我可以追加一個 doD 而不是再次把第一份代碼中邏輯整理清楚再小心翼翼的修改代碼。

命名與註釋

   
   
   
  1. There are style="box-sizing: border-box; color: rgb(204, 153, 204); font-size: 13px !important; line-height: 20px; white-space: inherit !important; font-family: consolas, menlo, courier, monospace, "Microsoft Yahei" !important;">in ComputerScience: cache invalidation and naming things.

命名與緩存失效是兩大難題,今年講了不少緩存問題,同時,命名的確是很困難的一件事情。通過一句話來解釋你們在做什麼事情,通過一句話來解釋一件事的意圖。

不說在程序世界中,在現實世界中也是如此。例如: 《震驚!xxx居然xxx》等新聞,雖然說看完後都會想要罵一句,但是,正如這樣的名字才能吸引人家點擊進入,讓人情不自禁的被騙一次又一次。所以在項目沒有發佈前,要取一個簡單而又好記的名字。

但在程序內部,我們不需要“騙取”人家的點擊量,反而是要務實點,不要欺騙另外的同伴,比如說寫了一個簡單的名字,結果內部卻封裝了很多的業務代碼。同時我認爲這也是函數越寫越短的理由,因爲大家難以通過命名來解釋那一大坨代碼的意圖。所以,需要編寫可以自我解釋的代碼,而這種代碼最佳實踐就是好的命名。

對於開源代碼,你往往會發現,這些文件開頭都會有一系列註釋,這個註釋告訴我們了這個模塊的意圖與目的。讓你無需看代碼就可以進行開發。

對於業務開發而言,僅在你不能通過代碼清晰解釋其含義的地方,才寫註釋。在多個條件下都無法解釋你的代碼。

1、項目名

2、模塊名

3、文件名(類名)

4、函數名(方法名)

這並不是讓你不寫註釋。但是我覺得更多的註釋應該放在數據結構而不是代碼邏輯上。聰明的數據結構和笨拙的代碼要比相反的搭配工作的更好。更多的時候,看數據結構我能瞭解業務是如何運行的,但是僅僅看到代碼並不能實際想象出來。

實際上,隨着時間的推移,代碼做出了許多改動,但註釋並沒有隨之修改,這是一個很大的問題。註釋反而變得更有欺騙性。

這裏也提供一篇 export default 有害 的文章。我覺得 export default 導出一個可以隨意命名的模塊就是一種欺騙性代碼(隨着時間的推移,該模塊的意圖會發生變化)。

考慮場景

沒有放眼四海皆准的方案,所以我們必須要考慮到場景的問題,我們總是說可修改性,可讀性是第一位的(往往可讀,可修改的代碼性能都不差)。但是如果是急切需求性能的場景下,有些事情是需要再考慮的。

if 是業務處理中最常用的,在每次使用前要考慮以下,哪個更適合作爲主體,哪個更適合放在前面進行判斷。如果有兩個維度上的參數,一個是角色,一個是事件。一定是會先判斷角色參數,然後再去判斷事件參數,反之則一定不好。因爲前者更符合人的思維模式。在同一維度下,至於哪個放前面,一定是更多被使用的參數放在前面更好,因爲更符合機器的執行過程。

就像在 if 中你究竟是使用 else 還是 return。大部分情況下處理業務邏輯互斥使用 else,處理錯誤使用return。因爲這樣的代碼最符合人的思維邏輯。

但是在這裏我也要舉出來自《代碼之美》的例子,在第五章中,作者 Elliotte Rusty Harold 設計了一個 xml 驗證器,其中有一段在驗證數字字符:

   
   
   
  1. publicstaticboolean isXMLDigit(char c) {

  2.  if(c >= 0x0030&& c <= 0x0039) returntrue;

  3.  if(c >= 0x0660&& c <= 0x0669) returntrue;

  4.  if(c >= 0x06F0&& c <= 0x06F9) returntrue;

  5.  // ...

  6.  returnfalse

  7. }

這個優化之後如下:

   
   
   
  1. publicstaticboolean isXMLDigit(char c) {

  2.  if(c < 0x0030) returnfalse; if(c <= 0x0039) returntrue;

  3.  if(c < 0x0660) returnfalse; if(c <= 0x0669) returntrue;

  4.  if(c < 0x06F0) returnfalse; if(c <= 0x06F0) returntrue;

  5.  // ...

  6.  returnfalse

  7. }

全局思考,善於交流

軟件開發已經不是一個人打天下的時代了,你要不停的觸達邊界。在前後端分離的時代,前端可以不知道數據庫如何優化,後端也可以不清楚瀏覽器的渲染機制,但是卻不能不明白對方在做什麼。否則等於雞同鴨講,也會浪費時間。在開發時候,把一段邏輯放在那一端取決安全的思考以及簡化邏輯。

善於交流是一種能力,在與別人交流時給與足夠的上下文,讓你的 leader 溝通,讓她知道你的難處。和小夥伴溝通,說服他人按照你的想法推進,同時,善於聆聽才能不斷進步。

算法

我不是一個算法達人( leetcode 中等題目都費勁 ),但這個沒什麼可說的,你拿你的 O(n**3) 算法去對戰人家 O(n * logn) 算法就是費財。所以,知道自己某方面不夠好去努力就行了。

輔助工具 TypeScript

雖然早就接觸和實踐過,但是以往都是 AnyScript。今年也算重度使用了。才體會到該工具的利好。一個好的開發工具並不是讓你少寫那一點點代碼,而是讓你在交付代碼時候能夠更加自信。

TypeScript 最大的好處就是讓你在寫代碼前先思考,先做設計。就像之前說的。聰明的數據結構和笨拙的代碼要比相反的搭配工作的更好。

TypeScript 同時也可以讓大部分運行時錯誤變爲編譯時,並且可以減少使用中的防禦性編程(信任但是仍要驗證)。你不是一個人在寫代碼,協作優先。

在開發中,如果你接觸過複雜性數據結構,並且還要在模塊中不斷進行數據轉化,你就會不斷的遇到:我的數據呢?到底在那一步丟失了?並且即使是代碼對的,你仍舊害怕,仍舊懷疑。我已經過了那個“寫 bug 是因爲想的不夠多,不夠徹底”的年齡。

函數式思維

js 是有函數式的血統的,當年一直聽說,函數是一等公民,只是當時完全不能理解。

純函數,數據不可變以及代碼即數據這三點是我認爲是函數式思維對代碼能力提升最大的三點。

這個我不想展開去聊,因爲我沒有熟練掌握過任何一門純函數式語言。但是我的代碼一定有函數式的影子,並且它的確讓我的代碼更優美。

其他

單元測試,代碼審查,安全等等都沒有講到,這個我也需要足夠的學習纔能有所輸出。不過這裏列出一些資料供大家學習與瞭解:

谷歌代碼審查指南

SaaS型初創企業安全101

有理有據就是好代碼

工作在別人遺留的糟糕代碼上是常有的事情,同時面對開發需求實際表,爲了兼容,我們也不得不寫出一些不那麼好的代碼。但是面對他人的疑問,我們需要給與別人這樣做的理由,也就是你的每一行代碼寫下去一定有充分的理由和依據。

結語

明顯不等於簡單,上述都是很明顯的事情,但是要做好都需要很長時間的學習與經驗。

所以如何才能寫好代碼呢?那就是多看開源代碼,多讀書,多學習,多關注業界的動向與實踐。不斷學習,不斷進化的代碼纔是好代碼。


來源: https://url.cn/5u8GsNR



長按二維碼關注我們

點個在看再走唄!

本文分享自微信公衆號 - 小羅技術筆記(javaCodeNote)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章