zur Übersicht
4. Jan. 2025

Code und Kognition

Vor einigen Jahren hatte ich die Idee, dass das Verständnis von Code von unserem Gehirn abhängen muss. Ich hatte von George Millers “Magischer Sieben” gehört und versuchte, sie anzuwenden. Eine Freundin wies mich darauf hin, dass sie nie von der 7 gehört hatte, sondern von der 4. Anfangs interessierte mich nur, was davon stimmte, aber schließlich dauerte die Nachforschung enige Jahre und das Modell wurde immer ausgefeilter. Was war das Ergebnis?


Zunächst einmal - wir sprechen oft vom “Gehirn”, aber dies ist streng genommen nur die Bezeichnung für das Organ in unserem Kopf. Biologen sprechen vom Gehirn, Psychologen sprechen vom Gedächtnis. Das Gedächtnis fasst alle Prozesse zusammen, die am Erkennen, Speichern, Transformieren und Ausdrücken von Informationen beteiligt sind. Die Psychologie betrachtet verschieden Zuständigkeiten des Gedächtnisses; wir werden uns auf das Langzeitgedächtnis und das Arbeitsgedächtnis konzentrieren, denn deren Fähigkeiten und Beschränkungen beeinflussen direkt unser Verständnis.

Glücklicherweise klassifiziert eine Betrachtung des Gedächtnisses Menschen nicht in “verstehend” und “nicht verstehend”, sondern gibt uns wertvolle Einblicke, wie Informationen so präsentiert werden können, dass viele Menschen sie verstehen können.

Langzeitgedächtnis

Die Aufgabe des Langzeitgedächtnisses ist das Speichern von Informationen. Die gespeicherten Einheiten werden Chunks genannt. Jedes Konzept, das wir kennen (d.h. von dem wir eine Vorstellung haben), wird durch einen Chunk repräsentiert. Da ein Konzept in der realen Welt keine, eine schwache oder eine starke Vorstellung haben kann, können Chunks fehlen, oberflächlich oder detailliert sein. Beispiele für Chunks:

  • Ein Pixel wird fast immer als Chunk erkannt. Aber blinde Menschen haben keine Vorstellung von einem Punkt oder zumindest eine andere
  • Ein Buchstabe A besteht aus Pixeln. Alle Menschen, die lesen können, speichern ihn in einem ähnlichen Chunk. Aber Kulturen mit nicht-lateinischen Schriftzeichen haben einen anderen Chunk, und Menschen, die keine lateinischen Buchstaben lesen können, haben diesen Chunk überhaupt nicht
  • Das Wort Observer ist ein Chunk für die meisten englischsprachigen Menschen. Aber nicht für Sprecher anderer Sprachen
  • Das Konzept des Entwurfsmusters Observer ist ein Chunk für viele Softwareentwickler. Andere Berufsgruppen verbinden nicht die gleichen Aspekte mit diesem Wort

Das Langzeitgedächtnis kann uns also nur dann beim Verstehen helfen, wenn jede gelesene Information mit dem richtigen Chunk verbunden wird - und die Details dieses Chunks müssen ausreichend sein.

Arbeitsgedächtnis

Die Aufgabe des Arbeitsgedächtnisses ist das Kombinieren und Transformieren von Informationen. Nach den Arbeiten von Nelson Cowan kann das Arbeitsgedächtnis 4 Chunks gleichzeitig kombinieren. Aber warum sollten wir überhaupt Chunks kombinieren?

Wenn nur das Speichern und Abrufen von Informationen nötig wäre, würde das Langzeitgedächtnis wahrscheinlich ausreichen. Das würde aber bedeuten, dass unsere Kognition immer auf Konzepte beschränkt wäre, die wir bereits kennen. Das Arbeitsgedächtnis erweitert unser Wissen auf Szenarien, die für uns neu oder einfach zu speziell zum Speichern sind.

