循環的複雑度は、コードの複雑さともつれ具合を測定する指標です。
循環的複雑性が高いことは良いことではなく、むしろその逆です。
簡単に言えば、循環的複雑さはプログラム内で可能な実行パスの数に正比例します。言い換えれば、循環的複雑さと条件文 (特にその入れ子) の総数は密接に関係しています。
今日は、条件文について話しましょう。
2007 年、フランチェスコ・シリロは、アンチイフと呼ばれる運動を開始しました。
フランチェスコ・シリロは、ポモドーロ・テクニックを考案した男です。私は今このブログ記事を「ポモドーロの下」で書いています。
皆さんはその名前から、このキャンペーンが何を意味するのかすぐに理解できたと思います。興味深いことに、この運動の信奉者にはかなりの数のコンピューター科学者が含まれています。
彼らの議論は盤石であり、if ステートメントは邪悪であり、プログラムの実行パスの急激な増加につながります。
一言で言えば、それは循環的複雑さです。値が高くなるほど、コードを読んで理解するだけでなく、テストでカバーすることも難しくなります。
確かに、ある種の「逆」の指標があります。それは、コードのどの程度がテストでカバーされているかを示すコード カバレッジです。しかし、この指標と、カバレッジをチェックするためのプログラミング言語の豊富なツールは、循環的複雑性を無視し、「本能」だけに基づいて if ステートメントを散りばめることを正当化するのでしょうか?
私はそうではないと思います。
ある if を別の if の中に入れ子にしようとしていることに気づくたびに、私は自分が、別の方法で書き換えられる可能性のある本当に愚かなことをしていることに気づきます。それは、if を入れ子にしない、または if をまったく使わないのいずれかです。
「ほぼ」という言葉に気づきましたよね?
私はこれにすぐには気づきませんでした。私の GitHub を見ると、循環的な複雑さが高いだけでなく、まさに循環的な狂気を伴う古いコードの例が複数見つかります。
この問題をより認識するようになったのは何でしたか?おそらく、約 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
万歳、1 つ減りました!年齢の検証と検証は常に「年齢がわかっている場合」、つまり別のクラスの責任と範囲内で行われるようになりました。
Age クラスの if を削除したい場合は、おそらくバリデーターを備えた Pydantic モデルを使用するか、if を Assert に置き換えることによって、さらに別の方法を実行できます。それは今は問題ではありません。
同じメタアイデア内で条件チェックを取り除くのに役立つ他のテクニックやメカニズムには、条件をポリモーフィズム (または匿名ラムダ関数) で置き換えたり、卑劣なブール フラグを持つ関数を分解したりするようなアプローチが含まれます。
たとえば、次のコード (ひどいボクシングですね?):
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}."
そして、if/elif を match/case に置き換えても問題ありません。それは同じゴミです!
次のように書き直すのは非常に簡単です。
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}."
そうですか?
ブール値フラグを持つ関数を 2 つの別々の関数に分解する例は、非常に古いものですが、非常に見慣れたものですが、(私の正直な意見では) 信じられないほど面倒です。
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 が同一であっても、2 つの関数はどのような場合でもはるかに優れています。これは、DRY とのトレードオフが常識の結果であり、コードが改善されるシナリオの 1 つです。
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...
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.
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 中国語 Web サイトの他の関連記事を参照してください。