Zyklomatische Komplexität ist eine Metrik, die die Komplexität und Verworrenheit von Code misst.
Hohe zyklomatische Komplexität ist keine gute Sache, ganz im Gegenteil.
Einfach ausgedrückt ist die zyklomatische Komplexität direkt proportional zur Anzahl möglicher Ausführungspfade in einem Programm. Mit anderen Worten: Die zyklomatische Komplexität und die Gesamtzahl der bedingten Anweisungen (insbesondere deren Verschachtelung) hängen eng zusammen.
Lassen Sie uns heute über bedingte Anweisungen sprechen.
Im Jahr 2007 gründete Francesco Cirillo eine Bewegung namens Anti-if.
Francesco Cirillo ist der Typ, der die Pomodoro-Technik erfunden hat. Ich schreibe diesen Blogbeitrag gerade „unter dem Pomodoro“.
Ich denke, wir haben alle anhand des Namens schnell herausgefunden, worum es bei dieser Kampagne geht. Interessanterweise zählt die Bewegung etliche Informatiker zu ihren Anhängern.
Ihre Argumente sind grundsolide – wenn Aussagen böse sind, führt das zu einem exponentiellen Wachstum der Programmausführungspfade.
Kurz gesagt, das ist zyklomatische Komplexität. Je höher es ist, desto schwieriger ist es, den Code nicht nur zu lesen und zu verstehen, sondern ihn auch mit Tests abzudecken.
Natürlich haben wir eine Art „entgegengesetzte“ Metrik – die Codeabdeckung, die zeigt, wie viel von Ihrem Code durch Tests abgedeckt wird. Aber rechtfertigt diese Metrik zusammen mit den umfangreichen Werkzeugen unserer Programmiersprachen zur Überprüfung der Abdeckung, die zyklomatische Komplexität zu ignorieren und if-Anweisungen zu verstreuen, die nur auf „Instinkt“ basieren?
Ich glaube nicht.
Fast jedes Mal, wenn ich mich dabei erwische, ein If in ein anderes zu verschachteln, wird mir klar, dass ich etwas wirklich Dummes mache, das anders umgeschrieben werden könnte – entweder ohne verschachtelte Wenns oder ohne Wenns überhaupt.
Ihnen ist das Wort „fast“ aufgefallen, oder?
Das ist mir nicht sofort aufgefallen. Wenn Sie sich meinen GitHub ansehen, werden Sie mehr als ein Beispiel für alten Code finden, der nicht nur eine hohe zyklomatische Komplexität, sondern geradezu zyklomatischen Wahnsinn aufweist.
Was hat mir geholfen, mich dieser Problematik bewusster zu werden? Wahrscheinlich Erfahrung und ein paar kluge Dinge, die ich vor etwa einem Jahr gelernt und angenommen habe. Das möchte ich heute mit Ihnen teilen.
Etwas zu überprüfen, wenn wir es noch nicht „wissen“, ist wahrscheinlich die häufigste Quelle für die Verwendung von bedingten Anweisungen, die auf „Instinkt“ basieren.
Angenommen, wir müssen etwas basierend auf dem Alter eines Benutzers tun und sicherstellen, dass das Alter gültig ist (innerhalb angemessener Bereiche liegt). Am Ende erhalten wir möglicherweise einen Code wie diesen:
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")
Wir haben alle schon hunderte Male ähnlichen Code gesehen und wahrscheinlich auch geschrieben.
Wie eliminieren wir diese bedingten Prüfungen, indem wir das besprochene Meta-Prinzip befolgen?
In unserem speziellen Fall mit dem Alter können wir meinen Lieblingsansatz anwenden – weg von der primitiven Besessenheit hin zur Verwendung eines benutzerdefinierten Datentyps.
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
Hurra, eins weniger wenn! Die Validierung und Verifizierung des Alters erfolgt nun immer „wo das Alter bekannt ist“ – in der Verantwortung und im Rahmen einer eigenen Klasse.
Wir können noch weiter/anders vorgehen, wenn wir das „if“ in der Age-Klasse entfernen möchten, vielleicht indem wir ein Pydantic-Modell mit einem Validator verwenden oder sogar „if“ durch „assertion“ ersetzen – das spielt jetzt keine Rolle.
Andere Techniken oder Mechanismen, die dabei helfen, bedingte Prüfungen innerhalb derselben Meta-Idee loszuwerden, umfassen Ansätze wie das Ersetzen von Bedingungen durch Polymorphismus (oder anonyme Lambda-Funktionen) und das Zerlegen von Funktionen, die hinterhältige boolesche Flags haben.
Zum Beispiel dieser Code (schreckliches Boxen, oder?):
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}."
Und es spielt keine Rolle, ob Sie if/elif durch match/case ersetzen – es ist derselbe Müll!
Es ist ganz einfach, es wie folgt umzuschreiben:
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}."
richtig?
Das Beispiel der Zerlegung einer Funktion mit einem booleschen Flag in zwei separate Funktionen ist so alt wie die Zeit, schmerzlich vertraut und unglaublich nervig (meiner ehrlichen Meinung nach).
def process_transaction(transaction_id: int, amount: float, is_internal: bool) -> None: if is_internal: # Process internal transaction pass else: # Process external transaction pass
Zwei Funktionen sind auf jeden Fall viel besser, auch wenn 2/3 des darin enthaltenen Codes identisch sind! Dies ist eines dieser Szenarien, in denen ein Kompromiss mit DRY das Ergebnis des gesunden Menschenverstandes ist und den Code einfach besser macht.
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.
Das obige ist der detaillierte Inhalt vonDie Weisheit, bedingte Aussagen zu vermeiden. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!