//

Manches gehört zusammen, manches besser nicht - Konnaszenz in Python

30.11.2022 | 7 Minuten Lesezeit

Wir alle kennen es. Wir bekommen neuen Code und irgendwie macht der merkwürdige Sachen. Teilweise müssen wir Reverse Engineering betreiben. Wir wundern uns, warum eine Umgebungsvariable nicht korrekt gesetzt wird oder der Login schief geht. Bis wir merken, dass bei einer Funktion zwei Variablen gleichen Types vertauscht wurden und wir aus irgendeinem Grund erst den einen und dann den anderen Service manuell starten müssen. Willkommen in der Welt der Konnaszenz.

Konnaszenz könnte auch aus der Welt der Quantenphysik entstammen und dort verschränkte Teilchen benennen. Konnaszenz beschreibt, dass zwei oder mehr Elemente miteinander verbunden sind. Wird ein Element geändert, müssen auch die anderen entsprechend angepasst werden. Das häufigste Beispiel sind Codezeilen, bei denen wir sofort alle an DRY denken – don't repeat yourself. Wird eine dieser Codezeilen geändert, müssen häufig auch die Doppelungen angepasst werden. Konnaszenz selbst bedeutet die gemeinsame Geburt oder das Wachstum zweier Dinge. Der Begriff wurde in der Software-Entwicklung erstmals 1992 von Meilir Page-Jones diskutiert, als sprach-agnostisches Qualitätsmaß zum Erhalt der Gesamtkorrektheit. Im Prinzip ist sie ein Indikator, wo Refactoring hohe Gewinne erzielt.

Kurz gesagt wollen wir, dass die Konnaszenz niedrig ist. Wenn etwas konnaszent ist, dann soll es nah beieinander liegen und möglichst schwache Verzahnungen zeigen. Konnaszenz hat drei wichtige Merkmale: Stärke, Lokalität und Grad der Erscheinung. Die Stärke beschreibt, wie wahrscheinlich eine Veränderung an einer anderen Komponente ist, wenn ich etwas am Code ändere. Lokalität beschreibt, ob Komponenten nah beieinander oder weit entfernt, bspw. getrennt durch APIs, sind. Grad der Erscheinung versucht zu fassen, ob die Konnaszenz akzeptabel ist. Was das im Detail bedeutet, beschreibe ich weiter unten.

Es gibt zwei Gruppen von Konnaszenz: statisch und dynamisch.

Statische Konnaszenz

Statische Konnaszenz lässt sich im Code erkennen, d.h. sie lässt sich gut durch die IDE und entsprechende Plugins automatisch feststellen.

Die einzigen beiden guten Formen von Konnaszenz sind die des Namens und des Types. Diese lassen sich am folgenden Python-Programm verstehen:

1import os  
2from pathlib import Path  
3  
4  
5def read_data(directory, filename):  
6    return open(os.path.join(directory, filename), "r")  
7  
8  
9def raw_data(directory: Path):  
10    for filename in os.listdir(directory):  
11        log_data = read_data(directory, filename)  
12        print(log_data)

Die Funktion raw_data erfordert die Funktion read_data. Wenn read_data umbenannt wird, dann funktioniert raw_data nicht mehr, das ist Konnaszenz des Namens. Außerdem haben sich die beiden Funktionen darauf geeinigt, dass directory ein Path ist, Konnaszenz des Types. Würde ich stattdessen einen Integer übergeben, käme es zum Fehler. Mit modernen Refactoring-Tools sind solche Umbenennungen und Typenprüfungen kein Problem. In Python bemerkt die IDE anhand von Type Hints, ob die Variablen zueinander passen. Bereits in diesem Beispiel zeigt sich aber, dass diese beiden Formen der Konnaszenz einfach sinnvoll sind – hier passt zusammen, was zusammen gehört.

Schwieriger wird es bei einer der nächsten Formen der Konnaszenz: Position. Die Funktion read_data in unserem Beispiel erfordert erst directory und dann filename. Sie kann aber nicht prüfen, ob die Eingabe korrekt ist. Entweder ich typisiere die Eingabe oder ich fordere keyword-only Arguments, wie in dieser Funktionssignatur gezeigt:

1def read_data(*, directory: Path, filename:str):

Alle Argumente nach dem * müssen mit dem passenden keyword übergeben werden. Nun meckert auch meine IDE herum, wenn ich directory und filename aus Versehen vertausche. Würde read_data mehr Parameter, z. B. acht, benötigen, dann wäre der Grad der Konnaszenz schlecht und ich sollte dringend refactoren. Solche Haufen von Parametern werden auch als Data Clumping bezeichnet. Gleichzeitig ist die Lokalität ist hoch, was in diesem Fall sehr gut ist.

Die beiden anderen statischen Typen von Konnaszenz sind Konnaszenz der Bedeutung und des Algorithmus. Wenn wir zwischen einer API Daten austauschen, kommt es häufig zu Konnaszenz der Bedeutung. Das ist in diesem Payload JSON sichtbar:

1{    
2  "username": "Gisela",
3  "type": 3  
4}

Was bedeutet type = 3? Im Allgemeinen wird sowas an mehreren Stellen in verschiedenen Programmen genutzt um eine bestimmte Einstellung zu wählen. Damit ist die Lokalität niedrig und der Grad schlecht. Wenn in irgendeinem Teil des Programms type anders interpretiert wird, dann muss es überall angepasst werden. Hier greift das Zen von Python Explicit is better than implicit. Die Lösung könnte vielfältig sein. Ein anderer API-Endpunkt, statt 3 eher „Person" übergeben um ubiquitäre Sprache umzusetzen, oder, oder, oder. Das muss im Einzelfall unterschieden werden. Es ist aber bereits für mich in meiner täglichen Arbeit hilfreich meinem PO sagen zu können, dass unsere API Probleme erzeugen wird, da wir Konnaszenz der Bedeutung vorliegen haben.

Konnaszenz des Algorithmus beschreibt, dass zwei Objekte Daten auf gleiche Art und Weise verarbeiten müssen. read_data stellt Rohdaten zur Verfügung, welche weiter verarbeitet werden. Diese Weiterverarbeitung muss überall gleich passieren, wo die Daten verwendet werden. Idealerweise ist die Lokalität hoch.

Dynamische Konnaszenz

Dynamische Konnaszenz lässt sich sicher erst während der Runtime bestimmen. Teilweise lassen sich Indikatoren im Code ablesen, aber Sicherheit gibt es erst im Betrieb. Diese dynamischen Formen der Konnaszenz sind die üblichen Überraschungen, die wir alle beim Live-Gang oder Debugging von Programmcode erleben. Diese Form der Konnaszenz lassen sich deutlich schwieriger erkennen und sind meist sehr viel komplexer.

Ich möchte mich auf zwei beschränken. Zunächst Konnaszenz of Execution Order, also der Ausführungsreihenfolge. Schauen wir uns einmal das folgende Pattern an, welches aus einigen typischen sequentiellen Data-Science-Schritten besteht:

1data = build()  
2data = load(filename)  
3data = make_regression(data)  
4data = make_model(data)  
5store_model(data)

Hier wird ein Datenmodell gebaut, mit bestehenden Daten beladen, ein paar Transformationen und Berechnungen durchgeführt, bevor alles abgespeichert wird. Muss ich nun zuerst build und dann load ausführen oder geht das auch anders herum? Was passiert wenn ich erst make_regression und dann make_model durchführe? Es ist nicht per se schlecht, dass es diese Reihenfolge gibt, sondern dass ich sie vertauschen kann. In Java und anderen Sprachen kann dieses Problem gelöst werden, indem die Funktionen das Interface der Folgefunktion zurück geben. In Python geht das nur sehr umständlich, über eine vorgehaltene Historie, extra Variablen oder vielleicht über Decorator. Wenn ihr eine gute Idee habt, schreibt es mir gerne in die Kommentare.

