Die Weisheit, bedingte Aussagen zu vermeiden

PHPz
Freigeben: 2024-08-14 10:41:30
Original
437 Leute haben es durchsucht

The Wisdom of Avoiding Conditional Statements

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.

Anti-wenn

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.


Zwei heilige Techniken zur Zerstörung von if-Anweisungen

  1. Padawan, verschieben Sie jede bedingte Prüfung eines unbekannten Werts an eine Stelle, an der dieser Wert bereits bekannt ist.
  2. Padawan, ändern Sie Ihr mentales Modell der codierten Logik, sodass keine bedingten Prüfungen mehr erforderlich sind.

1. Unbekanntes bekannt machen

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")
Nach dem Login kopieren

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
Nach dem Login kopieren

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}."
Nach dem Login kopieren

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}."
Nach dem Login kopieren

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
Nach dem Login kopieren

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...

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
Nach dem Login kopieren

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
Nach dem Login kopieren

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.

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!

Quelle:dev.to
Erklärung dieser Website
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Beliebte Tutorials
Mehr>
Neueste Downloads
Mehr>
Web-Effekte
Quellcode der Website
Website-Materialien
Frontend-Vorlage
Über uns Haftungsausschluss Sitemap
Chinesische PHP-Website:Online-PHP-Schulung für das Gemeinwohl,Helfen Sie PHP-Lernenden, sich schnell weiterzuentwickeln!