La complexité cyclomatique est une métrique qui mesure la complexité et l'enchevêtrement du code.
Une complexité cyclomatique élevée n'est pas une bonne chose, bien au contraire.
En termes simples, la complexité cyclomatique est directement proportionnelle au nombre de chemins d'exécution possibles dans un programme. En d'autres termes, la complexité cyclomatique et le nombre total d'instructions conditionnelles (notamment leur imbrication) sont étroitement liées.
Alors aujourd'hui, parlons des instructions conditionnelles.
En 2007, Francesco Cirillo a lancé un mouvement appelé Anti-if.
Francesco Cirillo est le gars qui a inventé la technique Pomodoro. J'écris ce billet de blog en ce moment même « sous le Pomodoro ».
Je suppose que nous avons tous rapidement compris de quoi parle cette campagne grâce à son nom. Il est intéressant de noter que le mouvement compte parmi ses adeptes un certain nombre d’informaticiens.
Leurs arguments sont solides : si les déclarations sont mauvaises, cela conduit à une croissance exponentielle des chemins d'exécution des programmes.
En bref, c’est la complexité cyclomatique. Plus il est élevé, plus il est difficile non seulement de lire et de comprendre le code mais aussi de le couvrir de tests.
Bien sûr, nous avons une sorte de métrique « opposée » : la couverture du code, qui montre la quantité de votre code couverte par les tests. Mais cette métrique, ainsi que les riches outils de nos langages de programmation pour vérifier la couverture, justifient-elles d'ignorer la complexité cyclomatique et de saupoudrer des déclarations if uniquement basées sur « l'instinct » ?
Je ne pense pas.
Presque chaque fois que je me surprends à imbriquer un if dans un autre, je me rends compte que je fais quelque chose de vraiment idiot qui pourrait être réécrit différemment - soit sans si imbriqués, soit sans si du tout.
Vous avez remarqué le mot « presque », n'est-ce pas ?
Je n’ai pas commencé à le remarquer tout de suite. Si vous regardez mon GitHub, vous trouverez plus d'un exemple d'ancien code avec non seulement une grande complexité cyclomatique, mais aussi une pure folie cyclomatique.
Qu'est-ce qui m'a aidé à prendre davantage conscience de ce problème ? Probablement de l'expérience et quelques choses intelligentes que j'ai apprises et adoptées il y a environ un an. C'est ce que je veux partager avec vous aujourd'hui.
Vérifier quelque chose alors que nous ne le « savons » pas encore est probablement la source la plus courante d'utilisation d'instructions conditionnelles basées sur « l'instinct ».
Par exemple, supposons que nous devions faire quelque chose en fonction de l'âge d'un utilisateur et que nous devons nous assurer que l'âge est valide (se situe dans des fourchettes raisonnables). Nous pourrions nous retrouver avec un code comme celui-ci :
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")
Nous avons tous vu et probablement écrit du code similaire des centaines de fois.
Comment éliminer ces contrôles conditionnels en suivant le méta-principe discuté ?
Dans notre cas spécifique avec l'âge, nous pouvons appliquer mon approche préférée : s'éloigner de l'obsession primitive pour utiliser un type de données personnalisé.
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
Hourra, un de moins si ! La validation et la vérification de l'âge se font désormais toujours « là où l'âge est connu » — sous la responsabilité et dans le cadre d'une classe distincte.
Nous pouvons aller plus loin/différemment si nous voulons supprimer le if dans la classe Age, peut-être en utilisant un modèle Pydantic avec un validateur ou même en remplaçant if par assert — cela n'a plus d'importance maintenant.
D'autres techniques ou mécanismes qui aident à se débarrasser des vérifications conditionnelles au sein de cette même méta-idée incluent des approches telles que le remplacement des conditions par du polymorphisme (ou des fonctions lambda anonymes) et la décomposition de fonctions qui ont des drapeaux booléens sournois.
Par exemple, ce code (horrible boxe, non ?) :
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}."
Et peu importe si vous remplacez if/elif par match/case — c'est la même poubelle !
Il est assez facile de le réécrire comme :
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}."
n'est-ce pas ?
L'exemple de décomposition d'une fonction avec un indicateur booléen en deux fonctions distinctes est aussi vieux que le temps, douloureusement familier et incroyablement ennuyeux (à mon avis honnête).
def process_transaction(transaction_id: int, amount: float, is_internal: bool) -> None: if is_internal: # Process internal transaction pass else: # Process external transaction pass
Deux fonctions seront de toute façon bien meilleures, même si les 2/3 du code qu'elles contiennent sont identiques ! C'est l'un de ces scénarios où un compromis avec DRY est le résultat du bon sens, rendant le code tout simplement meilleur.
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.
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!