iT邦幫忙

2024 iThome 鐵人賽

DAY 30
0
Software Development

程式淨化計畫:痛苦是重構的起源!系列 第 30

Clean Code - Chapter 3 Functions - Part 2

  • 分享至 

  • xImage
  •  

以下是讀完 Clean code 第三章節(part 2)的筆記:

Have No Side Effects

  • 有些函數是有破壞性
    • 有時將某類別的狀態作非預期的變更
    • 有時內部變數亂指向傳遞參數或全域變數
  • 想一下這段程式碼錯在哪?
public class UserValidator {
  private Cryptographer cryptographer;
  public boolean checkPassword(String userName, String password) {
    User user = UserGateway.findByName(userName);
    if (user != User.NULL) {
      String codedPhrase = user.getPhraseEncodedByPassword();
      String phrase = cryptographer.decrypt(codedPhrase, password);
      if ("Valid Password".equals(phrase)) {
        Session.initialize();
        return true;
      }
    }
    return false;
  }
}
  • 這段副作用是Session.initialize()
    • 為何CheckPassword要有這種功能?
  • 導致CheckPassword如果在不正確的地方呼叫, session資料可能默默消失
  • 如果要[時序性耦合], 得重新命名
    • checkPasswordAndInitializeSession
    • 但這違反只做一件事的原則

Output Arguments

  • 看看appendFooter(s)
    • 他意思是將資料串在s後面? 還是什麼變數往後串s?
    • 得實際用函數定義, 發現是public void appendFooter(StringBuffer report), 得花時間確定用途
  • 在物件導向的世界, 輸出參數逐漸消失
  • 最理想是這樣使用
    • report.appendFooter()
  • 讓變更某狀態, 實作在物件的欄位

Command Query Separation

  • 函數就兩種功能

    • 要嘛做什麼事
    • 要嘛回答什麼事
  • 因此不要混和這兩種功能

  • 看看這個函數

    • public boolean set(String attribute, String value);
    • 設置成功回傳true,否則false
  • 使用情境

    • if (set("username", "unclebob"))…
    • 一開始觀察, 到底這個set是動詞還是形容詞? 無法知道兩個參數要做什麼
  • 如果重構成setAndCheckIfExists, 仍對if的可讀性沒提高

  • 最好是這樣重構, command與query分離

    if(attributeExists("username"))
    {
    	setAttribute("username", "unclebob");
    }
    

Prefer Exceptions to Returning Error Codes

  • 從command函數回傳錯誤碼, 是一種破壞CQS原則

  • 比如if(deletePage(page) == E_OK), 是一種簡單的情境

  • 但如果更深的nest

    if (deletePage(page) == E_OK) {
      if (registry.deleteReference(page.name) == E_OK) {
        if (configKeys.deleteKey(page.name.makeKey()) == E_OK) {
          logger.log("page deleted");
        } else {
          logger.log("configKey not deleted");
        }
      } else {
        logger.log("deleteReference from registry failed");
      }
    } else {
      logger.log("delete failed");
      return E_ERROR;
    }
    
  • 這樣是強迫呼叫者都得立刻處理錯誤碼

  • 透過拋出exception, 簡化錯誤處理

try 
{
	deletePage(page);
	registry.deleteReference(page.name);
	configKeys.deleteKey(page.name.makeKey());
}
catch (Exception e)
{
	logger.log(e.getMessage());
}

Extract Try/Catch Blocks

public void delete(Page page) {
  try {
    deletePageAndAllReferences(page);
  } catch (Exception e) {
    logError(e);
  }
}
private void deletePageAndAllReferences(Page page) throws Exception {
  deletePage(page);
  registry.deleteReference(page.name);
  configKeys.deleteKey(page.name.makeKey());
}
private void logError(Exception e) {
  logger.log(e.getMessage());
}
  • delete函數只與錯誤處理有關
  • deletePageAndAllReferences只有刪除page有關, 錯誤處理可忽略
  • 以上示範用Try/catch, 將正常流程與錯誤處理拆開的示範

Error Handling Is One Thing

  • 錯誤處理就是一件事! 因此處理錯誤的函數不該做其他事
  • 如果Try關鍵字在某函數存在, 那他就得是這個函數中第一個單字!!
  • catch/finally後面不該有其他內容

The Error.java Dependency Magnet

public enum Error {
	OK,
	INVALID,
	NO_SUCH,LOCKED,
	OUT_OF_RESOURCES,
	WAITING_FOR_EVENT;
}
  • 上述是有多個模組依賴這個Error處理碼, 但是當處理碼有新增修改時, 大家都得重新編譯和部署, 於是這稱為[依賴磁鐵]
  • 使用exception替代error code, 且新的Exception能用繼承基礎Exception類別產出新子類別, 不需要重新編譯和部署

Don’t Repeat Yourself

  • 程式碼3-1, 有SetUp, SuiteSetUp, TearDown, SuiteTearDown, 4個地方的邏輯相似
  • 當有邏輯修改, 就得修改4次
  • 而3-7程式碼用include函數修正了這重複性
  • 重複是軟體的萬惡根源

Structured Programming

  • 定義是每個函數只有一個入口與一個出口, 也就是得有一個return
  • 不可以有break和continue, 更不可以有goto
  • 這個用在大函數恰當, 但小函數沒必要.
  • goto千萬別用…

How Do You Write Functions Like This?

  • 一開始函數也是很長, 可以先用單元測試來覆蓋這醜陋程式碼
  • 接著才透過抽離函數、修改名稱、消除重複等重構, 甚至拆出類別, 不斷磨這個函數

Conclusion

  • 大師級程式設計師, 把系統當作是說故事, 手上的程式語言與相關工具, 打造出豐富且具表達力的語言, 來描述這故事
  • 函數可以乾淨俐落的拼裝一起, 形成可精準、清晰的語言, 幫助我們講故事

上一篇
Clean Code - Chapter 3 Functions - Part 1
下一篇
重構影響我多深?
系列文
程式淨化計畫:痛苦是重構的起源!31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言