Eine sehr viel spannendere Form der Konnaszenz ist die der manuellen Aufgaben. Wir kennen sie alle. Ich deploye meinen Code per Pipeline auf ein Gerät und „muss noch schnell die env-Variable in Prod anpassen". Ich selbst weiß das, aber ist das auch dokumentiert bzw. offensichtlich? Weiß ich es auch noch, wenn ich ein halbes Jahr in einem anderen Projekt bin? Im schlimmsten Fall muss ich sogar an der Hardware selbst noch etwas machen, bevor der Schritt erfolgreich war. In einem IoT-Projekt ist es nicht ungewöhnlich, dass bei einem Neustart auch ein Gerät neu booten muss. Außerdem sind nicht alle Projekte und Programme so automatisiert in Produktion, dass nicht noch händisch die Datenbank migriert und angepasst oder ein Server konfiguriert werden muss. All diese Schritte sind konnaszent. Spätestens seit „The Goal" und „The Phoenix Project" wissen wir, dass solche Abhängigkeiten eine grauenvolle Spirale aufbauen können, die den Erfolg des Unternehmens gefährdet.

Die anderen Formen der Konnaszenz sind Konnaszenz des Timings und der Identität. Timing passiert bspw., wenn wir einen Test oder eine API ausführen, die eine gewisse Zeit warten muss. Wie lange müssen wir nun genau warten, bis Erfolg sicher ist? Hier geht es um Wahrscheinlichkeiten und Fluktuationen können immer Auftreten. Zumindest im Testing lässt sich das durch ein Mocking der Zeit auflösen. Event-basiert zu arbeiten ist meist auch eine gute Lösung dieser Konnaszenz.

Konnaszenz der Identität beschreibt das Singleton-(Anti)Pattern oder globale Variablen. Wir kennen es alle: irgendwo ändert sich was und das ist vollkommen überraschend.

Konnaszenz des Wertes zeigt auf, wenn sich Werte in Produktions- und Testcode gemeinsam ändern müssen. Bspw. könnte sich mit der Zeit die Standardeinstellung einer Variable von test zu prod ändern. Teste ich diese Variable, dann muss auch die Standardeinstellung dort anders geprüft werden.

Eine aufsteigende Reihenfolge der verschiedenen Arten und ihrer Schwere wäre diese:

  1. Name
  2. Typ
  3. Bedeutung
  4. Algorithmus
  5. Position
  6. Execution Order (Ausführungsreihenfolge)
  7. Timing
  8. Wert
  9. Identität
  10. Manuelle Aufgaben

Wie geht es weiter?

Nun könnten wir uns fragen, warum Konnaszenz nicht häufiger diskutiert wird. Sie ist ein sprach-agnostisches Tool, um ubiquitäre Sprache für Codequalität und zukünftige Stolpersteine zu ermöglichen. Zumindest auf unserem codecentric Blog wurde Konnaszenz bisher nur einmal 2020 genannt. Ein Vortrag der KiwiPycon von 2015 zeigt, warum bspw. Konnaszenz im Atomwaffenbereich schlecht ist. Konnaszenz ist nicht die Lösung aller Probleme, sondern ein Werkzeug im Berater:innenkoffer. Ähnlich zu meinen Erfahrungen aus funktionaler Programmierung, hilft es mir Schmerzen zu erleben, wenn ich bestimmte Antipatterns sehe. In Code Reviews kann ich besser benennen, warum mich ein Singleton (Konnaszenz der Identität) stört oder warum das Builder Pattern nicht optimal ist (Konnaszenz of Execution Order).

Dieser Blogpost entstand nach meiner Teilnahme an der KanDDDinsky in Berlin. Ich danke Marco Consolaro und Alessandro Di Gioia für die Impulse und den Austausch.

Beitrag teilen

Gefällt mir

4

//

Weitere Artikel in diesem Themenbereich

Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.

//

Gemeinsam bessere Projekte umsetzen

Wir helfen Deinem Unternehmen

Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.

Hilf uns, noch besser zu werden.

Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.