If-Anweisungen design: guard-Klauseln können alles sein, was Sie brauchen

Es wird viel darüber diskutiert, wie die if-Anweisungen für eine bessere Klarheit und Lesbarkeit des Codes verwendet werden sollen. Die meisten von ihnen laufen auf eine Meinung hinaus, die völlig subjektiv und ästhetisch ist. Normalerweise stimmt das, aber manchmal können wir immer noch eine gemeinsame Basis finden. Dieser Beitrag zielt darauf ab, eine kleine Reihe von Ratschlägen für einige dieser Fälle abzuleiten. Hoffentlich wird dies das Nachdenken und Diskutieren reduzieren, wenn etwas so Einfaches wie eine if-Anweisung geschrieben wird.

Schutzklauseln

Der wahrscheinlich dokumentierteste Fall ist die Schutzklausel, die auch als Geltendmachung oder Vorbedingung bezeichnet wird. Die Idee ist, dass Sie, wenn Sie am Anfang einer Methode etwas zu behaupten haben, dies mit einer schnellen Rückkehr tun.

public void run (Timer timer) {
  if (! timer.isEnabled ())
    Rückkehr;
  
  if (! timer.valid ())
    werfen neue InvalidTimerException ();
  timer.run ();
}

Obwohl es offensichtlich aussieht, bleibt es oft aufgrund der Tatsache zurück, dass unser Gehirn anders funktioniert: Wir neigen dazu zu der Annahme, dass "der Timer ausgeführt wird, wenn er aktiviert ist", nicht "der Timer ausgeführt wird, aber wenn er nicht aktiviert ist, nichts zu tun". Letzteres führt jedoch im Code zu einer besseren Trennung von Eckfällen von der Hauptmethodenlogik, wenn wir genau befolgen, dass nur etwas, was am Anfang behauptet wurde, als Schutzklausel betrachtet werden kann.

Wie es scheint, ist die Schutzklausel bei weitem keine neue Idee. Dennoch sehe ich es als eines der einfachsten und direktesten an, das ohne viel Debatte akzeptiert werden kann. Die größte Entdeckung für mich war jedoch, dass die meisten Fälle, in denen die if-Anweisung verwendet wird, in eine Guard-Klausel umgewandelt werden können. Darauf werde ich näher eingehen.

Große Wenn-Blöcke

Es ist ziemlich üblich, in einer Codebasis Folgendes zu finden:

if (something.isEnabled ()) {
  // ziemlich
  // lang
  // Logik
  // von
  // Laufen
  // etwas
}

Normalerweise ist es nicht von Anfang an so gestaltet. Stattdessen erscheinen solche Blöcke, wenn eine neue Anforderung angezeigt wird, die besagt: "Wir sollten das nur im Fall von X ausführen". Die einfachste Lösung wäre natürlich, den Teil "run that something" in eine if-Anweisung zu packen. Einfach.

Das Problem ist, dass es eigentlich nur leicht ist zu schreiben, aber nicht zu lesen. Sehen Sie, um den Code zu verstehen, muss man das ganze Bild daraus ableiten, ein Konzept, mit dem man weiterarbeiten kann - denn Menschen können viel besser mit Konzepten arbeiten, nicht mit Algorithmen. Wenn Sie diese Art von Bedingung am Anfang einführen, entsteht ein zusätzlicher Kontext, der bei der Arbeit mit der Hauptmethodenlogik berücksichtigt werden muss. Selbstverständlich sollten wir uns bemühen, den Umfang des mentalen Kontexts zu verringern, der zu einem bestimmten Zeitpunkt erforderlich ist.

Ein weiteres großes Problem tritt auf, wenn die Logik eines solchen Codes lang genug wird, um den Bildschirm nicht vertikal anzupassen. Irgendwann vergisst du vielleicht sogar, dass es da ist, was dein Bild komplett zerstören würde.

Um all dies zu vermeiden, müssen Sie nur die if-Anweisung invertieren, damit die Bedingung zu einer Guard-Klausel wird:

if (! something.isEnabled ())
  Rückkehr;
// gleich
// lang
// Logik
// von
// Laufen
// etwas

