編碼之道:小函數的大威力

一屏之地,一覽無餘!對的!要的就是短小精悍!

翻開項目的代碼,處處可見成百上千行的函數,函數體裏面switch-case、if、for等交錯在一起,一眼望不到頭的感覺。有些變態的函數,長度可能得按公里計算了。神啊,請賜予我看下去的勇氣吧!先不論邏輯如何,首先這長度直接就把人給嚇到了。這些超大號函數是怎麼來得呢?

更多內容:http://game-lab.org/archive.html

  • 直接從別處COPY一段代碼,隨便改改即可,造成大量重複代碼。
  • 缺少封裝,甚至說就沒有封裝,完全就是隨意亂加一氣,造成各個抽象層次的代碼混合在一起,混亂不堪。
  • 成篇的異常處理和特殊處理,核心邏輯或許就是函數體開頭、中間或結束那麼幾行而已。

這些超長的函數,給我們造成了很大的麻煩:閱讀代碼找BUG幾乎是不可能的事情,沒有調試器估計撞牆的心都有了;重複代碼造成修改困難,漏掉任何一處遲早是要出問題的;各個層次的代碼混在一起,閱讀代碼相當吃力,人的臨時記憶是有限的,不斷在各個層次之間切換,一會兒就給繞暈了。

解決這些問題最重要的就是要保持函數的短小,短小的函數閱讀起來要好得多,同時短小的函數意味着較好的封裝。下面談談關於函數,應該遵循的一些原則:

1. 原則:取個描述性的名字

  • 取個一眼就看出函數意圖的名字很重要
  • 長而具有描述性的名稱,要比短而讓人費解的好(長度適中,也不能過分長)
  • 使用動詞或動詞+名詞短語

編碼之道:取個好名字中已經介紹過,好名字的重要性,不再贅述。

2. 原則:保持參數列表的簡潔

  • 無參數最好,其次一元,再次二元,三元儘量避免
  • 儘量避免標識參數
  • 使用參數對象
  • 參數列表
  • 避免輸出和輸入混用,無法避免則輸出在左,輸入在右
bool isBossNpc();
void summonNpc(int id);
void summonNpc(int id, int type);
void summonNpc(int id, int state, int type); // 還能記得參數順序嗎?

void showCurrentEffect(int state, bool show); // Bad!!!
void showCurrentEffect(int state); // Good!!
void hideCurrentEffect(int state); // 新加個函數也沒多難吧?

bool needWeapon(DWORD skillid, BYTE& failtype); // Bad!!!

3. 原則:保持函數短小

  • 第一規則:要短小
  • 第二規則:還要更短小
  • 要做到“一屏之地,一覽無餘”更好

4. 原則:只做一件事

  • 函數應該只做一件事,做好這件事
  • 且只做這一件事

5. 原則:每個函數位於同一抽象層級

  • 要確保函數只做一件事,函數中的語句都要在同一個抽象層級上
  • 自頂下下讀代碼

6. 原則:無副作用

  • 謊言,往往名不副實

7. 原則:操作和檢查要分離

  • 要麼是做點什麼,要麼回答點什麼,但二者不可兼得”)
  • 混合使用—副作用的肇事者

8. 原則:使用異常來代替返回錯誤碼

  • 操作函數返回錯誤碼輕微違法了操作與檢查的隔離原則
  • 用異常在某些情況下會更好點
  • 抽離try-cacth
  • 錯誤處理也是一件事情,也應該封裝爲函數
bool RedisClient::connect(const std::string& host, uint16_t port)
{
    this->host = host;
    this->port = port;
    this->close();

    try 
    {
        redis_cli = new redis::client(host, port);
        return true;
    }
    catch (redis::redis_error& e) 
    {
        redis_cli = NULL;
        std::cerr << "error:" << e.what() << std::endl;
        return false;
    }

    return false;
}

9. 原則:減少重複代碼”

重複是一些邪惡的根源!!!

10. 原則:避免醜陋不堪的switch-case

  • 天生要做N件事情的貨色
  • 多次出現就要考慮用多態進行重構

BAD:

bool saveBinary(type, data) {
   switch (type) {
     case TYPE_OBJECT:
           ....
          break;
     case TYPE_SKILL:
           ...
          break;
     ....
   }
}
bool needSaveBinary(type) {
   switch (type) {
     case TYPE_OBJECT:
          return true;
     case TYPE_SKILL:
           ...
          break;
     ....
   }
}

class BinaryMember
{
  BinaryMember* createByType(type){
   switch (type) {
     case TYPE_OBJECT:
          return new ObjectBinaryMember;
     case TYPE_SKILL:
          return new SkillBinaryMember;
     ....
  }

  virtual bool save(data);
  virtual bool needSave(data);
};

class ObjectBinaryMember : public BinaryMember
{
   bool save(data){
       ....
   }
   bool needSave(data){
       ....
   }
};")))

最後

上面提到的原則,若要理解的更加深刻,建議去閱讀《代碼整潔之道》,裏面有許多詳盡的例子,對於寫過幾年代碼的人來說,總會發現一些自己所在項目經常犯的毛病。

知道了這些原則,我們應該這樣做:

當在添加新函數的時候:

  • 剛下手時違反規範和原則沒關係
  • 開發過程中逐步打磨
  • 保證提交後的代碼是整潔的即可

重構現有的函數,有下面情況的,見一個消滅一個:

  • 冗長而複雜
  • 有太多縮進和嵌套循環
  • 參數列表過長
  • 名字隨意取
  • 重複了三次以上
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章