Die meisten von uns kennen das Ergebnis von 1+1, und die meisten werden nicht den Chunk Addition auf 1 und 1 anwenden, sondern direkt den Chunk 1+1 aktivieren. Aber wir alle wissen, dass Addition auf unendlich viele Zahlenkombinationen angewendet werden kann. Das Langzeitgedächtnis ist zwar sehr groß, aber nicht unendlich, daher ist das Addieren beliebiger Zahlen eine typische Aufgabe für das Arbeitsgedächtnis.

Betrachten wir nun Code. Wir wissen, dass es eine unendliche Anzahl von Turing-Maschinen gibt. Es ist also auch nicht möglich, jede mögliche Codezeile im Langzeitgedächtnis zu speichern. Stattdessen verarbeiten wir eine Codezeile in unserem Arbeitsgedächtnis. Die Grenze von 4 Chunks bedeutet, dass wir Codezeilen nicht gut verstehen können, die mehr als 4 Chunks enthalten.

An diesem Punkt wird es etwas knifflig. Wenn wir kein gemeinsames Verständnis (gleiche Chunks) haben können, können wir auch nicht wirklich beurteilen, ob eine Codezeile leicht verständlich ist oder nicht. Eine Codezeile mit 80 Wörtern kann für jemanden perfekt verständlich sein, der einen Chunk für genau diese 80 Wörter hat. Der durchschnittliche Leser wird jedoch zu viele Chunks erkennen, um sie in einem Schritt zu verarbeiten, und wird verwirrt sein.

Das Arbeitsgedächtnis begrenzt also unser Verständnis, weil wir beliebig komplexe Operationen nicht in einem Schritt auswerten können. Code muss so geschrieben werden, dass jeder logische Schritt höchstens 4 Chunks verwendet.

Zusammenfassung

Wir wissen jetzt, dass:

  • das Arbeitsgedächtnis uns darauf beschränkt, höchstens 4 Informations-Chunks gleichzeitig zu verarbeiten
  • das Langzeitgedächtnis unsere Chunks auf alle Konzepte beschränkt, die wir in der Vergangenheit gelernt haben

Folglich muss verständlicher Code:

  • höchstens 4 Informations-Chunks in einem Ausdruck/einer Anweisung/einer Struktur kombinieren
  • nur Chunks verwenden, die den beabsichtigten Lesern zur Verfügung stehen

Wenn wir dies tun, können wir die meisten Clean-Code-Regeln ableiten:

  • die Regel, nur wenige Attribute in Klassen zu verwenden (das Arbeitsgedächtnis kann nur 4 in einer Abstraktion erfassen)
  • die Regel, höchstens 2 Argumente in Methoden zu verwenden (das Arbeitsgedächtnis kann 4 Chunks kombinieren: this, die Methode, beide Argumente)
  • die Regel, gute Namen zu verwenden (weil schlechte Namen den falschen Chunks zugeordnet werden)
  • die Regel selbsterklärender Methodennamen (wenn wir Methodennamen nicht selbsterklärend schreiben können, werden sicherlich mehr als 4 Chunks benötigt, in diesem Fall ist auch die Dokumentation nicht verständlich)

Angewandt

Die folgenden Beispiele beschreiben alle das gleiche Programm, unterscheiden sich aber in Stil und Detailgrad:

var result = doIt(3,2,4,2,2);

Dies ist ein Beispiel für Chunks, die in nicht ausreichend information enthalten. Der Variablenname result gibt keinen Hinweis auf die durchgeführte Operation, während der Funktionsname doIt ebenso nichtssagend ist. Auch die Zahlenfolge gibt keinen Hinweis auf die Semantik der Zeile.

Das Hauptproblem liegt hier im Fehlen von bekannten (mächtigen) Abstraktionen. Das Langzeitgedächtnis kann aufgrund der abstrakten und vagen Natur des Codes keine Assoziationen bilden. (Um fair zu sein, liegt die Schuld hier beim Autor.)

var result = powerSumAndRoot(3,2,4,2,2)

