避免條件語句的智慧

PHPz
發布: 2024-08-14 10:41:30
原創
346 人瀏覽過

The Wisdom of Avoiding Conditional Statements

循環複雜度是衡量程式碼複雜度和混亂程度的指標。

高圈複雜度並不是一件好事,恰恰相反。

簡單地說,圈複雜度與程式中可能的執行路徑的數量成正比。換句話說,圈複雜度和條件語句的總數(尤其是它們的巢狀)密切相關。

今天我們來談談條件語句。

反如果

2007 年,Francesco Cirillo發起了一場名為 Anti-if 的運動。

Francesco Cirillo 是發明番茄工作法的人。我現在正在「番茄鐘下」寫這篇文章。

我想我們都很快從這個活動的名字就明白了它的意義。有趣的是,該運動的追隨者中有不少計算機科學家。

他們的論點堅如磐石-如果語句是邪惡的,會導致程式執行路徑呈指數級增長。

簡而言之,這就是圈複雜度。它越高,不僅閱讀和理解程式碼就越困難,而且用測試來覆蓋它就越困難。

當然,我們有一個「相反」的指標——程式碼覆蓋率,它顯示了測試覆蓋了多少程式碼。但是,這個指標以及我們程式語言中用於檢查覆蓋率的豐富工具是否證明忽略圈複雜度並僅基於「本能」散佈 if 語句是合理的?

我想不會。


幾乎每次我發現自己要將一個if 嵌套在另一個if 中時,我都會意識到我正在做一些非常愚蠢的事情,可以用不同的方式重寫- 要么沒有嵌套的if ,要嘛根本沒有if 。

你確實注意到了「幾乎」這個詞,對吧?

我並沒有立即開始注意到這一點。如果您查看我的 GitHub,您會發現不只一個舊程式碼範例,它們不僅具有高圈複雜度,而且具有直接的圈複雜度。

是什麼讓我更意識到這個問題?可能是我一年前學到和接受的經驗和一些聰明的事情。這就是我今天想跟大家分享的內容。


破壞 if 語句的兩種神聖技術

  1. Padawan,將未知值的每個條件檢查移至該值已知的位置。
  2. 學徒,改變你的編碼邏輯思考模型,讓它不再需要條件檢查。

1. 讓未知為人所知

當我們還不「知道」某件事時檢查它可能是使用基於「本能」的條件語句的最常見來源。

例如,假設我們需要根據使用者的年齡做一些事情,並且我們必須確保年齡有效(在合理範圍內)。我們最後可能會得到這樣的程式碼:

from typing import Optional

def process_age(age: Optional[int]) -> None:
    if age is None:
        raise ValueError("Age cannot be null")
    if age < 0 or age > 150:
        raise ValueError("Age must be between 0 and 150")
登入後複製

我們都看過並且可能編寫過數百次類似的程式碼。

我們如何透過遵循所討論的元原則來消除這些條件檢查?

在我們特定的年齡情況下,我們可以應用我最喜歡的方法——擺脫原始的痴迷,轉向使用自訂資料類型。

class Age:
    def __init__(self, value: int) -> None:
        if value < 0 or value > 150:
            raise ValueError("Age must be between 0 and 150")
        self.value = value

    def get_value(self) -> int:
        return self.value

def process_age(age: Age) -> None:
    # Age is guaranteed to be valid, process it directly
登入後複製

萬歲,少一個如果!年齡的驗證和驗證現在始終在「已知年齡的地方」—在單獨類別的職責和範圍內。

如果我們想刪除 Age 類別中的 if ,我們可以走得更遠/不同,也許可以使用帶有驗證器的 Pydantic 模型,甚至用斷言替換 if — 現在已經不重要了。


其他有助於擺脫同一元思想中的條件檢查的技術或機制包括用多態性(或匿名 lambda 函數)替換條件以及分解具有偷偷摸摸的布爾標誌的函數等方法。

例如,這段程式碼(可怕的拳擊,對吧?):

class PaymentProcessor:
    def process_payment(self, payment_type: str, amount: float) -> str:
        if payment_type == "credit_card":
            return self.process_credit_card_payment(amount)
        elif payment_type == "paypal":
            return self.process_paypal_payment(amount)
        elif payment_type == "bank_transfer":
            return self.process_bank_transfer_payment(amount)
        else:
            raise ValueError("Unknown payment type")

    def process_credit_card_payment(self, amount: float) -> str:
        return f"Processed credit card payment of {amount}."

    def process_paypal_payment(self, amount: float) -> str:
        return f"Processed PayPal payment of {amount}."

    def process_bank_transfer_payment(self, amount: float) -> str:
        return f"Processed bank transfer payment of {amount}."
