Kapitel 4. Trennung der Belange
Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com
"Trennung der Belange" ... ist das, was ich mit "sich auf einen Aspekt konzentrieren" meine: Es bedeutet nicht, dass man die anderen Aspekte ignoriert, sondern nur, dass man der Tatsache gerecht wird, dass aus der Sicht dieses Aspekts der andere irrelevant ist. Es bedeutet, gleichzeitig ein- und mehrgleisig zu denken.
Edsger Dijkstra, "Über die Rolle des wissenschaftlichen Denkens"
Unser Quellcode ist gewachsen. Je nach Sprache sind es 50-75 Zeilen in einer Quelldatei. Das ist mehr als ein Bildschirm voll auf vielen Monitoren und sicherlich mehr als eine gedruckte Seite in diesem Buch.
Bevor wir zum nächsten Feature kommen, werden wir einige Zeit damit verbringen, unseren Code zu refaktorisieren. Das ist das Thema dieses und der nächsten drei Kapitel.
Test- und Produktionscode
Bis jetzt haben wir zwei verschiedene Arten von Code geschrieben.
-
Code, der unser Geldproblem löst. Dazu gehören
Money
undPortfolio
und alle darin enthaltenen Verhaltensweisen. Wir nennen dies Produktionscode. -
Code, der prüft, ob das Problem richtig gelöst wurde. Dazu gehören alle Tests und der Code, der zur Unterstützung dieser Tests benötigt wird. Wir nennen das Testcode.
Es gibt Ähnlichkeiten zwischen den beiden Arten von Code: Sie sind in der gleichen Sprache geschrieben, wir schreiben sie in schneller Folge (durch den mittlerweile bekannten Rot-Grün-Refactor-Zyklus) und wir übertragen beide in unser Code-Repository. Es gibt jedoch ein paar wichtige Unterschiede zwischen den beiden Arten von Code.
Unidirektionale Abhängigkeit
Der Testcode muss vom Produktionscode abhängen - zumindest von den Teilen des Produktionscodes, die er testet. In der anderen Richtung sollte es jedoch keine Abhängigkeiten geben.
Derzeit befindet sich unser gesamter Code für jede Sprache in einer Datei, wie in Abbildung 4-1 dargestellt. Es ist also nicht einfach, sicherzustellen, dass es keine zufälligen Abhängigkeiten zwischen Produktions- und Testcode gibt. Es besteht eine implizite Abhängigkeit zwischen dem Testcode und dem Produktionscode. Das hat einige Auswirkungen:
-
Wenn wir Code schreiben, müssen wir darauf achten, dass wir nicht versehentlich Testcode in unserem Produktionscode verwenden.
-
Beim Lesen von Code müssen wir die Nutzungsmuster erkennen und auch die fehlenden Muster bemerken, z. B. die Tatsache, dass der Produktionscode keinen Testcode aufrufen kann.
Wichtig
Der Testcode hängt vom Produktionscode ab; es sollte jedoch keine Abhängigkeit in die andere Richtung bestehen.
Wenn der Produktionscode vom Testcode abhängt, was sind dann die möglichen schlechten Ergebnisse? In besonders schlimmen Fällen kann es dazu führen, dass der Codepfad, der getestet wird, "makellos" ist, während die Pfade, die nicht getestet werden, voller Fehler sind. Abbildung 4-2 zeigt einen Teil des Pseudocodes für das Motorsteuergerät in einem Auto. Der Code funktioniert anders, wenn der Motor für die Einhaltung der Abgasvorschriften getestet wird, als wenn der Motor "in der realen Welt" eingesetzt wird.
Wenn du skeptisch bist, dass ein so eklatanter Fall von "zeig dich bei Prüfungen von deiner besten Seite" jemals in der Realität vorkommen könnte, solltest du dir den Abgasskandal von Volkswagen durchlesen, aus dem der Pseudocode in Abbildung 4-2 stammt.1
Eine unidirektionale Abhängigkeit - d.h., dass der Produktionscode in keiner Weise vom Testcode abhängt und sich daher nicht anders verhält, wenn er getestet wird - ist entscheidend, um sicherzustellen, dass sich solche Fehler (ob versehentlich oder böswillig) nicht einschleichen.
Dependency Injection
Dependency Injection ist eine Methode, um die Erstellung eines Objekts von seiner Verwendung zu trennen. Sie erhöht den Zusammenhalt des Codes und reduziert seine Kopplung.2 Dependency Injection erfordert, dass verschiedene Codeeinheiten (Klassen und Methoden) voneinander unabhängig sind. Die Trennung von Test- und Produktionscode ist eine wichtige Voraussetzung, um Dependency Injection zu ermöglichen.
Wir werden in Kapitel 11 mehr über Dependency Injection sagen, wo wir sie nutzen werden, um das Design unseres Codes zu verbessern.
Verpackung und Einsatz
Wenn der Anwendungscode für die Bereitstellung verpackt wird, wird der Testcode fast immer getrennt vom Produktionscode verpackt. So können Produktions- und Testcode unabhängig voneinander eingesetzt werden. Oft wird nur der Produktionscode in bestimmten "höheren" Umgebungen wie der Produktionsumgebung eingesetzt. Dies ist in Abbildung 4-3 dargestellt.
Wir werden die Bereitstellung in Kapitel 13 genauer beschreiben, wenn wir eine kontinuierliche Integrationspipeline für unseren Code aufbauen.
Modularisierung
Das erste, was wir tun , ist, den Testcode vom Produktionscode zu trennen. Dazu müssen wir das Problem des Einbeziehens, Importierens oder Erforderns des Produktionscodes im Testcode lösen. Es ist wichtig, dass es sich dabei immer um eine einseitige Abhängigkeit handelt, wie in Abbildung 4-4 dargestellt.
In der Praxis bedeutet das, dass der Code entlang dieser Linien modularisiert werden sollte:
-
Der Test- und der Produktionscode sollten in getrennten Quelldateien stehen. So können wir den Test- oder Produktionscode unabhängig voneinander lesen, bearbeiten und uns darauf konzentrieren.
-
Der Code sollte Namensräume verwenden, um klar zu kennzeichnen, welche Einheiten zusammengehören. Ein Namensraum kann je nach Sprache als "Modul" oder "Paket" bezeichnet werden.
-
Soweit es möglich ist, sollte es eine explizite Code-Direktive geben -
import
,require
, oder ähnlich, je nach Sprache - um anzuzeigen, dass ein Modul von einem anderen abhängt. Dadurch wird sichergestellt, dass wir die in Abbildung 4-1 gezeigte Abhängigkeit explizit angeben können.
Wir werden auch nach Möglichkeiten suchen, den Code selbstbeschreibend zu machen. Dazu gehört, dass wir Entitäten, Methoden und Variablen umbenennen und neu anordnen, damit sie ihren Zweck besser widerspiegeln.
Redundanz beseitigen
Als Zweites werden wir auf Redundanzen aus unseren Tests entfernen.
Wir haben schon seit einiger Zeit zwei Multiplikationstests: einen für Euro und einen für Dollar. Sie testen die gleiche Funktionalität. Im Gegensatz dazu haben wir nur einen Test für die Division. Sollten wir die beiden Multiplikationstests beibehalten?
Darauf gibt es selten eine eindeutige Antwort: "Ja" oder "Nein". Wir könnten argumentieren, dass die beiden Tests uns davor schützen, die Währung versehentlich in den Code für die Multiplikation zu kodieren - obwohl dieses Argument durch die Tatsache geschwächt würde, dass wir einen Test für die Division haben und ein ähnlicher Fehler bei der Kodierung der Währung dort auftreten könnte.
Um unsere Entscheidungsfindung zu objektivieren, gibt es hier eine Checkliste:
-
Hätten wir die gleiche Codeabdeckung, wenn wir einen Test löschen würden? Die Zeilenabdeckung ist ein Maß für die Anzahl der Codezeilen, die bei der Durchführung eines Tests ausgeführt werden. In unserem Fall gäbe es keinen Verlust an Abdeckung, wenn wir einen der beiden Multiplikationstests löschen würden.
-
Überprüft einer der Tests einen wichtigen Kanten-Fall? Wenn wir zum Beispiel in einem unserer Tests eine wirklich große Zahl multiplizieren und unser Ziel darin besteht, sicherzustellen, dass es auf verschiedenen CPUs und Betriebssystemen nicht zu einem Über- oder Unterlauf kommt, könnten wir beide Tests beibehalten. Das ist aber bei unseren beiden Multiplikationstests nicht der Fall.
-
Bieten die verschiedenen Tests einen einzigartigen Wert als lebendige Dokumentation? Wenn wir zum Beispiel Währungssymbole außerhalb des alphanumerischen Zeichensatzes ($, €, ₩) verwenden würden, könnten wir sagen, dass die Anzeige dieser unterschiedlichen Währungssymbole einen zusätzlichen Wert für die Dokumentation darstellt. Derzeit verwenden wir für unsere Währungen jedoch Buchstaben aus denselben 26 Buchstaben des englischen Alphabets (USD, EUR, KRW), so dass die Unterschiede zwischen den Währungen nur einen geringen Dokumentationswert haben.
Tipp
Zeilen- (oder Anweisungs-) Abdeckung, Verzweigungsabdeckung und Schleifenabdeckung sind drei verschiedene Metriken, die messen, wie viel eines bestimmten Codeteils getestet wurde.
Wo wir sind
In diesem Kapitel haben wir uns mit der Bedeutung der Funktionstrennung und der Beseitigung von Redundanzen beschäftigt. Diesen beiden Zielen werden wir in den folgenden drei Kapiteln unsere Aufmerksamkeit widmen.
Aktualisieren wir unsere Funktionsliste, um das zu berücksichtigen:
5 USD × 2 = 10 USD |
10 EUR × 2 = 20 EUR |
4002 KRW / 4 = 1000,5 KRW |
5 USD + 10 USD = 15 USD |
Trenne den Testcode vom Produktionscode |
Überflüssige Tests entfernen |
5 USD + 10 EUR = 17 USD |
1 USD + 1100 KRW = 2200 KRW |
Unsere Ziele sind klar. Die Schritte, um diese zu erreichen - insbesondere das erste Ziel der Trennung von Belangen - unterscheiden sich erheblich von Sprache zu Sprache. Deshalb haben wir die Umsetzung in die nächsten drei Kapitel aufgeteilt:
Lies diese Kapitel in der Reihenfolge, die für dich am sinnvollsten ist. Im Abschnitt "Wie man dieses Buch liest" findest du eine Anleitung.
1 Zum "Dieselgate"-Skandal von Volkswagen hat Felix Domke eine Menge Arbeit geleistet. Er hat ein Whitepaper mitverfasst. Außerdem hielt er eine Keynote auf der Konferenz des Chaos Computer Clubs.
2 Kohäsion und Kopplung werden in Kapitel 14 genauer beschrieben.
Get Testgetriebene Entwicklung lernen now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.