In dieser Version hat der Autor versucht, den Code verständlich zu machen. Der Funktionsname powerSumAndRoot deutet auf eine Operation mit Potenzen, Summen und Wurzeln hin. Die Beziehung zwischen den Wörtern und wie die numerischen Parameter mit den Operationen zusammenhängen, bleibt jedoch unklar.

Hier verlagert sich das Problem auf das Arbeitsgedächtnis. Der Leser muss 8 Chunks (Operationen und Zahlen) kombinieren, was die Grenze des Arbeitsgedächtnisses von 4 Chunks bei weitem übersteigt.

var exp = 3;
exp *= 3;
exp += 4 * 4;
exp = pow(exp, 1 / 2);
		
var result = exp;

Dieses Beispiel scheint die Logik des früheren Funktionsaufrufs inline darzustellen. Obwohl es den Prozess in kleinere Schritte unterteilt, ist es nicht einfach nachvollziehbar. Die Chunks *, + und pow sind uns bekannt, und jede Zeile ist für sich genommen verständlich. Die imperative Natur des Codes stellt uns jedoch vor Herausforderungen:

  • Jede Zeile weist exp einen neuen Wert zu.
  • Jede Zuweisung überschreibt den vorherigen Wert von exp.
  • Um result zu verstehen, muss der Leser alle Zeilen mental simulieren.

Dieser Ansatz belastet sowohl das Arbeitsgedächtnis (mit mehreren Operationen) als auch das Langzeitgedächtnis (mit häufigen Schreib- und Löschvorgängen). Wie auch bei neuronalen Netzen ist das Lernen neuer Informationen vergleichsweise günstig, das Verlernen hingegen sehr aufwändig. Die ständigen Neuzuweisungen fügen also unnötige kognitive Last hinzu. Diese Version ist nicht völlig unlesbar, aber bleibt dennoch recht anspruchsvoll.

var aa = 3 * 3;
var bb = 4 * 4;
var sum = aa + bb;
var result = sqrt(sum);

Dieser deklarative Ansatz verbessert das vorherige Beispiel. Die Chunks *, + und sqrt (Quadratwurzel) sind bekannt, und es gibt nur “Deklarationen”, keine Zuweisungen. Jede Zeile repräsentiert eine Relation. Und die Kombination aller jede Relationen führt zu result.

Außerdem kann der Code rückwärts gelesen werden (vom Ergebnis zu seinen Quellen), was Lesern erlaubt, aufzuhören, wenn sie genug verstanden haben. Wer die letzten beiden Zeilen kennt, kann schon antizipieren, was davor kommen könnte.

Dieses Beispiel belastet Arbeits- und Langzeitgedächtnis in angemessener Weise. Jede Zeile kombiniert weniger als 4 Chunks, und diese Chunks sind allgemein verständlich, d.h. in hinreichender Tiefe im Langzeitgedächtnis vorhanden.

var vec = new Vec(3,4);
var result = vec.norm();

Dieses Beispiel ist etwas polarisierend. Einige Leser mögen es als am besten lesbar empfinden, während andere es nur als leichte Verbesserung gegenüber den früheren Beispielen sehen. Warum?

Entwickler mit einem Hintergrund in Algebra werden die Begriffe Vec (Vektor) und norm (Vektorbetrag) erkennen. Für sie ist dieser Code sehr gut lesbar. Andere ohne diesen Hintergrund müssen die Dokumentation konsultieren, erkennen aber zumindest die Wissenslücke.

In diesem Fall ist der im Langzeitgedächtnis gespeicherte Chunk domänenspezifisch: Er ist einer Gruppe von Entwicklern vertraut, einer anderen nicht. Abgesehen davon ist das Arbeitsgedächtnis aber nicht belastet, da jede Zeile weniger als 4 Chunks enthält.

Weitere Beispiele

Für Beispiele und weitere Details verweise ich auf meinen Artikel auf entwickler.de oder besuchen Sie einen meiner Vorträge zu diesem Thema.