Auch hier gibt es eine Trennung der Bedenken: Zuerst werden die Voraussetzungen zu Beginn beseitigt (und auch die Voraussetzungen werden aus dem Kopf geworfen - was wichtig ist, um sich besser auf die Hauptlogik zu konzentrieren), und dann wird genau das getan, was für die Methode erforderlich ist machen.

Rückkehr in die Mitte

Mehrfachrückgaben werden aus Gründen als schlechte Idee angesehen, und dies ist wahrscheinlich eine der größten. Im Gegensatz zu einem Return in einer Guard-Klausel ist ein Return oder Throw in der Mitte der Methode auf den ersten Blick nicht so einfach zu erkennen. Wie jede andere bedingte Logik erschwert sie den Prozess der Erstellung eines mentalen Bildes der Methode. Außerdem müssen Sie berücksichtigen, dass die Hauptlogik dieser Methode unter Umständen nicht vollständig ausgeführt wird und Sie jedes Mal entscheiden müssen, ob Sie den neuen Code vor oder nach dieser Rückkehr einfügen möchten.

Hier ist ein Beispiel für echten Code:

public void executeTimer (String timerId) {
  logger.debug ("Timer mit der ID {} ​​ausführen", timerId);
  TimerEntity timerEntity = timerRepository.find (timerId);
  logger.debug ("TimerEntity {} für Timer-ID {} ​​gefunden", timerEntity, timerId);
  if (timerEntity == null)
    Rückkehr;
  Timer timer = Timer.fromEntity (timerEntity);
  timersInvoker.execute (timer);
}

Sieht nach einer Vorbedingung aus, nicht wahr? Sagen wir, wo eine Voraussetzung sein muss:

public void executeTimer (String timerId) {
  logger.debug ("Timer mit der ID {} ​​ausführen", timerId);
  TimerEntity timerEntity = timerRepository.find (timerId);
  logger.debug ("TimerEntity {} für Timer-ID {} ​​gefunden", timerEntity, timerId);
  executeTimer (timerEntity);
}
private void executeTimer (TimerEntity timerEntity) {
  if (timerEntity == null)
    Rückkehr;
  Timer timer = Timer.fromEntity (timerEntity);
  timersInvoker.execute (timer);
}

Dieser überarbeitete Code ist viel einfacher zu lesen, da die Hauptlogik keinerlei Bedingungen enthält, sondern lediglich eine Folge von einfachen Aktionen. Was ich zeigen wollte, ist, dass es fast immer möglich ist, eine Methode mit einem Return in der Mitte aufzuteilen, so dass die Voraussetzungen gut von der Hauptlogik getrennt sind.

Kleine bedingte Aktionen

Es ist auch üblich, viele kleine bedingte Anweisungen wie die folgende zu haben:

if (timer.getMode ()! = TimerMode.DRAFT)
  timer.validate ();

Dies ist im Allgemeinen völlig legitim, aber oftmals nur eine versteckte Voraussetzung, die besser in einer Methode selbst verankert werden sollte. Sie werden dies wahrscheinlich weiter bemerken, wenn Sie jedes Mal, wenn Sie die Methode aufrufen, diese if-Anweisung hinzufügen müssen, da die Geschäftslogik dies vorgibt. In Anbetracht dessen wäre die richtige Lösung:

public void validate () {
  if (mode == TimerMode.DRAFT)
    Rückkehr;
  
  // Validierungslogik
}

Und die Bedienung ist jetzt so einfach wie:

timer.validate ();

Fazit

Die Verwendung von Schutzklauseln ist eine gute Methode, um unnötige Verzweigungen zu vermeiden und Ihren Code schlanker und lesbarer zu machen. Ich glaube, wenn diese kleinen Ratschläge systematisch berücksichtigt werden, werden die Wartungskosten Ihrer Software erheblich sinken.

Und schließlich ist es im Allgemeinen nichts Schlimmes, hier oder da eine bedingte Anweisung in Ihrem Code zu haben. Obwohl meine Erfahrung zeigt, dass Sie irgendwann Stunden damit verschwenden werden, ein mentales Bild eines komplexen, stark verzweigten Codes zu erstellen, während Sie versuchen, das nächste, was ein Unternehmen möchte, in den Code zu integrieren .