动态

详情 返回 返回

「譯」布爾值的坑,你踩過嗎? - 动态 详情

原文地址:Booleans Are a Trap
原文作者:Katafrakt
本文永久鏈接:https://segmentfault.com/a/1190000046582649
譯者:ChatGPT
校對者:Fw惡龍

作為開發者,我們經常使用布爾值(Boolean)。它們與計算機底層的工作方式完美契合,且與if語句等控制流工具配合得天衣無縫。布爾值簡單易懂,似乎沒有什麼不妥。

我們甚至喜歡到將布爾值用於業務建模。然而,問題也由此而生。本文將通過一些例子展示使用布爾值可能帶來的混亂,並提出更優的替代方案。

期望與現實的差距

業務建模是軟件工程師的核心職責之一。簡而言之,它是將現實世界的問題通過代碼、數據庫、網絡調用等方式進行抽象和表示。然而,現實世界是複雜、混亂且難以預測的,往往無法完全契合我們理想的確定性模型。此外,軟件開發中的一個確定性事實是:需求會不斷變化。

這與布爾值有何關係?

假設你需要建模一個門(Door)。看起來很簡單,對吧?

class Door {
  public isOpen: boolean;
}

這似乎很容易理解。你可能還會添加openclose方法,總體上沒有什麼複雜的地方。我們正如預期地使用了布爾值——門要麼是打開的,要麼是關閉的。還能有什麼其他狀態呢?

你高興地提交了代碼,但幾周後,一個客户提出他們的門不僅僅是打開或關閉的,還可以上鎖!產品經理為你添加了一個新需求。幸運的是,這仍然很容易實現。

class Door {
  public isOpen: boolean;
  public isLocked: boolean;
}

我們再次使用了布爾值,門可以上鎖或不上鎖,還有什麼其他可能嗎?需求完成了。

但讓我們停下來重新審視這個模型。它有兩個屬性,每個屬性都有兩個可能的值。簡單的數學告訴我們,門可以處於四種狀態之一。等等,四種?

  • 關閉且上鎖
  • 關閉且未上鎖
  • 打開且未上鎖
  • 打開且上鎖

最後一種狀態似乎不合理。現實中,技術上你可以在門打開時上鎖,但這是否真正改變了門的狀態?然而,我們的模型允許這種不可能的組合。我可以肯定地説,某個時候你會遇到一個對象,其狀態為isOpen: true, isLocked: true。即使你非常小心,代碼中沒有官方路徑導致這種狀態,可能由於數據庫中的某次遷移,某人不小心將特定建築物的所有門設置為open

既然這種情況可能發生,你的代碼就需要為此做好準備。你應該為這種情況編寫測試。這會增加代碼量、複雜性,以及測試套件的運行時間(即使只是幾毫秒)。

超越門的例子

好吧,這只是一個簡單的例子,可能容易被忽視。

讓我講一個基於我實際工作經驗的小故事。該項目是一個 B2B SaaS 平台,主要實體是Company。我們並沒有標準的定價,所有合同都是在系統外單獨協商的。系統中唯一的表示是isPaying: boolean。一些公司仍在評估階段,即未付款,另一些公司已經簽約。這個模型運作良好,因為付款與否是唯一的區分因素,沒有標準的試用期等。對於付款的公司,啓用了額外功能,顯示了發票等。簡單的布爾值解決了問題。

幾個季度後,出現了新的業務情況。我們現在有了“合作伙伴公司”:他們獲得了完整的服務包,但是免費的——或者説,是以非貨幣的服務交換,例如在某處展示我們的公司,或向我們免費提供他們的服務。由於他們沒有付款,顯示發票部分會引起誤解。原來的布爾值已無法滿足需求,因此添加了另一個布爾值:isPartner: boolean

你可能已經看出,類似於門的情況再次發生。合作伙伴從未付款,但系統允許布爾值的這種組合,我們需要處理它,否則可能會導致嚴重的異常。但故事並未就此結束……