登入後複製

如果你用 match/case 替換 if/elif 也沒關係——都是一樣的垃圾!

很容易重寫為:

from abc import ABC, abstractmethod

class PaymentProcessor(ABC):
    @abstractmethod
    def process_payment(self, amount: float) -> str:
        pass

class CreditCardPaymentProcessor(PaymentProcessor):
    def process_payment(self, amount: float) -> str:
        return f"Processed credit card payment of {amount}."

class PayPalPaymentProcessor(PaymentProcessor):
    def process_payment(self, amount: float) -> str:
        return f"Processed PayPal payment of {amount}."

class BankTransferPaymentProcessor(PaymentProcessor):
    def process_payment(self, amount: float) -> str:
        return f"Processed bank transfer payment of {amount}."
登入後複製

對嗎?


將帶有布林標誌的函數分解為兩個獨立函數的範例與時間一樣古老,令人痛苦地熟悉,並且令人難以置信地煩人(在我看來)。

def process_transaction(transaction_id: int,
                                                amount: float,
                                                is_internal: bool) -> None:
    if is_internal:
        # Process internal transaction
        pass
    else:
        # Process external transaction
        pass
登入後複製

無論如何,兩個函數都會好得多,即使其中 2/3 的程式碼是相同的!這是其中一種場景,其中與 DRY 的權衡是常識的結果,使程式碼變得更好。


The big difference here is that mechanically, on autopilot, we are unlikely to use these approaches unless we've internalized and developed the habit of thinking through the lens of this principle.

Otherwise, we'll automatically fall into if: if: elif: if...

2. Free Your Mind, Neo

In fact, the second technique is the only real one, and the earlier "first" technique is just preparatory practices, a shortcut for getting in place :)

Indeed, the only ultimate way, method — call it what you will — to achieve simpler code, reduce cyclomatic complexity, and cut down on conditional checks is making a shift in the mental models we build in our minds to solve specific problems.

I promise, one last silly example for today.

Consider that we're urgently writing a backend for some online store where user can make purchases without registration, or with it.

Of course, the system has a User class/entity, and finishing with something like this is easy:

def process_order(order_id: int,
                                  user: Optional[User]) -> None:
    if user is not None:
        # Process order for a registered user
       pass
    else:
        # Process order for a guest user
           pass
登入後複製

But noticing this nonsense, thanks to the fact that our thinking has already shifted in the right direction (I believe), we'll go back to where the User class is defined and rewrite part of the code in something like this:

class User:
    def __init__(self, name: str) -> None:
        self.name = name

    def process_order(self, order_id: int) -> None:
        pass

class GuestUser(User):
    def __init__(self) -> None:
        super().__init__(name="Guest")

    def process_order(self, order_id: int) -> None:
        pass
登入後複製

So, the essence and beauty of it all is that we don't clutter our minds with various patterns and coding techniques to eliminate conditional statements and so on.

By shifting our focus to the meta-level, to a higher level of abstraction than just the level of reasoning about lines of code, and following the idea we've discussed today, the right way to eliminate conditional checks and, in general, more correct code will naturally emerge.


A lot of conditional checks in our code arise from the cursed None/Null leaking into our code, so it's worth mentioning the quite popular Null Object pattern.

Clinging to Words, Not Meaning

When following Anti-if, you can go down the wrong path by clinging to words rather than meaning and blindly following the idea that "if is bad, if must be removed.”

Since conditional statements are semantic rather than syntactic elements, there are countless ways to remove the if token from your code without changing the underlying logic in our beloved programming languages.

Replacing an elif chain in Python with a match/case isn’t what I’m talking about here.

Logical conditions stem from the mental “model” of the system, and there’s no universal way to "just remove" conditionals entirely.

In other words, cyclomatic complexity and overall code complexity aren’t tied to the physical representation of the code — the letters and symbols written in a file.

The complexity comes from the formal expression, the verbal or textual explanation of why and how specific code works.

So if we change something in the code, and there are fewer if statements or none at all, but the verbal explanation of same code remains the same, all we’ve done is change the representation of the code, and the change itself doesn’t really mean anything or make any improvement.

以上是避免條件語句的智慧的詳細內容。更多資訊請關注PHP中文網其他相關文章!

來源:dev.to
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板
關於我們 免責聲明 Sitemap
PHP中文網:公益線上PHP培訓,幫助PHP學習者快速成長!