1. 程式人生 > 實用技巧 >常犯錯誤

常犯錯誤

前言

很多問題是能力還是態度,還是經驗,我認為每個人的成長都要犯很多錯誤,但是是不以為然,還是想盡辦法找到解決方案進而改進,在資訊爆炸的如今,對於很多問題的原因,我個人的觀點是熱情和態度。

命名囉嗦,不規範

禁止這類寫法,因為在一個func裡跟到最後我們可能完全不知道a是個什麼東西,又得跳回看。

var a = 1

型別名要表達的意思要和實際大致一樣,減少維護心智負擔。

userId
userInfo
orderHistory

函式命名更要講究英文和中文的準確對應,比如下面這兩種,想要表達的意思是處理資料,但是handle和deal似乎在英文中和資料不是很搭配,而且一般來講,函式對資料無非是做一箇中間處理,不會產生什麼終態效應。所以可以改為formatData()

func handleData() {}
func dealData() {}

魔法數字

萬惡之源,現代專案應該從一開始就著手配置相關開發, config driven development

updateState(1)
updateState(stateConfig.UP)

配置分類

配置做到了,但是還有一個比較頭疼的問題,配置爆炸,把所有的配置資訊都放在一個conf檔案,或者常量類...解決方法就是分業務邏輯,比如訂單類的就放到訂單下,使用者類就...

go專案的話其實不推薦使用傳統的MVC架構,所以根據業務邏輯層來進行配置切分更加方便。

分類的粒度

如果配置切分過於細化,反而會影響後期維護,這點主要是難以檢索等問題,個人感覺倒不是細化本身是錯的。

解決方案:

  • 從一開始掌握好粒度要求,比如就細分到某個業務邏輯層,不在某個業務邏輯層下再細分使用者,訂單...這個看專案和團隊的設計。
  • 相關的配置寫好comment和doc,這樣檢索很方便,一個配置檔名在清晰也可能會導致別人語義的理解錯誤還有不同模組可能細化程度不一會導致重複。(但是comment這東西在程式碼中就儘量要注意不要造成註釋災難,個人看老外的專案註釋比較多,但是看一些軟體設計相關的書籍,其實並不推薦過多的使用comment,因為本能的comment使用會導致自己並不會對程式碼有較高的要求,而是寄希望於comment來理解。)
  • 模組切分,同時配置互不影響。

函式寫太長

最近看的《軟體設計哲學》中倒是對這個觀點並不完全贊同,其主要原因是很多人把小函式當成了一個死記硬背的東西,不管什麼只要超過了n行就拆出一個小函式,這其實無形之中反而增加了維護成本,同時也打破了一個時間順序。

所以拆分要注意不要將狀態分離出去,時序不要改變。

func ABCDE() {
  A
  B
  C
  D
  E
}

=>

func main() {
  A()
  B()
  C()
  D()
  E()
}

再具體一些

func createOrder(userInfo UserInfo, products []Product) {
  checkUserValid(userInfo);
  checkProducts(products);
  checkUserAndProducts(userInfo, products);
  var order = insertOrder(userInfo, products);
  var createFlag = createOrderDetail(order, products);
  return createFlag;
}

濫用回撥,增加複雜性

回撥在業務場景很常見,比如增加一條資料回撥一個log,回撥一個狀態更新...其實完全可以同步來代替,回撥反而在直觀時序上不那麼明確。

實際例子

這裡有一個使用者的評論系統,評論系統會對服務你的商家進行一些tag勾選和內容填空。

對於評論系統本身,只需要簡單記錄被打tag和被評論的物件到mysql即可。

但後面有信用系統、使用者畫像系統、客服系統依賴於這些評論的資料,所以需要把這些評論tag和內容同步給其它的幾個系統,或者甚至是跨部門的系統。

一個提交,回撥函式n個,關鍵是回撥函式根本不是直接的邏輯,自己根本不知道寫了這段邏輯產生的真正影響,所以出了問題可能又得查到別人那裡,如果對方離職又是一個難題。

解決方案:時序解耦,訊息佇列。

event A happend
then {
    call sys A1();
    call sys A2();
    call sys A3();
    call sys A4();
    call sys A5();
    call sys A6();
    call sys A7();
    call sys A8();
    ...
}

=>

event A happend
then {
    push msg to msg queue
}

A1~AN subscribe topic A in msg queue

這裡就是將問題轉移出去,即使出了問題,你的函式的最終作用是傳送訊息給訊息佇列,這樣你只需要確認你的問題就ok了,不像之前那樣n個回撥都耦合在一起。再進一步將push打上log,這樣監控也更方便了,當然分散式的一致性會不如之前,而且不僅要做到預警還要做好恢復方案,出了問題可快速恢復。

分層但是分層沒有明確的界線

比如在某一層裡,有些只是在記憶體操作,比如返回一個result array,但是有些直接返回一個status state給使用者,直接持久化...

公共介面本身只能用一次

換個說法就是可重入不可重入。

比如你的func內引用了一個全域性靜態變數,而且沒有加鎖,那這就屬於不可重入,因為幾個執行緒一起呼叫不久亂了。

所以不要吝惜你的鎖!儘量拒絕使用全域性變數!

效能問題?你的業務真的需要考慮嗎?又要搬出“過早優化是萬惡之源”

設計模式濫用

設計模式並非銀彈,如果做Java開發,其實現代framework做的已經蠻好了。

查詢資料庫不做批量

這個根據id查沒問題,但是如果商品是n+個,那就意味著執行一次這個func要進行n+次資料庫連線,即使有連線池,這似乎也不合理,這種要做到批量查詢。

func getProductList(xxxx) {
   var products []Product
   for product := range products {
      categoryName := getCategoryName(product.getCategoryId())
      product.setCategoryName()
   }
}

if else巢狀

這個在寫golang不是很常見,因為寫久了就會有一種錯誤及時處理的概念。

同一張資料庫表的查詢,每換一種查詢方式就寫一個函式

public interface CategoryMapper xxxx {
    @Select("select * from category where name = #{name}")
    public List<Category> findByName(@Param("name") String name);
    
    @Select("select * from category where id = #{id}")
    public List<Category> findById(@Param("id") Long id);

    @Select("select * from category where parentId = #{parentId}")
    public List<Category> findByParentId(@Param("parentId") Long parentId);

    @Select("select * from category where status = #{status}")
    public List<Category> findByStatus(@Param("status") Integer status);

    //以下略
}

解決方案:使用sql builder等類似開源工具

工作流update不考慮修改前的state

其實就是狀態機,一種狀態要滿足一些條件才能轉移到另一種狀態,但是轉移前我們要確定前置狀態,尤其是現在的程式大多數都是多執行緒環境下執行。

很常見的一種update sql,其實這問題可大了,不合理的處理方案是在其他地方對某個前置狀態再查一次,的確這樣可以避免前端上的錯誤反饋,但是問題是已經持久化了,難道還要包在一個事務裡?

update xxx set status = yyy where id = zzz;

解決方案:比如我們在上面這種update時多加一個,解決方案很多,大致就是“訂單流狀態機樂觀鎖”這種關鍵字搜尋就好了。

where status = ?

open資源不關閉

套接字,檔案描述符,連線池~~~

golang就好多了,記住defer close()

抱怨!

抱怨前要有證據,如果你說是因為自身之外的原因導致的bug或者效能問題,請拿出證據,否則只能給人一種甩鍋,態度問題。

結語

希望早日能成為一個合格的程式設計師,曹大是我的學習榜樣。

學習自曹大blog

https://xargin.com/rookie-programmer-faults/