隨着時間的推移,我們添加了更多功能。例如:AI 功能。一些合同包含這些功能,一些則沒有,但在試用期間從不允許。因此,我們又添加了一個布爾值:isAIEnabled,導致了一個不可能的狀態:未付款的公司啓用了 AI 功能。布爾值列表不斷增加,某個時候我數了數,已經有 12 個不同的布爾標誌。這意味着有 4096 種可能的組合,其中可能只有 20 種是有效的。這一切都是因為我們最初依賴於簡單的布爾值,並且在適當的時候沒有及時制止添加新的布爾值。

更好的方法

如果我已經説服你相信使用布爾值可能會導致問題,你可能會問:“那有什麼解決方案?”確實,我有一個:使用枚舉(enums)和枚舉集合(enum sets)。

讓我們從修復門的情況開始。考慮以下代碼:

enum DoorState {
  Open,
  Closed,
  Locked,
}

class Door {
  public state: DoorState;
}

這可能看起來有些過度設計,但你已經聽過前面的故事,所以可以猜到添加LockedDoorState是我們的下一步,這樣我們就得到了門的三種可能狀態。

對於公司,我們可以對合同狀態採用類似的解決方案。

enum ContractStatus {
  Trial,
  Paying,
  Partner,
}

class Company {
  public contractStatus: ContractStatus;
}

這樣,對於完整服務包中可用的功能,我們可以檢查狀態是否為PayingPartner,但對於顯示發票頁面,我們只需檢查是否為Paying

我們解決了第一個問題,但對於僅對部分付款公司開放的額外功能呢?我們需要另一個枚舉和一個集合。

enum PremiumFeature {
  Ai,
  ApiAccess,
  Sso,
  SalesforceIntegration,
  // ...
}

class Company {
  public premiumFeatures: Set<PremiumFeature>;
}

細心的讀者可能注意到,結合這兩個仍然會導致無法達到的狀態。這是真的,有時確實很難避免。但我們可以推斷出,整體狀態遠少於 4096 種,只有一種狀態是無法達到的:公司處於Trial狀態,卻擁有任何高級功能。這本質上只是一個需要處理的情況,更容易測試和防範。我堅信這比布爾值的爆炸要好得多。

布爾值本身有問題嗎?

讀完這篇文章後,你可能會想,是否應該完全避免使用布爾值,因為它們本身就不好。答案是:當然不是!布爾值絕對有其適用之處。我的建議是將布爾值的使用限制在技術層面,而不是業務邏輯或業務模型中。

例如,我們前面提到了集合。假設你編寫了一個集合的實現。你需要一個hasElement方法。你可以使用枚舉作為返回值嗎?當然可以。但實際上,在這裏使用布爾值完全沒問題,也更自然。因為你不會得到第三個值,比如“可能”或“很可能”。這是一個較低層次的技術概念,布爾值在這裏完全適用。

附加內容:狀態機的基礎

當你通過枚舉(enum)梳理清楚所有可能的狀態後,其實就已經為另一個非常實用的工具——狀態機(state machine)打下了很好的基礎。以我們的門為例,它的狀態機可能如下所示:

當前狀態 事件 結果狀態
Open Close Closed
Closed Lock Locked
Closed Open Open
Locked Unlock Closed

就我個人而言,有時我對在業務邏輯中使用狀態機持保留態度。畢竟,最終業務需求可能要求每個步驟之間都可以轉換——因為人們會犯錯,需要有辦法修復它們。但如果你想使用狀態機,從枚舉開始會比從一堆布爾標誌開始容易得多。

結語

“布爾值的陷阱”只是一個例子,説明那些看起來簡單的建模決策,隨着系統發展,可能會帶來意想不到的問題。

布爾值在它本來的用途上——表示某個技術層面的“是/否”狀態——是非常合適的。但一旦將它用於表示複雜的業務狀態,它往往就不夠用了,甚至會誤導我們。

相比之下,使用枚舉(enum)或枚舉集合(enum set)能幫助我們構建更健壯、更貼近實際業務世界的模型,讓代碼更容易演進、擴展,也更能體現真實的業務規則。

有時候,與其盲目地再加一個布爾字段,不如重新思考我們究竟該如何表達這個狀態。這不僅能讓系統更清晰,也能避免陷入日後難以維護的困境。

user avatar huizhudev 头像 tizuqiudexiangpica 头像 amin_671603a27d1a0 头像
点赞 3 用户, 点赞了这篇动态!
点赞

Add a new 评论

Some HTML is okay.