首页  >  文章  >  后端开发  >  避免条件语句的智慧

避免条件语句的智慧

PHPz
PHPz原创
2024-08-14 10:41:30429浏览

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中文网其他相关文章!

声明:
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn