循环复杂度是衡量代码复杂性和混乱程度的指标。
高圈复杂度并不是一件好事,恰恰相反。
简单来说,圈复杂度与程序中可能的执行路径的数量成正比。换句话说,圈复杂度和条件语句的总数(尤其是它们的嵌套)密切相关。
所以今天我们来谈谈条件语句。
反如果
2007年,francesco cirillo发起了一场名为anti-if的运动。
francesco cirillo 是发明番茄工作法的人。我现在正在“番茄钟下”写这篇博文。
我想我们都很快从它的名字就明白了这个活动的意义。有趣的是,该运动的追随者中有不少计算机科学家。
他们的论点坚如磐石——如果语句是邪恶的,会导致程序执行路径呈指数级增长。
简而言之,这就是圈复杂度。它越高,不仅阅读和理解代码就越困难,而且用测试来覆盖它也就越困难。
当然,我们有一种“相反”的指标——代码覆盖率,它显示了测试覆盖了多少代码。但是这个指标以及我们编程语言中用于检查覆盖率的丰富工具是否证明忽略圈复杂度并仅基于“本能”散布 if 语句是合理的?
我觉得不是。
几乎每次我发现自己要将一个 if 嵌套在另一个 if 中时,我都会意识到我正在做一些非常愚蠢的事情,可以用不同的方式重写 - 要么没有嵌套的 if ,要么根本没有 if 。
你确实注意到了“几乎”这个词,对吧?
我并没有立即开始注意到这一点。如果您查看我的 github,您会发现不止一个旧代码示例,它们不仅具有高圈复杂度,而且具有直接的圈复杂度。
是什么帮助我更加意识到这个问题?可能是我一年前学到和接受的经验和一些聪明的事情。这就是我今天要跟大家分享的内容。
破坏 if 语句的两种神圣技术
- padawan,将未知值的每个条件检查移动到该值已知的地方。
- 学徒,改变你的编码逻辑思维模型,让它不再需要条件检查。
1. 让未知为人所知
在我们还不“知道”某事时检查它可能是使用基于“本能”的条件语句的最常见来源。
例如,假设我们需要根据用户的年龄做一些事情,并且我们必须确保年龄有效(在合理范围内)。我们最终可能会得到这样的代码:
from typing import optional def process_age(age: optional[int]) -> none: if age is none: raise valueerror("age cannot be null") if age 150: raise valueerror("age must be between 0 and 150")
登录后复制
我们都见过并且可能写过数百次类似的代码。
我们如何通过遵循所讨论的元原则来消除这些条件检查?
在我们特定的年龄情况下,我们可以应用我最喜欢的方法——摆脱原始的痴迷,转向使用自定义数据类型。
class age: def __init__(self, value: int) -> none: if 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 的权衡是常识的结果,使代码变得更好。
这里最大的区别是,从机械角度来说,在自动驾驶仪上,我们不太可能使用这些方法,除非我们已经内化并养成了通过这一原则进行思考的习惯。
否则,我们会自动陷入 if: if: elif: if...
2. 释放你的思想,尼奥
事实上,第二个技巧才是真正的技巧,而之前的“第一个”技巧只是准备练习,是到位的捷径:)
事实上,实现更简单的代码、降低圈复杂度和减少条件检查的唯一最终方式、方法(无论你怎么称呼它)是改变我们在头脑中建立的用于解决特定问题的心理模型 .
我保证,今天最后一个愚蠢的例子。
考虑一下,我们正在紧急为一些在线商店编写一个后端,用户可以在不注册或使用注册的情况下进行购买。
当然,系统有一个 user 类/实体,完成这样的事情很容易:
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
登录后复制
但是注意到这些废话,由于我们的思维已经转向正确的方向(我相信),我们将回到 user 类的定义位置并重写部分代码,如下所示:
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
登录后复制
因此,这一切的本质和美妙之处在于,我们不会用各种模式和编码技术来消除条件语句等,从而使我们的头脑变得混乱。
通过将我们的注意力转移到元级别,转移到更高的抽象级别,而不仅仅是对代码行的推理级别,并遵循我们今天讨论的想法,消除条件检查的正确方法,并且一般来说,更正确的代码将会自然出现.
我们代码中的很多条件检查都是由于被诅咒的 none/null 泄漏到我们的代码中而产生的,所以值得一提的是非常流行的 null 对象模式。
执着于文字,而不是意义
当遵循反如果时,你可能会走上错误的道路,因为执着于文字而不是意义,并盲目地遵循“如果是坏的,如果必须被删除”的想法。
由于条件语句是语义而不是语法元素,因此有无数种方法可以从代码中删除 if 标记,而无需更改我们喜爱的编程语言中的底层逻辑。
我在这里讨论的不是用 match/case 替换 python 中的 elif 链。
逻辑条件源于系统的心理“模型”,并且没有通用方法可以完全“删除”条件。
换句话说,圈复杂度和整体代码复杂度与代码的物理表示(文件中写入的字母和符号)无关。
复杂性来自于形式表达,口头或文字解释特定代码为何以及如何工作。
因此,如果我们更改代码中的某些内容,并且 if 语句更少或根本没有,但相同代码的口头解释保持不变,我们所做的只是更改代码的表示,并且更改本身并没有真正的意义或做出任何改进。
以上就是避免条件语句的智慧的详细内容,更多请关注其它相关文章!