Montag, 26. Januar 2026

Geht's noch etwas minimalistischer, bitte?

Vor kurzem unterzog ich meine minimalistische JavaScript-Bibliothek minlib.js (2011) einer Revision, da sich seit ihrem Erscheinen viel verändert hat. Nach massiven Kürzungen und Vereinfachungen entstand schließlich ein ES6-Modul minlib.mjs, das ich hier beschreiben will.

Hatte das minlib.js von 2011 noch 10.8 KB, so ist das Modul minlib.mjs auf nur noch 5.1 KB Größe geschrumpft (natürlich unkomprimiert).

Und strenggenommen ist das ganze Modul heute überflüssig. Im besten Fall hilft es durch die Abkürzungen und Array-typisierten Rückgabewerte, JavaScript-Code noch etwas lesbarer zu machen.

Die Verfallszeit von Frameworks

Die Programmiersprache JavaScript ist im Umfang gewachsen und wächst ständig weiter, so dass die Sprache immer flüssiger lesbar wird und unnötigen Code (syntaktisches Rauschen) zusehends vermeidet. Der Sprachkern von JavaScript - also ohne die DOM-API, die für den Zugriff auf die HTML-Elemente einer Webseite zuständig ist - basiert auf dem Standard ECMAScript, zu dem es mittlerweile im Jahrestakt neue Versionen gibt. Aber auch die DOM-API-Funktionen werden ständig verbessert.

Wir blicken zurück auf gottlob längst vergangene Zeiten, als Sprachkern und DOM-API noch sehr unreif waren und auch nicht in allen Browsern einheitlich verwendbar. Der Internet Explorer war stets das schwarze Schaf, das sich nicht um Regeln und Standards kümmerte - aber auch unter den anderen Browsern gab es störende Abweichungen. In diesen Zeiten begann der Einsatz separater JavaScript-Frameworks, die die im Standard noch fehlenden Features anboten. Die Frameworks garantierten auch den zuverlässigen Einsatz in unterschiedlichen Browsern (Browserunabhängigkeit).

Es war daher klar, dass all diese Frameworks als Lückenfüller nur eine begrenzte Lebensdauer haben würden: sie haben nur solange eine Existenzberechtigung,

  • wie es gravierende Differenzen der JavaScript-Engines in den verschiedenen Browsern gibt,
  • und wie es der bestehenden JavaScript-Syntax und der DOM-API an denjenigen praktischen und vereinfachenden Funktionen mangelt, für die die Framework-Hersteller ihre Workarounds anbieten.

In Webanwendungen war zunächst das Framework jQuery der Platzhirsch, der sich gegen Konkurrenten wie Prototype.js durchsetzte. Später liefen ihm Frameworks wie Ember.js, AngularJS, Vue.js und React den Rang ab. Diese Frameworks arbeiten mit Erweiterungen wie virtuellem DOM und Datenbindung (der Blog RisingStack gibt eine Einführung in Datenbindung und zeigt gleichzeitig, wie man sie heute mittels Definition von Settern und Gettern für Objekteigenschaften erreichen kann - mit purem JavaScript, ohne Frameworks zu benötigen).

All diese Frameworks generieren einen zusätzlichen Overhead. Viele erfinden sogar neue Zwischensprachen und Layer mit eigenen UI-Elementen (fraglos macht es Entwicklern Spaß, sich solche Layer auszudenken und Interpreter dafür zu schreiben), für deren Interpretation es dann wieder eigene Frameworks wie Handlebars.js gibt.

Aber jedes Framework erzeugt eine zusätzliche Software-Abhängigkeit. Was wir beim Rückblick auf die letzten drei Jahrzehnte wissen, ist: allein die Triade HTML, CSS und JavaScript hat im Web die Zeiten überdauert und wird sie weiter überdauern. Durch die fortgesetzte Optimierung dieser Triade auf Browserseite werden die meisten Frameworks mit der Zeit obsolet. Es ist daher empfehlenswert, mit dem zukunftsweisenden Framework Vanilla.js zu arbeiten - es ist mit seinen 0 Bytes Downloadgröße überdies extrem leichtgewichtig. 😀

Modularisierung

Ein Problem von "frühen" JavaScripts ist, dass man sehr unbefangen mit dem globalen Namensraum umging. Ein in einer Webseite ausgeführtes JavaScript-File besteht in der Regel aus einer Reihe von Funktionen, die allesamt ab dem Zeitpunkt ihrer Deklaration global bekannt und verfügbar sind.

Um diese Verschmutzung des globalen Namensraums zu verhindern, bot die Sprache schon immer die Möglichkeit innerer Funktionen an: Funktionen, die nur als Hilfsfunktionen von anderen Funktionen gebraucht werden, können im Rumpf der verwendenden Funktion deklariert werden und sind dann - wie auch dort deklarierte Variablen - nur innerhalb dieser verwendenden Funktion bekannt.

Mit diesem Feature der inneren Funktionen konnte man in JavaScript ein Modulkonzept einführen:

  • Es gibt eine Hauptfunktion (meist anonym), die unmittelbar ausgeführt wird (sogenannte IIFE = immediately invoked function expression);
  • Rückgabewert dieser Funktion ist ein Objekt, das Referenzen auf alle Funktionen enthält, die außerhalb des Moduls bekannt sein sollen;
  • Dieses eine Objekt kann nun einer globalen Variablen zugewiesen werden, die die Sammlung aller Bibliotheksfunktionen enthält.
In der Praxis kamen dann "Module" von folgender Art vor, die wenigstens nur noch eine globale Variable brauchen (die Variable Library in diesem Beispiel - ein Objekt, dessen Eigenschaften die API-Methoden und -Attribute sind).
// "Modul" vor ES6, bietet API-Komponenten api1, api2, apivar an
Library = (function(){
  var x,y,z;  // Interne Hilfsvariablen, sichtbar nur hier und in den inneren Funktionen
  var apivar; // Durch Aufnahme ins return-Objekt kann man veröffentlichen
  return {
    "api1":api1,
    "api2":api2,
    "apivar":apivar
  };
  function api1() {
  }
  function api2() {
  }
  // Hilfsfunktionen, verwendet in den API-Funktionsimplementierungen
  function aux1() {  
  }
  function aux2() {  
  }
  function aux3() {  
  }
})();
Alternativ könnte man auch eine Generatorfunktion veröffentlichen und diese dann in den verwendenden Scripten aufrufen:
// Modul, bietet API-Funktionen api1, api2 an
function getLibrary(){
  ...
  return {
    "api1":api1,
    ...
  };
  ...
}
Aber dennoch ist klar, dass das alles nur Notbehelfe waren. Es war eine Änderung des JavaScript-Sprachkerns notwendig, um echte Module schreiben zu können. Das passierte dann mit ES6 (= ES2015). Seitdem gibt es export- und import-Anweisungen in JavaScript.

Files, die eine export-Anweisung verwenden, werden als Modul betrachtet, und alle im File deklarierten Variablen und Funktionen haben die neue Sichtbarkeit "File", wenn sie nicht in der export-Anweisung aufgeführt sind.

// ES6-Modul bietet API-Komponenten api1, api2, apivar an
export {
  api1,
  api2,
  apivar
};

var apivar; // Öffentliche Variable, da im export aufgeführt
var x,y,z;  // Interne Hilfsvariablen, sichtbar nur im Modul
  
function api1() {
}
function api2() {
}

// Hilfsfunktionen, nur in diesem Modul sichtbar
function aux1() {  
}
...

Ein Konsument der Bibliothek kann dann mit der import-Anweisung diejenigen Funktionen oder Variablen in seinen eigenen Kontext importieren, die er benötigt. Dabei kann er die importierten Objekte noch mit dem Schlüsselwort as umbenennen, falls er mit den Namen nicht zufrieden ist, die der Bibliotheksentwickler für seine Objekte gewählt hat.

import {
  api1 as f,
  apivar
} from "./minlib.mjs";

Der erste Umbau von minlib.js war daher, die Bibliothek in ein ES6-Modul umzuwandeln - und um klarer hervortreten zu lassen, dass es sich um ein Modul handelt, änderte ich das Suffix auf .mjs. Die exportierten Funktionen von minlib.mjs sind:

export {
  deepCopy,
  byName,
  select,
  selectAll, 
  byClass,
  byCondition,
  byId,
  byTagName,
  selectParent,
  getParentByCondition,
  getElement,
  setText,
  getText,
  hasClass,
  setClass,
  toggleClass,
  resetClass,
  navigateTo
};
Auf diese Funktionen werde ich nun noch im einzelnen eingehen.

Tiefe Kopie eines komplexen Datenobjekts

Die Funktion deepCopy() ist die einzige grundlegende Basisfunktion der Bibliothek, die nicht auf das HTML-DOM bezogen ist. Sie dient dazu, ein vorgelegtes komplexes Datenobjekt, bestehend aus Arrays, Objekten und einfachen Datenobjekten, vollständig zu kopieren.

Die Idee ist, es einmal in einen JSON-String zu serialisieren und danach wieder zu deserialisieren. Dieser Weg ist wesentlich effizienter als alle Implementierungen, die mit JavaScript-Code den gesamten Datenbaum durchgehen und dabei kopieren (bei der JSON-Serialisierung wird zwar ebenfalls der Datenbaum durchlaufen, aber dies geschieht intern, im nativen Code des JavaScript-Interpreters und ist damit wesentlich effizienter als expliziter JavaScript-Code jemals sein kann).

// --- Vollständige Kopie eines Datenobjekts
function deepCopy(data) {
  return JSON.parse(JSON.stringify(data));
}

Nachteil ist allenfalls, dass man die Funktion nicht auf eine bestimmte Tiefe beschränken kann - man muss immer alles kopieren. Dennoch ist die Funktion so einfach, elegant und nützlich, dass sie mir für minlib.mjs geeignet erschien.

Abkürzungen

Einige der minlib-Funktionen sind einfach Abkürzungen. Nehmen wir die Funktion byId(), die als Abkürzung für document.getElementById() fungiert. Auch umfangreiche JavaScript-Frameworks wie Prototype benutzen eine solche Abkürzung, in Prototype heißt die Funktion $() - um der größtmöglichen Kürze willen.

In der alten Version minlib.js war byId() wie folgt definiert:

function byId(id) {
  return document.getElementById(id);
}
Beim Aufruf entsteht also eine weitere Stackebene, nur um die Umleitung auf document.getElementById() auszuführen. Das kann man sich sparen, wenn man die Funktion als echten Alias definiert:
const byId = document.getElementById;  // ...aber Vorsicht! 
So einfach funktioniert es allerdings noch nicht! Wenn ich in der Konsole die so definierte Funktion auf eine wirklich existierende ID anwende:
byId("div1")
erscheint die Fehlermeldung
Uncaught TypeError: Illegal invocation
    at <anonymous>:1:1
Grund ist, dass getElementById nicht einfach eine Funktion, sondern eine Objektmethode ist. Sie erfordert ein Bezugsobjekt document als this-Objekt, um ordnungsgemäß zu funktionieren. Tatsächlich können im Ausführungskontext einer Webseite mehrere HTML-Dokumente als Bezugsobjekte vorhanden sein - etwa wenn mit Frames oder iFrames gearbeitet wird.

Nun bietet das eingebaute Function-Objekt von JavaScript eine Methode bind(o), die eine modifizierte Funktionsreferenz zurückliefert; die Modifikation besteht darin, dass bei Funktionsaufrufen das this-Objekt an das bei bind übergebene Objekt o gebunden ist.

Eine solche modifizierte Funktion benötigen wir auch hier. Wenn wir die Funktion explizit an das aktuelle document-Objekt binden,

const byId = document.getElementById.bind(document);
dann funktioniert auch der Aufruf wie erwartet - und liefert das DOM-Element zur angegebenen ID zurück, oder null, falls kein solches existiert.

Dank der Modularisierung ist kein Verwender der Bibliothek gezwungen, den vom Modul-Autor gewählten Namen zu verwenden. Wenn jemand lieber $ statt byId verwenden will, gibt er den gewünschten Namen eben beim Import an:

import { byId as $ } from "./minlib.mjs";

Die CSS-Klassen

Schon in der alten minlib.js gab es Funktionen hasClass(), setClass() und resetClass(), die alle auf dem Attribut className von DOM-Elementen operierten. Dieses Attribut enthält alle aktiven CSS-Klassen des Elements, in Form einer durch Leerzeichen getrennten Liste. Um eine einzelne Klasse ausfindig zu machen, wurde ein regulärer Ausdruck verwendet, der mit der Wortgrenzen-Zusicherung \b arbeitete:
function hasClass(elem,theClass) {
  return !! elem.className.match(  new RegExp("\\b"+theClass+"\\b") );
}
Mittlerweile gibt es die Schnittstelle DOMTokenList, die eine ganze Reihe sinnvoller Operationen auf einer Menge von Tokens erlaubt (hier also verwendet für die Liste der für dieses Element aktuell aktiven CSS-Klassen).

Andererseits ist wegen der doppelten Indirektion der Zugriff über das separate Attribut classList immer etwas unklar. Wenn er schon nicht vermeidbar ist, könnte man ihn einmal in einer Funktion vergraben - und dann immer mit dieser Funktion arbeiten. Dies wäre daher die Reimplementierung von hasClass():

function hasClass(elem,theClass) {
  return elem.classList.contains(theClass);
}
Analog sind die folgenden drei Funktionen definiert:
function setClass(elem,theClass) {
  elem.classList.add(theClass); 
}

function resetClass(elem,theClass) {
  elem.classList.remove(theClass);
}

function toggleClass(elem,theClass,force) {
  return elem.classList.toggle(theClass,force);
}

Das DOM mit CSS-Selektoren durchsuchen

Eine der praktischsten Funktionen, um die das DOM API seit 2009 in den Browsern erweitert wurde, ist die Möglichkeit, die HTML-Seite mit einem CSS-Selektor zu durchsuchen. Das Modul, das in CSS für den Stil von Webseiten zuständig ist, ist nun also auch im JavaScript zugänglich. Hierzu gibt es die Funktionen element.querySelector() und element.querySelectorAll(). Beide Funktionen durchsuchen das vom Bezugs-element aufgespannte HTML-Fragment auf Elemente, die dem CSS-Selektor genügen. Während die erste, element.querySelector(), beim ersten Treffer mit der Suche aufhört und das gefundene Element zurückliefert, bringt die zweite Funktion, element.querySelectorAll(), eine vollständige Liste aller Elemente, die die Bedingung erfüllen.

Hier gibt es nur zwei Unschönheiten:

  1. Die Namen der Funktionen sind zu lang. Das kennen wir schon von getElementById().
  2. Der Rückgabetyp von querySelectorAll() ist kein Array, sondern eine NodeList. Um Array-Methoden wie filter(), map() oder reduce() auf das Ergebnis anzuwenden, muss man dieses zuerst in einen Array konvertieren.
Die minlib.mjs bietet ersatzweise zwei Funktionen select(selector,context) und selectAll(selector,context) an, die einen kurzen, passenden Namen haben und sich flüssig lesen. Die Funktion selectAll() konvertiert das Ergebnis auch gleich noch in einen Array:
// --- Shortcut für querySelectorAll
function selectAll(selector,context=document) {
  return [...context.querySelectorAll(selector)];
}

// --- Shortcut für querySelector
function select(selector,context=document) {
  return context.querySelector(selector);
}
Eine weitere Funktion selectParent(context,selector) sucht in der Kette der Vorfahren das erste Element, auf das der CSS-Selektor selector passt - und gibt dieses zurück. Dies ist sehr ähnlich der Standardmethode Element.closest(selector). Der einzige Unterschied besteht darin, dass selectParent mit dem direkten Vorfahren des Bezugselements zu suchen beginnt, während Element.closest auch schon das Bezugselement selbst prüft.

Die Funktion byTagName(tagName,context=document) gehört ebenfalls in diese Familie. Einerseits ist sie eine Abkürzung der Standardmethode getElementsByTagName(tagName). Darüberhinaus liefert sie aber auch einen Array der gefundenen Elemente zurück (statt der weniger flexiblen NodeList). byTagName(tagName) funktioniert identisch wie select(tagName). Durch den Funktionsnamen byTagName wird aber die Intention des Codes klarer. Daher erschien es mir sinnvoll, diese Funktion in der Sammlung zu behalten.

Den Baum hinauf- und hinabsteigen

Um die Knoten des Baums beginnend bei einem Bezugselement zu durchsuchen, gibt es in minlib.js zwei Funktionen, je nach Suchrichtung:
  • byCondition(condition,context) wendet auf die Nachfahren des Elements context die Funktion condition an und gibt einen Array aller Elemente zurück, für die diese Funktion "etwas Wahres" im JavaScript-Sinne zurückliefert (also einen Wert x, der die Bedingung x != false erfüllt). Wird nichts gefunden, wird der leere Array [] zurückgeliefert.
  • getParentByCondition(context,condition) durchsucht die Kette der Vorfahren, bis es ein Element gefunden hat, das die Bedingungsfunktion condition() erfüllt - und liefert dieses zurück. Wird sie nicht fündig, ist der Rückgabewert null.

    Wie schon bei selectParent() ist der Unterschied zur Standardfunktion Element.closest() nur der, dass das Bezugselement selbst nicht geprüft wird, sondern die Prüfungen beim Vaterelement beginnen.

Das name-Attribut

Das name-Attribut muss in einer Webseite nicht eindeutig sein - in der überwiegenden Mehrzahl der Fälle ist es aber eindeutig, und man ist an einer Funktion interessiert, die das Element zum angegebenen Namen zurückliefert.

Die folgende Funktion leistet das. Sie macht sich das optional chaining mit dem Operator ?. zunutze, um den Fall, dass es gar kein Element des angegebenen Namens gibt, nicht gesondert behandeln zu müssen. In diesem Fall gibt die Funktion einfach undefined zurück:

function byName(name) {
  return document.getElementsByName(name)?.[0];
}
Nun kann ein Elementname auf einer Webseite in verschiedenen Formularen vorkommen. Es ist also sinnvoll, einen zweiten Parameter als Kontextparameter vorzusehen, um nur den durch diesen Parameter aufgespannten Teilbaum des HTML zu durchsuchen. Per Default soll einfach document verwendet werden. Die endgültige Implementierung von byName(name,context) sieht dann folgendermaßen aus:
function byName(name,context=document) {
  return (context == document) ? 
    document.getElementsByName(name)?.[0] :
    [...document.getElementsByName(name)].find(x=>context.contains(x));
}
Um im (häufigeren) Fall, dass der implizite Kontext document verwendet wird, möglichst schnell zu sein, ist der alternative Fall separat programmiert: hier wird die von der API-Funktion document.getElementsByName() zurückgegebene NodeList in einen Array verwandelt, um mit der Array-Funktion find() das erste Element zu finden, das im angegebenen context enthalten ist.

Tatsächlich gibt es Fälle, in denen es sogar sinnvoll ist, dass mehrere Eingabeelemente denselben Namen tragen - z.B. kann eine Reihe von Checkboxes oder Radiobuttons den gleichen Namen haben, sich aber im Wert unterscheiden. Beim Formularsubmit werden dann die Name-/Wert-Paare der jeweils angekreuzten Elemente / übertragen.

Ein typisches Beispiel dafür stammt aus der HTML-Spezifikation: eine Webseite zur Bestellung von Pizzen, bei denen mit Checkboxes die verschiedenen Zutaten gewählt werden können. All diese Checkboxes tragen den gleichen Namen topping:

<form method="post">
  <label>Customer name: <input name="custname"></label>
  <label>Telephone:     <input type="tel" name="custtel"></label>
  <label>Email address: <input type="email" name="custemail"></label>
  <fieldset>
    <legend>Pizza Size </legend>
    <label><input type="radio" name="size" value="small">Small</label>
    <label><input type="radio" name="size" value="medium">Medium</label>
    <label><input type="radio" name="size" value="large">Large</label>
  </fieldset>
  <fieldset>
    <legend>Pizza Toppings </legend>
    <label><input type="checkbox" name="topping" value="bacon">Bacon</label>
    <label><input type="checkbox" name="topping" value="cheese">Extra Cheese</label>
    <label><input type="checkbox" name="topping" value="onion">Onion</label>
    <label><input type="checkbox" name="topping" value="mushroom">Mushroom</label>
  </fieldset>
  <label>Preferred delivery time:
    <input type="time" min="11:00" max="21:00" step="900" name="delivery">
  </label>
  <label>Delivery instructions:
    <textarea name="comments"></textarea>
  </label>
  <button>Submit order</button>
</form>
In solchen Fällen kann es vorkommen, dass man im JavaScript alle vom Benutzer gewählten Toppings ermitteln möchte.

Mit minlib.mjs könnte man hierfür die Funktion selectAll verwenden:

const toppings = selectAll("[name=topping]:checked").map(x=>x.value)
verwenden. Dieser Einsatz erschien mir allerdings so speziell, dass ich keine eigene Funktion byNameAll() in minlib.mjs dafür vorsehen wollte.

Texte schreiben und lesen

Die Funktionen setText(idOrNode,text) und getText(idOrNode) sind sich funktional praktisch gleich geblieben. Sie erlauben das Einlesen oder Schreiben von Texten in DOM-Elemente: entweder als untergeordnete Textknoten, oder - z.B. bei <input>-Elementen, im value-Attribut. Die beiden Funktionen abstrahieren also von der konkreten Art, wie ein Text in Elementen notiert wird.

Als erstes Argument idOrNode kann die ID eines Elements oder das Element selbst übergeben werden (wenn man es kennt). Um selbst API-Funktionen mit dieser Art von polymorphen Argumenten zu schreiben, bietet minlib.mjs die Hilfsfunktion getElement(idOrElement) an:

function getElement(idOrElement) {
  return idOrElement instanceof Element ? idOrElement : byId( idOrElement );
}

HTTP-Requests

In minlib.js gab es die Funktion doRequest für das Ausführung von HTTP-Requests, um weitere Ressourcen vom Backend zu laden (z.B. JSON-Files mit Rohdaten, die in das HTML-UI einzufüllen sind). Sie operierte mit dem Object XMLHttpRequest, das damals für solche Aufgaben benötigt wurde. Die Funktion doRequest habe ich in minlib.mjs ersatzlos gestrichen. Denn mit der Fetch API ist diese Funktion vollständig in den Browser eingezogen.
const response = await fetch("index.json");
const content = await response.json( );
// Ab hier: das JSON-Objekt content verarbeiten (z.B. ins HTML-Dokument einfüllen)

Navigation

Um von einer HTML-Seite zu einer anderen HTML-Seite zu navigieren, verwendet man normalerweise Formulare. Kontext kann dabei in Form von Formularfeldern übertragen werden. Formular und Formularfelder sind bei der Verwendung zum Navigieren meist unsichtbar. Die Methode POST wird in der Regel dem GET vorgezogen, damit die URL des Navigationsziels nicht mit den Formularfeldern verschmutzt wird.

Es gibt jedoch Fälle, in denen es unpraktisch ist, ein Formular mit seinen Feldern fix vorzubelegen: es soll möglich sein, auf verschiedene Seiten zu wechseln und dabei auch eine von Fall zu Fall variierende Menge von Formularfeldern mitzusenden. In solchen Fällen ist es praktisch, ein Formular in der aktuellen Seite "on the fly" aufzubauen, versteckte Formular mit den gewünschten Namen und Werten zu definieren und dann dieses dynamisch generierte Formular zu versenden, wodurch die Navigation ausgelöst wird. Genau dies leistet die Funktion navigateTo():

function navigateTo(url="", fields={}) {

// adhoc-Formular erzeugen, befüllen und abschicken
  const submitter = document.createElement("form");
  Object.assign(submitter,{action:url,method:"post"});

// Formular mit versteckten Formularfeldern abfüllen
  for (const [name,value] of Object.entries(fields)) {
    const field = document.createElement("input");
    Object.assign(field,{type:"hidden",name,value});
    submitter.appendChild(field);
  }

  document.body.appendChild(submitter);
  submitter.submit();

}

Fazit

Die meisten Probleme, die es 2011 im JavaScript-Bereich noch gab, sind mittlerweile gelöst. Auch erlaubt das Modularisierungskonzept nun feine und feinste Steuerung aller benutzten Softwareeinheiten. Vielleicht erweist sich das eine oder andere Funktiönchen von minlib.mjs in diesem neuen Umfeld ja doch noch als nützlich.

Sonntag, 7. April 2024

Fragen > Zweifeln

Es ist ein Unterschied zwischen der Haltung des Fragens und der des Zweifels. Der Zweifel enthält eine Anmaßung: über den in Frage stehenden Gegenstand hinaus greift er etwas Grundsätzlicheres an. Dabei könnten wir uns darauf beschränken, nach dem Wie einer Sache zu fragen, ohne deswegen gleich das umfassendere Daß ins Visier zu nehmen. Der Zweifler bleibt gewissermaßen nicht in den Grenzen und dem Sachgebiet seiner Frage, sondern überschreitet den Gegenstand der Frage und maßt sich an, das Stehen und Fallen des ganzen übergreifenden Zusammenhangs von der einen konkreten Frage abhängig zu machen.

Der hl. Apostel Thomas zweifelte an der Auferstehung. Er fragte nicht nur nach dem Wie, weil ihm die Berichte nach seiner sinnlichen Erfahrung sehr unwahrscheinlich vorkamen (womit er ja recht hat), sondern wir müssen befürchten, daß mit dieser Infragestellung der Auferstehung auch sein ganzer Glaube in Gefahr war. Obwohl zunächst nur die leibliche Auferstehung als solche ihm fragwürdig erschien, ging die Stoßrichtung seines Zweifels gegen den Glauben an den Sohn Gottes selbst. Sagt nicht der hl. Apostel Paulus (1. Kor. 15,13-14):

Wenn es keine Auferstehung der Toten gibt, ist auch Christus nicht auferstanden. Ist aber Christus nicht auferstanden, dann ist unsere Predigt leer und euer Glaube sinnlos.
Andererseits ist unser Erkenntnisvermögen uns ja gegeben, um die Frage nach der Wahrheit zu stellen. Etwas zu fragen, auch etwas in Frage zu stellen, ist gut und richtig und unserer Natur gemäß. Wir sind ja als Menschen so gebaut, daß wir durch das Hin- und Herbewegen einer Frage zu einer guten Antwort gelangen können - und sollen. Es wäre eine schwere Last, wenn wir uns das Fragen verbieten würden und alle Dinge nur auf blinden Glauben hin annähmen, ohne daß ihre Wahrheit in unserem eigenen Verstand zum Schwingen kommt.

Wie aber wäre eine gute Haltung des Fragens zu pflegen?

Hierin kann uns - wie in vielem - die Gottesmutter Vorbild sein. Auch sie hatte eine Frage. Sie fragte den Engel, der ihr die Empfängnis des Gottmenschen in ihrem Leibe ankündigte (Luk. 1,34):

Wie soll das geschehen, da ich doch von keinem Manne weiß?
Das ist gut gefragt! Denn es enthält die Haltung des Sich-Hineinfügens. Sie fragt nur nach dem Wie, sozusagen nach den Ausführungsdetails des göttlichen Willens, da sie dieses Wie nicht versteht. Den Plan Gottes selbst stellt sie dabei nicht in Frage. Daher wird sie einer ausführlichen Antwort gewürdigt (Luk. 1,35-37):
Der heilige Geist wird auf dich herabkommen, und die Kraft des Allerhöchsten wird dich überschatten; darum wird auch das Heilige, welches aus dir geboren werden soll, Sohn Gottes genannt werden.

Und siehe, Elisabeth, deine Verwandte, auch sie hat einen Sohn empfangen in ihrem Alter, und dies ist der sechste Monat für sie, die unfruchtbar heißt, denn bei Gott ist kein Ding unmöglich.

Sie bekommt also nicht nur eine direkte Antwort auf ihre Frage - wenn sie auch natürlich für den Menschensinn schwer faßbar ist - sondern darüberhinaus noch einen Hinweis auf das Wunder der Geburt des Johannes. Schließlich appelliert der Engel an ihre Demut, indem er darauf hinweist, daß für Gottes Allmacht und Größe sowieso kein Ding unmöglich ist.

Maria antwortet mit ihrem großen hochzeitlichen Ja, durch das sie sich in Demut in Gottes Plan fügt (Luk. 1,38):

Siehe, ich bin die Magd des Herrn, mir geschehe nach deinem Worte.
Wenige Verse vorher lesen wir im selben Kapitel von einer anderen Frage mit einem anderen Duktus. Als ein Engel dem Priester Zacharias beim Tempeldienst ankündigt, daß seine hochbetagte Frau Elisabeth ihm einen Sohn gebären werde, fragt er zurück (Luk. 1,18):
Woran soll ich das erkennen? denn ich bin alt, und mein Weib ist vorgerückt an Tagen.
Hier ist keine Frage nach dem Wie, sondern es ist ein grundsätzlicherer Zweifel herauszuhören. Zacharias möchte offenbar ein Zeichen haben, um zu erkennen, daß der Engel die Wahrheit spricht. Als Folge seines Zweifels muß er bis zur Geburt seines Sohnes Johannes des Täufers verstummen.

Es ist bestimmt kein Zufall, daß diese beiden Reaktionen hier im selben Kapitel des Lukasevangeliums aufgeführt sind. Zacharias wie Maria wurde etwas offenbart, das zu verstehen in seiner ganzen Tiefe sie überforderte. Aber sie reagieren verschieden darauf. Zacharias verschließt sich, kann es nicht glauben und verlangt einen Beweis. Auch für Maria ist die Botschaft schwer zu verstehen, auch ihr tut sich eine Frage auf, aber sie bittet nur um Klarheit über das "Wie".

Wie gehen wir mit Glaubensinhalten um, die zwar in nichts gegen unsere Vernunft sind, aber unser Verstehen übersteigen? Maria lebte es uns vor: in der Geburtsgeschichte heißt es (Lk 2,19):

Maria aber bewahrte alle diese Worte, und bewegte sie in ihrem Herzen.
Die Fragen immer wieder zu bewegen, sie offenhalten im Geiste, es auszuhalten, daß man sie noch nicht beantworten kann - wenn wir das tun, haben wir die Haltung Mariens. In dieser Welt gibt es stets mehr Fragen als Antworten, gar zu vieles muß ungeklärt bleiben. Schließlich haben wir nur eine endliche Lebenszeit - nicht genug, um all die Fragen, die sich uns aufwerfen, mit der gebotenen Gründlichkeit zu studieren: "Und eh’ man nur den halben Weg erreicht, muß wohl ein armer Teufel sterben," seufzt Wagner in Goethes Faust.

Darüberhinaus ist unser Erkenntnisvermögen nur an der Sinnenwelt ausgebildet. Es kann zwar die allergrößten Gegenstände erfassen, ja sogar die Frage der Existenz Gottes läßt sich mit Klarheit beantworten (vgl. Röm. 1,19-21) - aber dieses Erfassen bleibt doch dürr und skeletthaft, unlebendig, abgeschattet durch unseren irdischen Sinn. Auch kann unser Erkenntnisvermögen sehr leicht in die Irre gehen. Wenn sich diese Fehlbarkeit schon in der Naturerkenntnis zeigt, um wieviel mehr müssen wir mit Fehlern rechnen, wenn wir uns den göttlichen Dingen zuwenden? Gott denkt und plant anders als wir denken (Jes. 55,8):

Meine Gedanken sind nicht eure Gedanken, noch meine Wege eure Wege.
Wenn unsere eigenen Verstandeskräfte also kaum in diese Regionen hineinreichen, brauchen wir Gelehrigkeit (docilitas) gegenüber Gott - und auch gegenüber der Mutter Kirche. Gott gibt uns durch Seine Offenbarung Aufschluß, wenn wir diese gelehrig erforschen - Er stillt den Hunger nach Geist, erhört Gebete, öffnet dem, der anklopft. Auch Kirchenväter und große Gelehrte haben bereits intensiv über Fragen nachgedacht, die uns selbst ganz neu erschienen - Wissensschätze aus Jahrtausenden liegen bereit, die uns helfen können.

Die sogenannten evangelischen Räte erweisen sich als die besten Waffen, um dem Zweifel seine Spitze zu nehmen und ihn in die "gelehrige Frage" zu verwandeln (und dem Sinne nach kann sie jeder annehmen, egal in welcher Lebenssituation): In Demut anzuerkennen, daß es Höheres, Reiferes, Verständigeres gibt als uns selbst, gibt uns die Bereitschaft, Dinge anzunehmen, die nicht in unsere vorgefaßten Meinungen passen. Die Keuschheit lehrt uns die Enthaltung von Dingen, die uns nicht weiterführen - so setzen wir die Prioritäten richtig und laden unseren Geist nicht mit unwichtigen irdischen Dingen voll, die uns nur die Aussicht versperren. Und auch der Gehorsam hängt - wie die Demut - mit der Anerkennung von Autorität zusammen. Nicht blinder Gehorsam wie gegenüber einem Tyrannen, sondern ein verstehender, gelehriger Gehorsam ist das Ideal.

Es gibt viele Fragen, die wir (noch) nicht beantworten können und die zu einem unproduktiven Zweifel verführen können - etwa: Wie wirkt die göttliche Vorsehung? Wie wirken die Hierarchien im individuellen Schicksal, wie im Völkerschicksal, wie im Menschheitsschicksal, wie in der Naturordnung? Wie ist der Mensch zu dem geworden, was er heute ist?

Aber über all diese Fragen, die wir bewegen, aber noch offenlassen müssen, mögen wir nicht den Ausblick des hl. Paulus vergessen (1 Kor. 13,12):

Jetzt sehen wir durch einen Spiegel im Rätsel, alsdann aber von Angesicht zu Angesicht. Jetzt ist mein Erkennen Stückwerk, dann aber werde ich erkennen, so wie auch ich erkannt bin.


Hendrick ter Brugghen: Der ungläubige Thomas

Sonntag, 3. Dezember 2023

Den Sinn ändern

peccavimus et facti sumus tamquam immundus nos
et cecidimus quasi folium universi
et iniquitates nostrae quasi ventus abstulerunt nos

Wir haben gesündigt und sind Unreine geworden,
Wie ein Blatt sind wir alle gefallen,
Und unsere Missetaten haben uns fortgetragen wie der Wind

Aus dem Adventsgesang Rorate Caeli, nach Jesaja 64,6

Die poetischen Bilder dieses Klagegesanges, die vom Propheten Jesaja stammen, laden zu einer näheren Betrachtung ein, denn sie umschreiben unsere irdische Verfaßtheit.

Zentraler Gedanke ist die Feststellung, daß wir gewissermaßen “nicht in der Ordnung leben” - und damit nicht "in Ordnung sind". Das kann der Anfang einer tiefgreifenden Sinnesänderung werden.

Wir sind gefallen wie ein Blatt. - Ein Blatt fällt langsam immer tiefer, wobei es in einer ziellos schwingenden Bewegung mal in diese, mal in jene Richtung wandert. Der leiseste Wind kann es ergreifen und mit sich nehmen, bevor er das Interesse an ihm verliert und es wieder in seinen eigenen Fall entläßt. Die horizontalen Bewegungen erregen zwar die Aufmerksamkeit - bei allem aber bleibt eines sicher: der stete Fall, die Abwärtsbewegung.

So ist es auch mit uns. Wir lassen uns forttragen, wähnen uns vielleicht sogar in zielgerichteter Eigenbewegung, aber wir haben in Wahrheit keinen festen Halt. Und ohne den ist nur eines sicher: daß es immer weiter bergab geht mit uns. Womit ich nicht den unausweichlichen Verfall des Leibes meine, sondern vor allem und in erster Linie den Verfall der Seele.

Dabei haben wir, anders als das Blatt, tief in uns die Sehnsucht, in der Ordnung zu sein. Der Fall ist nur die Folge einer aktiven Verdrängung dieser Sehnsucht. Das ist der Grund, daß die Strophe aus dem Rorate-Gesang mit dem Wort peccavimus beginnt - wir haben gesündigt. Aktiv Perfekt - Ursache des Falls ist eine aktive innere Abwendung, eine Verdrängung dessen, was das Gute und Richtige gewesen wäre, die zu unserer Vorgeschichte gehört. Das zieht die Krankheit des Aussatzes nach sich - das Seelenkleid wird fleckig und häßlich, denn die eigene Kraft ist nicht stark genug, um es rein zu erhalten. Schließlich erlischt das innere, übernatürliche Leben der Seele vollständig.

Eine typische, von der Gesellschaft einprogrammierte Reaktion auf solche Gedanken ist: “Hört auf mit diesem Gerede von Schuld und Sünde - laßt uns doch positive Gedanken pflegen, laß es uns einfach gut miteinander haben!” Denn das Thema Schuld und Sünde gehört zu den unbeliebten Teilen der Religion. Religion wird geduldet, wenn sie das sogenannte “selbstbestimmte Leben” nicht besonders stört, wenn sie der spirituellen Erbauung dient und dem einzelnen einen Sinn gibt - aber nicht wenn sie mahnt und warnt, zu Buße und Gebet aufruft.

Das Bewußtsein von der Sündhaftigkeit - und damit eng verknüpft: die Sehnsucht, wieder in der Ordnung zu sein - ist kein Sondergut der christlichen Religion, sondern etwas allgemein Menschliches, das in unsere Seelen gelegt ist. Viele Kulturen legen davon Zeugnis ab. Die alten Ägypter strebten beispielsweise besonders danach, in der Ma’at zu leben - im Frieden und in der Ordnung mit den Göttern und den Menschen; das zugrundeliegende Verb ma’a heißt soviel wie “richten” oder “lenken”. Auf einer USA-Reise erfuhr ich einmal, daß das Wichtigste im Leben eines Navajo-Indianers etwas war, das er Hózhó nannte - was soviel wie Schönheit, Harmonie oder Ordnung heißt. Eine Sehnsucht nach diesem Zustand kann aber nur entstehen, wenn man bemerkt, daß man noch nicht (oder: nicht mehr) in ihm ist.

Aber die Ursache für das Leben außerhalb der Ordnung liegt allein in unserem Willen. Wir selbst sind dafür verantwortlich. Zum Begriff der Sünde (die mit "Absonderung" wortverwandt ist) gehört das Selbstverschuldete, der selbstgewählte Akt des Sich-Verschließens. Wir verschließen Augen und Ohren für die Quelle, von der her alles wieder gerichtet werden könnte.

Nebenbei: es ist nicht nur für den einzelnen, sondern auch für die Gemeinschaft als Ganzes das Beste, in dieser Ordnung zu sein. Die Sehnsucht des einzelnen, sich selbst auf diese Ordnung hin auszurichten, geht in Harmonie mit dem richtigen Leben unter den Mitmenschen. Letzteres - der richtige Umgang mit den Mitmenschen - fließt aus der Quelle, ohne selbst die Quelle zu sein, als bloße Wirkung. Dies könnte zu anderer Gelegenheit näher betrachtet werden.

An den Umkehrpunkten der Hin- und Herbewegungen des Blattes merken wir selbst, daß die Richtung nicht stimmt. Wir geraten in eine Lebenskrise und suchen nach neuen Wegen. Beispielsweise bemerken wir eine zunehmende Trockenheit oder Leere, wenn wir in der bisherigen Richtung weitergehen würden, oder wir spüren, daß dieser Weg nicht die erhoffte Erfüllung bringen wird. Eine Verheißung, die am Anfang stand, erweist sich als trügerisch. Oder wir sind so tief verstrickt in einen falschen Weg, die Konsequenzen treffen uns so deutlich und schmerzhaft, daß es uns nicht mehr möglich ist, die Augen davor zu verschließen. Dann greifen wir schnell nach einem neuen Ziel, meist ohne uns zu fragen, ob dies nun ein höherwertiges ist, oder wieder nur ein Windhauch, nur diesmal aus einer anderen Richtung, der sein Spiel mit uns treibt. Oft ist uns sogar mehr oder weniger bewußt, daß wir uns mit all dem nur zerstreuen, uns ablenken von schmerzhaften, aber wahren Einsichten.

Ein bekanntes, leicht melancholisches Herbstgedicht von Rainer Maria Rilke nimmt auch von der Beobachtung fallender Blätter seinen Ausgang:

Die Blätter fallen, fallen wie von weit,
als welkten in den Himmeln ferne Gärten;
sie fallen mit verneinender Gebärde.

Und in den Nächten fällt die schwere Erde
aus allen Sternen in die Einsamkeit.

Wir alle fallen. Diese Hand da fällt.
Und sieh dir andre an: es ist in allen.

Und doch ist Einer, welcher dieses Fallen
unendlich sanft in seinen Händen hält.

In diesem schicksalhaften Fallen ist Erlösung möglich durch Hinwendung zu dem, der uns auch in unserem Fallen in seinen Händen hält: Johannes der Täufer, der Wegbereiter des Herrn, predigte: “Ändert euren Sinn!” Darin liegt die Rettung: sich aus dem Hin und Her dieser Welt herauszuziehen, sich für die höheren, himmlischen Dinge zu öffnen, und sich bereit zu machen, von Christus berührt und von Grund auf verwandelt zu werden.

Donnerstag, 3. Juni 2021

Iterationen beim Entwickeln

Programmierkunst

Das Entwickeln von Software ist ein schöpferischer Vorgang: es gibt kein Rezept, kein "Schema F", das man einfach nur lernen und anwenden müßte, um eine gegebene Anwendungslogik zu implementieren, also in ein System zusammenarbeitender Objekte umzusetzen.

Was wir nur haben, ist eine Sammlung von Empfehlungen, von Best Practices, von Erfahrungsregeln, auch von kolossalen Fehlschlägen in der Vergangenheit. So wächst die Kunst des Entwickelns – sowohl des einzelnen Entwicklers als auch des ganzen Berufsstands – mit jedem Erfolg und mit jedem Fehlschlag.

Reflexion

Dieses Wachstum beruht vor allem auf der Reflexion. Der Zugewinn durch eine konkrete Aufgabe entsteht nicht darin, sie einfach nur gelöst zu haben, so daß ich etwas Funktionierendes abliefere. Der Zugewinn entsteht allein dadurch, daß ich mich frage: 

  • Was kann ich von dieser Arbeit mitnehmen?
  • Welche allgemeinen Features, die gar nicht spezifisch für diese Aufgabe sind, kann ich in späteren Aufgaben wiederverwenden?
  • Wie war die Arbeit?
  • Auf welche Probleme bin ich beim Implementieren gestoßen?
  • Was hat mich unverhältnismäßig lange aufgehalten?
  • Wie habe ich die einzelnen Teilaufgaben konkret gelöst?
  • Wie paßt das Produkt in den Gesamtkontext der Entwicklungen im System?
  • Was hätte ich besser machen können?
  • Was kann ich vielleicht auch jetzt, wo die Sache schon gerade auf "Testbereit" gesetzt wurde, aber noch frisch und in meinem Kopf noch maximal präsent ist, an der Lösung verbessern, am internen Zusammenspiel der Klassen, am Datenmodell, an der Effizienz?

Loops

Es hat sich gezeigt, daß Loops wie solche Reflexionsschritte – die Schwungräder des produktiven Arbeitens sind. Eine Loop besteht – allgemein gesprochen – darin, daß zu einem bestimmten Punkt die Arbeit nicht einfach nur linear fortgesetzt wird, sondern das bislang Produzierte aus einem Distanzverhältnis heraus angeschaut und bewertet wird.

Was bedeutet das? Zur Loop gehören:

  • eine Kontrollinstanz, die ich Agent nennen möchte
  • eine konkrete reflektierende Tätigkeit,
  • und ein Ergebnis, das Einfluß auf den Hauptstrom der Tätigkeit hat.

Agenten

Wer ist der Agent? Er steht, wie gesagt, in einem distanzierten Verhältnis zum aktuellen Produktionsprozeß. Dieser Agent kann sein:

  • der Produzent selbst, indem er seine eigene Arbeit aus der Distanz betrachtet
  • eine andere Person,
  • oder eine Maschine.

Die reflektierenden Tätigkeiten

Was für reflektierende Tätigkeiten führt der Agent aus?

  • Er kann die Einhaltung formaler Standards im Produkt überwachen
    (gut geeignet für maschinelle Agenten).
  • Er kann das Produkt auf die beabsichtigte Funktion hin testen.
  • Er kann die konkrete Ausgestaltung des Produkts auf seine Güte hin bewerten
    (etwa nach Kriterien wie Ergonomie, Zugänglichkeit, Performance).
Diese Tätigkeiten sind hier aufsteigend nach Schwierigkeit aufgeführt. Die Bewertung auf Güte ist das Schwierigste, da sie kaum operationalisierbar ist, nur von einem Menschen ausgeführt werden kann und die Kriterien der Bewertung vom Erfahrungswissen des Menschen abhängen, oft nur als implizites Wissen im einzelnen Menschen vorliegen.

Wirkung

Welchen Einfluß auf den Produktionsprozess hat die Loop?

  1. Die Justierung des Arbeitsschrittes auf das Gesamtziel hin:
    • die Vergewisserung, daß man noch "auf Kurs" ist, oder
    • die Verbesserung des gerade reflektierten Arbeitsabschnitts. Man geht gewissermaßen in der Zeitachse zurück zum Beginn dieses Abschnitts und korrigiert oder verbessert
  2. Man hat etwas gewonnen (Erfahrung, Wissen, wiederverwendbare Teile der Arbeit) und kann es mitnehmen für spätere Aufgaben

Die Arbeit des Entwickelns

Wenn man genauer hinschaut, besteht die Arbeit des Entwickelns bis in die kleinsten Schritte immer aus solchen Loops. Wenn ich in die Entwicklungsarbeit zu irgendeinem Zeitpunkt t hineinzoome, sehe ich beispielsweise folgendes:

  • Ich produziere eine oder mehrere Codezeilen.
  • Ich bekomme beim Aktivieren Syntaxfehler der Methode
  • Ich korrigiere diese.
  • Dasselbe eine Ebene höher: eine syntaktisch korrekte Methode kann immer noch zu Syntaxfehlern auf Ebene der Klasse führen.
  • Auch diese korrigiere ich.
  • Wenn ich das Gefühl habe, die Änderung könnte Risiken für Subklassen haben, wähle ich "KlassePrüfenSyntax (Subklassen)". Das muß ich allerdings aktiv tun, dem ging also die Reflexion über mögliche Seiteneffekte meines gerade produzierten Codes voraus.
  • Wenn ich auf diese Weise einige Zeit gearbeitet habe, ist die Klasse als Ganzes in einem Zustand für weitere Prüfungen (den Zeitpunkt ermittele ich durch Reflexion).
  • Dann füge ich den Reflexionsschritt "Erweiterte Syntaxprüfung" und/oder "Code Inspector" und/oder "Unittests" ein. Dies sind bereits Loops, die einen größeren Teil meiner in vielen kleineren Iterationsschritten geleisteten Arbeit zusammenfassen.
  • Ich bewerte die Ergebnisse dieser Reflexionsschritte: sind die Prüfungen der Erweiterten Syntaxprüfung oder des CodeInspectors wirklich relevant? Oft sind es nur Warnungen, die auf potentielle Probleme hinweisen.
  • Ich bewerte die Ergebnisse der bestehenden Unit Tests auf die Abdeckung des Codes hin: wie gut ist die Methoden- und Zweigabdeckung? Für völlig neue Methoden sollte ich spätestens jetzt erwägen, das beabsichtigte Programmverhalten durch einen Unit Test abzudecken – wenn auch nicht starr: es ist völlig in Ordnung, wenn mich eine Aufwand/Nutzen-Überlegung dazu führt, keinen Unit Test zu schreiben, weil der Aufwand, die gerade entwickelte Codeeinheit mit (weiteren) Unit Tests abzusichern, zu hoch ist im Verhältnis zum Nutzen.
  • Wenn ich auf diese Weise einige Codeeinheiten erstellt habe, will ich ihr Zusammenwirken testen. Dazu führe ich einen Entwicklertest aus. Üblicherweise machen wir das heute noch manuell:
  • wir rufen beispielsweise eine Transaktion auf und schauen, ob das neue Feld korrekt versorgt und fortgeschrieben wird, oder ob die neue Prüfung korrekt in das Transaktionsverhalten integriert ist.
  • oder wir rufen Methoden, Funktionsbausteine oder Sequenzen von Funktionsbausteinen im Einzeltest auf
  • oder wir schreiben sogar kleine Testreports, die ein bestimmtes Feature überprüfen.
    Diesen Testreports messen wir oft keine große Bedeutung zu, betrachten sie als "Wegwerf-Code". Zu Unrecht. Wenn wir uns die kleine Mühe machen würden, diesen Testcode statt in einem Programm in der Methode einer Testklasse zu implementieren, wäre für die Zukunft schon einiges gewonnen. Es zeigt sich nämlich immer wieder, daß jeder, der später auf dieses Thema zurückkommt, genau nach solchen einfachen Run Tests sucht, um das Laufzeitverhalten anzuschauen. Findet er nichts, muß er den "Wegwerfcode" noch einmal neu schreiben.
  • Schlägt der Entwicklertest fehl, analysiere ich das Programmverhalten und gehe dabei geistig zurück zum Punkt t-∆t, als ich den neuen Code eingefügt hatte. Wird er überhaupt aufgerufen? Wird seine Schnittstelle beim Aufruf korrekt versorgt? Werden die übergebenen Daten gemäß der von mir beabsichtigen Logik verarbeitet? Klappt die Ergebnisübergabe an die UI-Objekte?
  • Funktioniert der Entwicklertest, so mache ich mich an die Entwicklung des nächsten Features und verfahre wieder wie eben beschrieben.
  • Habe ich einen gewissen Stand erreicht, so beurteile ich (eigener Reflexionsschritt), ob die Anzahl der entwickelten Features bereits ausreicht, um die Weiterentwicklungen ins Q-System zu transportieren und dort integrativ testen zu lassen.
  • Der Berater testet das neue Feature-Set im Q-System mit seinen Daten, nach seinen Regeln, mit seiner Sicht auf die Prozesse, mit seinen eigenen Akzeptanzkriterien.
  • Der Berater meldet mir seine Ergebnisse zurück, positive wie negative.
  • Negative Ergebnisse versuche ich mit einem Entwicklertest zu reproduzieren.
  • Gelingt mir dies nicht, muß ich das Problem im Q-System analysieren und den Grund herausfinden, warum im D-System das Symptom nicht zu reproduzieren war.
  • Gelingt es mir, muß ich die gerade entwickelten Codeeinheiten auf dieses Symptom hin analysieren (das wäre der Fehlerfall), oder – der häufigere Fall – bislang nicht beschriebene, aber eigentlich gewünschte Features in mein Backlog für die weitere Arbeit aufnehmen. (Aufgrund der Komplexität des Systems ist es meist nicht möglich, alle benötigten Features in allen möglichen Szenarien bereits im Vorfeld zu beschreiben.)

Wie man sieht, besteht die Arbeit letztlich aus lauter kleinen Mikro-Iterationen, die von etwas größeren Iterationen umfaßt werden usw.


In das quantitative Schema eines "Kausalschleifendiagramms" gefaßt (mit Pfeilen vom Typ "je mehr …, desto mehr/weniger…"), ist jede einzelne Iteration, selbst die kleinste, von folgendem Typ:


Man sieht, daß im Reflexionsschritt stets ein Standard von außen dazukommt ("Desired Product Quality"). Ist man selbst der Agent des Reflexionsschritts, setzt man sich gewissermaßen "einen anderen Hut auf", um diese Anforderungen an das Produkt heranzubringen. Das funktioniert, ist aber nicht unbedingt die beste Wahl. Denn ein Außenstehender findet mögliche Schwachstellen eines Produkts oft besser als sein Hersteller.

Das, was hier mit "Desired Product Quality" beschriftet ist, können Syntaxregeln oder Entwicklungsrichtlinien sein, auch weniger formale Anforderungen wie Wiederverwendbarkeit, Wartbarkeit, Intentionalität des Codes, und natürlich die Produktanforderungen, wie sie z.B. in den Use Cases beschrieben wurden. Eigentlich alle Anforderungen an das Produkt neben der einen, die ich gerade implementiert habe (und auch diese eine noch hinsichtlich der Seitenaspekte wie Korrektheit usw., die ich beim Entwickeln nicht im Auge hatte.)

Montag, 4. Januar 2021

Vergiß den Letzten nicht!

Eine Gruppenschleifenfunktion in JavaScript


Eine bekannte Konstruktion


Bei der Abarbeitung von Listen in einer Schleife will man häufig die Elemente der Liste zu Gruppen zusammenfassen und dann für jede Gruppe eine Aktion ausführen. Es kann beispielsweise eine Liste von Positionen mehrerer Aufträge gegeben sein, und man will pro Auftrag eine Funktion aufrufen, um dessen Positionen zu verbuchen:

AuftragPos.Aktion
471110
471120
471130Auftrag 4711 verbuchen
471210
471220Auftrag 4712 verbuchen
471310
.........
489910
489920Auftrag 4899 verbuchen

Neben der vollständigen Liste allItems aller Auftragspositionen ist also noch ein "Gruppenwechselkriterium" gegeben, um zu erkennen, ob mit der aktuellen Position ein neuer Auftrag begonnen wurde. Ist dieses erfüllt, führt man die Aktion aus. Wenn nicht, sammelt man die Position weiter im "Gruppenarray".

Und hier muß man nun Vergiß den Letzten nicht! beachten: nachdem die Schleife beendet wurde, befinden sich die Positionen des letzten Auftrags im Gruppenarray, wurden aber noch nicht verbucht. Man muß daher den Verbucher nicht nur beim Gruppenwechsel aufrufen, sondern ausdrücklich noch ein letztes Mal nach Beendigung der Schleife.

Von der Idee her müßte der Code also etwa so aussehen:
let orderItems = [], previousItem;
for (let item of allItems) {
  if ( previousItem !== undefined && 
       item.orderNumber != previousItem.orderNumber ) {
    saveOrder(orderItems);
    orderItems = [];
  }
  orderItems.push(item);
  previousItem = item;
}
// Don't forget the last!
if (orderItems.length > 0) saveOrder(orderItems);

Dieser an die Schleife hinten angehängte Extra-Aufruf von saveOrder() ist häßlich, aber leider nötig. Es gibt keinen vernünftigen Weg, ihn zu vermeiden.[1] Der Grund ist, daß man bei der Prüfung auf Gruppenwechsel gewissermaßen zurückschaut: von der neuen Position wird auf die zurückliegenden Positionen geschaut, und durch den Vergleich mit der zuletzt durchlaufenen Position wird erkannt, daß wieder ein Auftrag verbucht werden muß. Die Positionen dieses Auftrags werden also zu einem Zeitpunkt verbucht, zu dem sie "gar nicht mehr aktuell" sind, weil man bereits bei der ersten Position des nachfolgenden Auftrags angekommen ist. Für die Positionen des letzten Auftrags gibt es aber keine nachfolgende Position mehr, von der aus man zurückschauen kann.

Idee: eine Gruppenschleifenfunktion


Man könnte sich aber ein Konstrukt "Gruppenschleife" ausdenken, in der diese Art der Gruppenbildung - inclusive des häßlichen letzten expliziten Aufrufs - hinter den Kulissen abgearbeitet wird. Tatsächlich gibt es in der Programmiersprache ABAP genau ein solches Konstrukt - die sehr mächtige Anweisung LOOP AT ... GROUP BY ... (die neben dieser Aufgabe noch eine Menge anderer Aufgaben rund um das Thema "Gruppierung von Einträgen interner Tabellen" löst). Dann müßte man genau noch einmal das häßliche Don't forget the last programmieren, aber eben hinter den Kulissen, in der Implementierung des Konstrukts "Gruppenschleife", das dann im Anwendungscode nur noch aufgerufen wird.

In JavaScript würde man nicht die originale Liste abarbeiten, sondern einen Gruppeniterator, der in jedem Iterationsschritt die Gruppe der zu dieser Auftragsnummer gesammelten Position liefert (Voraussetzung ist natürlich, daß die Liste nach der Auftragsnummer sortiert ist, also Positionen zum gleichen Auftrag in der Liste aufeinander folgen).

Mit einer (noch zu schreibenden) Funktion groupsByKey() könnte der obige Code dann wie folgt vereinfacht werden, wobei die Absicht des Programms klarer ausgedrückt wird und die technischen Details der Gruppenbildung in diese Funktion ausgelagert werden:
for (let orderItems of groupsByKey(allItems,it=>it.orderNumber)) {
  saveOrder(orderItems);
}

Wie sie zu schreiben wäre


Damit eine solche Funktion groupsByKey() ihre Arbeit tun kann, braucht sie zweierlei:
  • Eine Liste (ein iterierbares Objekt) baseIterable - im Beispiel die Grundliste allItems vieler Auftragspositionen
  • Eine Funktion getKey(), die einem Element der Liste seinen Gruppenschlüsselwert zuordnet. Durch Vergleich der Gruppenschlüsselwerte kann die Gruppenschleifen-Implementierung erkennen, daß ein Gruppenwechsel vorliegt.

Man könnte sich nun eine Implementierung vorstellen, die einen Array von Arrays (AoA) zurückliefert: der äußere Array zählt die einzelnen Gruppen auf, und jede einzelne Gruppe ist ihrerseits ein Array, bestehend aus den Elementen dieser Gruppe. Diese Lösung würde aber keinen effizienten Gebrauch vom Speicher machen, was für große Arrays ein Problem darstellen kann.

Eine bessere Lösung ist es, die Funktion groupsByKey() nur einen Iterator zurückgeben zu lassen und die einzelnen Gruppen nur pro Iterationsschritt zurückzugeben. Während also der Aufrufer über die Gruppen iteriert:
for (let orderItems of groupsByKey(allItems,it=>it.orderNumber)) { ... }
wird in der Implementierung von groupsByKey über die Elemente von allItems selbst iteriert.

Generatorfunktionen in JavaScript


Das für eine solche Implementierung perfekt passende Konstrukt sind in JavaScript die sogenannten Generatorfunktionen, die mit dem Schlüsselwort function* definiert werden. Bei einem Iterationsschritt werden sie ausgeführt, bis sie zur nächsten yield-Anweisung stoßen. Den dort angegebenen Ausdruck geben sie zurück. Beim nächsten Iterationsschritt werden sie genau nach dem letzten yield fortgesetzt, wobei der gesamte Ausführungskontext der Funktion erhalten bleibt. Wird die Funktion schließlich beendet, so wird auch die Iteration beendet.

Die Gruppenschleife kann daher als function* wie folgt definiert werden:
function* groupsByKey(baseIterable,getKey) {

  let currentGroup = [];
  for (let entry of baseIterable) {
    let key = getKey( entry );
    if (currentGroup.key !== key) {
        yield currentGroup;  // Gruppen-Array an Aufrufer zurückgeben
        currentGroup = [];   // Gruppen-Array für neuen Key initialisieren
        currentGroup.key = key;
      } 
    }
    currentGroup.push(entry);
  } 
  // "DON'T FORGET THE LAST" (falls baseIterable nicht leer war):
  if (currentGroup.length > 0) {
    yield currentGroup;
  }
  
}

Wie man sieht, gibt es immer noch den Teil DON'T FORGET THE LAST, bei Verwendung dieser Funktion aber eben nur noch einmal - und zwar hinter den Kulissen, nicht mehr im Anwendungscode selbst.

In dieser Implementierung sind die bei der Iteration gelieferten Gruppenobjekte einfach Arrays, die die einzelnen Elemente der Gruppe bis zum Gruppenwechsel enthalten, "verziert" mit einem zusätzlichen Attribut key, das den Schlüssel dieser Gruppe enthält.

Die Funktion groupsByKey() wäre ein Kandidat für eine Bibliothek übergreifender (applikationsunabhängiger) Funktionen, die dann für die verschiedensten konkreten Gruppenschleifen genutzt werden kann.

Noch ein Beispiel


Nehmen wir beispielsweise an, testArray sei ein sortierter Array natürlicher Zahlen. Dann können wir ihn mit dem folgenden Code in Tausendern gruppieren:
for (let g of groupsByKey(testArray,x=>Math.floor(x/1000))) {
  console.log(g.key,[...g])  
}
Die Konsole gibt dann beispielsweise aus:
  0 [ 364 ]
  2 [ 2628 ]
  3 [ 3208, 3550 ]
  4 [ 4110, 4602, 4851 ]
  5 [ 5480, 5532, 5533 ]
  7 [ 7032, 7385, 7485, 7632 ]
  9 [ 9271 ]

Beliebige groupChange()-Funktionen


Die Funktion groupsByKey() ist noch leicht verallgemeinerbar. Die Annahme war ja, daß die Einträge über eine Schlüsselfunktion getKey() zu gruppieren sind, wobei vorausgesetzt wird, daß Einträge gleichen Schlüssels in der Grundliste aufeinanderfolgen. Das ist aber schon eine Spezialisierung. Eigentlich benötigt man nur eine Funktion groupChange(currentEntry,lastEntry), die für aufeinanderfolgende Einträge des Arrays aufgerufen wird (wie im allerersten Beispiel dieses Blogposts) und true zurückliefert, falls ein Gruppenwechsel vorliegt. Wie ein Gruppenwechsel definiert wird, liegt dann voll in der Freiheit des Aufrufers.

Nun ist natürlich die Ermittlung des Gruppenwechsels über eine Schlüsselfunktion schon der häufigste Anwendungsfall. Daher sollte man aus der obigen Funktion groupsByKey() eine allgemeinere Iteratorfunktion groups() extrahieren, die mit einer beliebigen groupChange-Funktion arbeitet. So ergeben sich schließlich zwei Funktionen:
// Implementation of the function "groups()" 
// using a generator function
function* groups(baseIterable,groupChange) {

  let currentGroup = [], previousEntry;
  for (let entry of baseIterable) {
    if (previousEntry !== undefined) {
      if (groupChange(entry,previousEntry)) {
        yield currentGroup;
        currentGroup = [];
      } 
    }
    currentGroup.push(entry);
    previousEntry = entry;
  } 
  // "DON'T FORGET THE LAST" 
  // (if there were iterations at all)
  if (currentGroup.length > 0) {
    yield currentGroup;
  }

}

// The most typical use case: entries grouped by key
function* groupsByKey(baseIterable,getKey) {
  let key,lastKey; // Buffer current key and last key
  let groupChange = (a,b)=>{    
    lastKey = key ?? getKey(b);
    key = getKey(a);
    return key!=lastKey;
  };
  for (let g of groups(baseIterable,groupChange)){
    g.key = lastKey;
    yield g;
    lastKey = key;
  }
}
Die etwas umständliche Pufferung von key und lastKey in dieser Implementierung von groupsByKey() (die identisch wie die obige funktioniert, nur daß eben der eigentliche Gruppenschleifenmechanismus in eine allgemeinere Funktion groups() ausgelagert wurde) dient erstens dazu, daß die Funktion getKey() pro Eintrag wirklich nur einmal aufgerufen werden muß; zum anderen wird der lastKey verwendet, um das Attribut key des Gruppen-Arrays zu setzen (ebenfalls ohne getKey() noch einmal aufrufen zu müssen).

Online Testen


Hier kann man den Code in action betrachten - auf der JavaScript-Online-Plattform repl.it.





Sonntag, 15. Dezember 2019

Ostertermin und Osterparádoxa

Bewegliche Feiertage im Kirchenjahr
Die zyklische Osterrechnung (computus)
Das Osterparádoxon 2019
Über eine Reform der Osterrechnung
Paradoxien von 1950 bis 2050

Warum haben wir eigentlich die sogenannten "beweglichen Festtage"? Warum kann man sie nicht - wie die übrigen Feiertage - auf ein festes Kalenderdatum mit Tag und Monat legen?

Der Grund ist, daß unser Kalender am Sonnenjahr orientiert ist – die Kalendermonate fallen immer in die gleichen Jahreszeiten des Sonnenjahres – während die beweglichen Feste eine Mondkomponente haben: sie sind alle am Osterfest ausgerichtet, und dieses ist durch den Zusammenhang mit dem jüdischen Passahfest mit dem Mondkalender verknüpft – konkret: mit den Vollmondterminen.

Bewegliche Feiertage im Kirchenjahr

Der Termin für das Osterfest schwankt von Jahr zu Jahr, wobei er frühestens am 22. März und spätestens am 25. April liegen kann.

Mit dem von Jahr zu Jahr variierenden Ostertermin bewegt sich ein großer Teil des ganzen Kirchenjahres im Kalenderjahr: vom Sonntag Dominica in Septuagesima der Vorfastenzeit bis zum Fronleichnamsfest 60 Tage nach Ostern sind es 124 Tage, und auch das nachfolgende, bis zum November reichende tempus per annum post Pentecosten (die "nachpfingstliche Zeit im Jahr"), mit dem das Kirchenjahr ausklingt, hängt vom Ostertermin ab.

  • Am 9. Sonntag vor Ostern beginnt mit dem Tempus Septuagesimae die Vorfastenzeit (mit den Sonntagen Septuagesima, Sexagesima und Quinquagesima)
  • Mit dem Aschermittwoch, dem Mittwoch nach Quinquagesima, beginnt das Tempus Quadragesimae, die eigentliche Fastenzeit. Die Fastenzeit dauert vom Aschermittwoch bis zum Karsamstag und besteht somit genau aus 40 Werktagen und sechs Sonntagen (letztere sind im Gedenken an die Auferstehung Jesu vom Fasten ausgenommen). Der erste Sonntag der Fastenzeit heißt auch Quadragesima oder Invocabit (nach seinem Introitusvers).
  • Das sogenannte Triduum Paschale bezeichnet die mit dem Gründonnerstagabend beginnende, über Karfreitag und Karsamstag bis zum Ostersonntag reichende Zeit. Sie ist die bedeutendste, heiligste Zeit des ganzen Kirchenjahres und gilt, obwohl sie sich über drei Tage erstreckt, als ein einziges Hochfest.
  • Über die fünf Sonntage nach Ostern erstreckt sich das Tempus Paschatis, die eigentliche Osterzeit. Der fünfte Sonntag heißt auch Rogate, weil er die sogenannten “Kleinen Bittage” einläutet, die besonders dem Gebet gewidmet sein sollen und die dem Fest Christi Himmelfahrt vorausgehen.
  • In das mit Christi Himmelfahrt beginnende Tempus Ascensionis fällt genau ein Sonntag, Exaudi, der von der Sehnsucht nach dem Antlitz Gottes geprägt ist (quaesivi vultum tuum heißt es im Introitus - ich suche Dein Angesicht).
  • Mit dem 50. Tag nach Ostern, dem Pfingstsonntag, beginnt die Pfingstoktav (Octava Pentecostes), zugleich die achte und letzte Woche der österlichen Zeit, die mit dem nachfolgenden Dreifaltigkeitssonntag zu ihrem End- und Schlußpunkt kommt.
  • Im Dreifaltigkeitssonntag (Trinitatis) gipfeln die großen christlichen Feste Weihnachten, Ostern und Pfingsten zu ihrer unüberbietbaren Vollendung auf, münden sie doch in die geheimnisvolle Tiefe des unendlichen, dreifaltigen Gottes.
  • Die nun folgenden 23 bis 28 Wochen bis zum Beginn des nächsten Kirchenjahres, also bis zum nächsten 1. Advent, werden als Tempus per annum post Pentecosten bezeichnet. Es sind Wochen der Aussendung und des Glaubenszeugnisses. Im Evangelium des Dreifaltigkeitstags (Mt 28:18-20) sagt Jesus, daß ihm alle Gewalt gegeben ist im Himmel und auf Erden, und er beauftragt seine Jünger, also jeden Christen, zu allen Völkern der Erde zu gehen und sie in das Mysterium der Dreifaltigkeit einzutauchen. Schließlich verheißt er denen, die ihm nachfolgen, daß er bei ihnen bleiben wird bis zum Ende der Welt.
  • Am Donnerstag 60 Tage nach Ostern, schon im Tempus per annum post Pentecosten gelegen, wird das Fronleichnamsfest gefeiert. Auf eine Vision der heiligen Juliane von Lüttich zurückgehend (1209), spiegelt es das, was am Gründonnerstag eher innerlich gefeiert wurde – die Einsetzung des Allerheiligsten Altarsakraments durch Jesus Christus – um dieses Sakrament in einer feierlichen Prozession nach draußen zu tragen.
  • Am dritten Freitag nach Pfingsten, also 68 Tage nach Ostern, feiert die Kirche das Hochfest vom Heiligsten Herzen Jesu, aus dem die Liebe Gottes zu den Menschen entströmt - durch Seine Opfertat zu Golgotha ebenso wie durch die Sakramente Seiner Kirche. Dieser Festtag ragt schon weit in das tempus per annum post Pentecosten hinein.

Um diesen Zeitraum in das restliche, im Kalenderjahr fest bleibende Kirchenjahr einzufügen, läßt man einige der Sonntage nach Epiphanias (6. Januar) und vor dem Beginn der (beweglichen) Vorfastenzeit aus - und fügt sie dafür nach dem 22. Sonntag nach Pfingsten in die liturgische Ordnung ein.

Die zyklische Osterrechnung (computus)

Das zentrale bewegliche Fest, auf das die anderen beweglichen Festtage bezogen sind, ist Ostern, das Fest der Auferstehung Christi. Die Kreuzigung Christi fiel (so geht aus den Evangelien hervor) auf einen Freitag vor dem Beginn der jüdischen Passahfestwoche. Es muß sich also um einen Freitag gehandelt haben, der zugleich im jüdischen Kalender auf den 14. Tag des Frühlingsmonats Nisan fiel (denn am 15. Nisan beginnt bei den Juden das einwöchige Passahfest), vgl. O. Gerhardt (1930) für die Fragen der genauen Datierung der Kreuzigung Christi.

Angelehnt an diese Datierung hat man den Ostertermin folgendermaßen definiert:

Ostersonntag ist der erste Sonntag nach dem Tag des ersten "Vollmonds" ab "Frühlingsanfang".

Die Definition ist aber nicht naturalistisch: die Begriffe "Vollmond" und "Frühlingsanfang" sind nicht rein astronomisch, sondern kalenderarithmetisch bestimmt. Die rein astronomischen Ereignisse des Frühlingsanfangs und Vollmonds sind nur mit den Methoden der astronomischen Störungs- oder Mehrkörpertheorie exakt zu bestimmen, da die wechselseitigen Anziehungskräfte aller Himmelskörper unseres Sonnensystems zu berücksichtigen sind.

Kalender haben eine für den bürgerlichen Gebrauch vereinfachte, praktisch brauchbare tagesgenaue Systematik zu bieten, die an diesen natürlichen Ereignissen orientiert ist, ohne sie exakt reproduzieren zu müssen. Speziell für die Berechnung des Osterfestes gibt es eine eigene Spezialdisziplin in der traditionellen Chronologie, den sogenannten Computus.

Für die Osterberechnung arbeitet der Computus mit folgenden Vereinfachungen:

  • Die Regeln des von Papst Gregor XIII. (1572-1585) angeordneten gregorianischen Kalenders reproduzieren mit großer Genauigkeit die Länge des Sonnenjahrs, so daß man für die Zwecke der Osterrechnung als "Frühlingsanfang" den 21. März definiert.
  • Der erste "Vollmond" ab Frühlingsanfang wird von den Computisten Luna XIV genannt und ist ein Tag, den man zyklisch durch den Kalender wandern läßt. Die Zyklusregel ist, daß er in den 30 Tagen zwischen dem 21. März und dem 19. April von Jahr zu Jahr um 11 Tage zurückwandert (diese 11 Tage entsprechen dem Überschuß des Sonnenjahrs über das Mondjahr aus zwölf Mondmonaten), alle 19 Jahre aber sogar um 12 Tage (der sogenannte saltus lunae, Mondsprung).

    In der Sprache der Arithmetik ist diese Operation im Restklassenring modulo 30 (ℤ30) die Addition "+19". Nach 19 Jahren haben wir einen Verschub von 19·19=361=1 modulo 30, also einem Tag, der durch den Mondsprung auf 0 korrigiert wird. Der Termin Luna XIV wandert also nur durch genau 19 der 30 möglichen Kalenderdaten, solange nicht weitere Modifikationen dieser Regel dazukommen (und natürlich kommen welche dazu!). Wir sind mitten in einem 300 Jahre dauernden Zeitraum, in dem die reguläre Luna XIV nur auf einen der folgenden 19 Termine fällt: den 22., 23., 25., 27., 28., 30., 31. März sowie den 2., 3., 5., 7., 8., 10., 11., 13., 14., 16., 18. oder 19. April (wobei der 19. und 18. April aufgrund von Ausnahmeregeln je um einen Tag zurückdatiert werden).

In einem letzten Schritt nach Ermittlung von Luna XIV ist dann nur noch der auf sie folgende Sonntag zu ermitteln.

In der beigefügten Tabelle kann man die Wanderung von Luna XIV verfolgen (auf das Vorschaubild klicken, um in das Tabellenblatt zu navigieren):

  • Gelb markiert sind die Sonntage.
  • Mit einem "O" sind die aus dem Computus ermittelten Ostersonntage markiert.
  • Mit einem "L" und blauem Hintergrund ist der Luna XIV-Termin des betreffenden Jahres markiert
  • Hellblau sind Tage markiert, meist der 18. oder 19. April, auf die Luna XIV zwar nach rein zyklischer Rechnung fallen würde, die aber unter eine Ausnahmeregelung fallen, um einen zu späten Ostertermin verhindern: ein Ostersonntag am 26. April ist im Kirchenjahr nicht erwünscht, er liegt zu spät – und der 18. wird zurückdatiert, wenn er neben dem 19. als regulärer Termin von Luna XIV vorkommt, damit es im 19jährigen Zyklus immer 19 verschiedene Termine für Luna XIV gibt.
  • Mit einer anderen Farbe sind die Luna XIV-Termine vom 14. April markiert (1957, 1976 usw.), denn dies sind die Jahre, in denen der Mondsprung angewendet wurde und der 19jährige Zyklus wieder beginnt.
  • Mit einem gestrichelten orange Rand sind die Tage des astronomischen Frühlingsvollmonds markiert (Vollmonddatum in Ephemeridenzeit, der Datumswechsel ist also auf den Meridian von Greenwich bezogen).

Wer sich genauer für die lange und spannende Geschichte der Osterrechnung, des Computus, des gregorianischen Kalenders usw. interessiert, dem sei die Webpräsenz von Nikolaus A. Bär empfohlen. Man findet dort - neben sehr genauen Diskussionen vieler historischer Fragestellungen der Kalenderrechnung - auch Statistiken über die Verteilung der Ostertermine sowie Rechner, mit denen man zwischen dem gregorianischen, julianischen, jüdischen und islamischen Kalender umrechnen, Ostertermine berechnen oder suchen kann (also Fragen wie "in welchen Jahren fiel Ostern auf den 11. April?" beantworten kann).

Das Osterparádoxon 2019

Im Jahr 2019 fiel der Ostersonntag auf den 21. April. Der astronomische Frühlingsbeginn war am 20. März um etwa 22 Uhr Weltzeit. In der Frühe des 21. März, etwa um 1:43, wurde der Mond voll. Dies war astronomisch gesehen der Frühlingsvollmond. Würde man für die Ostertermine nur diesen natürlichen Ereignissen folgen, wäre der nächstfolgende Sonntag der Ostersonntag geworden, das war der 25. März.

Luna XIV war aber erst einen Monat später, am 19. bzw. 18. April (nach der erwähnten Ausnahmeregel auf den 18. April zurückdatiert). Der "Ostervollmond" im wahren Sinne des Wortes, der Vollmond vor Ostern, war also nicht der erste, sondern der zweite Frühlingsvollmond (der astronomisch am 19. April um 13 Uhr 20 eintrat). Der nächstfolgende Sonntag war Ostern: der 21. April.

Eine Abweichung des Ostertermins von dem Termin, den man nach einer "rein natürlichen" Berechnung mit dem astronomischen Frühlingsanfang und astronomischen Vollmond erwarten würde, nennt man ein Osterparádoxon. Die Osterparádoxa sind gut erforscht und klassifiziert. Die Abweichung des Jahres 2019 fällt demnach in die Kategorie A+ der sogenannten positiven Äquinoktialparádoxa, die aufgrund eines Unterschieds des astronomischen Frühlingsanfangs entstehen.

Neben diesen Äquinoktialparádoxa gibt es auch noch die sogenannten positiven (H+) und negativen (H-) Hebdomadalparádoxa, bei denen allein die Abweichung des astronomischen vom zyklischen Vollmond den Unterschied ergibt: Bei H- fällt Luna XIV auf einen Samstag, während der astronomische Vollmond am Sonntag folgt, bei H+ ist es umgekehrt.

Weitere Jahre mit solchen Abweichungen sind:

A+1590, 1666, 1685, 1924, 1943, 1962, 2019, 2038, 2057, 2076, 2095, 2114, 2133, 2152, 2171, 2190
H+1629, 1700, 1724, 1744, 1778, 1798, 1876, 1974, 2045, 2069, 2089, 2096
H-1598, 1609, 1622, 1693, 1802, 1805, 1818, 1825, 1829, 1845, 1900, 1903, 1923, 1927, 1954, 1967, 1981, 2049, 2076, 2106, 2119, 2133, 2147, 2150, 2170, 2174

Bei den Parádoxa vom Typ H+ und H- liegt der Fehler bei einer Woche: kirchliches Ostern ist eine Woche später oder früher als ein "rein astronomisch definiertes" Ostern wäre. Dagegen liegen bei den Osterparádoxa vom Typ A+ die Abweichungen bei etwa einem Monat.

Man sieht, daß sich das letzte Osterparádoxon im Jahre 1981 ereignete (vom Typ H-), und das nächste auf das Jahr 2038 fallen wird (wieder vom Typ A+).

Über eine Reform der Osterrechnung

Ein einheitliches Datum für das Osterfest, dieses zentralen Fest im ganzen Kirchenjahr, wäre wertvoll als ein äußeres Zeichen für die Bemühung der Christen um Einheit, die eines der Wesensmerkmale der Kirche ist. Dies hielt Papst Franziskus auf dem Rückflug vom Heiligen Land in einer Pressekonferenz fest (am 26.5.2014):
Ein anderes Thema, über das wir gesprochen haben, damit vielleicht im panorthodoxen Rat etwas getan werden kann, ist das Osterdatum, denn es ist ein bisschen lächerlich: – Sag mir, wann wird dein Christus auferstehen? – Nächste Woche – Meiner ist schon letzte Woche… – Ja, das Osterdatum ist ein Zeichen der Einheit.

Eine Reform der Osterrechnung müßte aber behutsam und mit großer Sensibilität gegenüber dem bisher Bestehenden erfolgen. Unser Zeitgeist neigt leider dazu, die Dinge allzu radikal umzukrempeln: oft wird in hemdsärmeliger Manier viel Sinnvolles, in Jahrhunderten Gewordenes und Gewachsenes um einer einzigen Idee willen zerstört, die man höher als alles andere wertet. Ein Vorbild für eine wirklich gute Reform stellt die Kalenderreform von Papst Gregor XIII. dar, die von Weitsicht, Umsicht und Traditionsbewußtsein zugleich getragen war.

Eine rein an den Naturvorgängen orientierte Definition (ein Vorschlag lautet zum Beispiel: "Ostern ist der erste Sonntag gemäß Jerusalemer Lokalzeit, der auf den ersten astronomischen Vollmond ab astronomischem Frühlingsanfang folgt") erscheint logisch und klar, wichtet aber gerade das Natürliche zu hoch. Es ist auch ein Fest des Gedächtnisses an jenen Ur-Karfreitag und aller Karfreitage, die seitdem auf diesen folgten. Die oben beschriebene Osterparadoxie des Jahres 2019 ergibt zwar einen aus "natürlicher" Sicht um einen Monat verspäteten Ostertermin. Andererseits fiel der Karfreitag 2019 bei dieser "unnatürlichen" Osterterminierung im jüdischen Kalender auf den 14. Nisan 5779, was wieder ein schöner Zusammenklang ist (auch ist es nicht selbstverständlich, daß der 14. Nisan auf einen Freitag fällt).

Paradoxien von 1950 bis 2050

Die folgende Tabelle zeigt die astronomischen Termine des Frühlingsanfangs sowie dreier in Frage kommender Vollmonde (die ab Mitte März gefundenen), dann den daraus ermittelten "astronomischen Ostertermin" und in der letzten Spalte den kirchlichen Ostertermin, falls dieser vom astronomischen abweicht. Alle Zeitangaben sind in Ephemeridenzeit, die von der Weltzeit UTC gegenwärtig um ca. eine Minute abweicht. Die Berechnungen erfolgten mit einem C-Programm, wobei für die kirchlichen Ostertermine die Gaußsche Osterformel verwendet wurde.

Frühling🌕🌕🌕Astr. O.Kirchl. O.
21.3.1950 4h35m2.4.1950 20h49m2.5.1950 5h19m31.5.1950 12h43m9.4.1950
21.3.1951 10h26m23.3.1951 10h50m21.4.1951 21h30m21.5.1951 5h45m25.3.1951
20.3.1952 16h14m10.4.1952 8h53m9.5.1952 20h16m8.6.1952 5h07m13.4.1952
20.3.1953 22h00m30.3.1953 12h55m29.4.1953 4h20m28.5.1953 17h03m5.4.1953
21.3.1954 3h53m19.3.1954 12h42m18.4.1954 5h48m17.5.1954 21h47m25.4.195418.4.1954
21.3.1955 9h35m7.4.1955 6h35m6.5.1955 22h14m5.6.1955 14h08m10.4.1955
20.3.1956 15h20m26.3.1956 13h11m25.4.1956 1h41m24.5.1956 15h26m1.4.1956
20.3.1957 21h16m16.3.1957 2h22m14.4.1957 12h09m13.5.1957 22h34m21.4.1957
21.3.1958 3h06m4.4.1958 3h45m3.5.1958 12h23m1.6.1958 20h55m6.4.1958
21.3.1959 8h55m24.3.1959 20h02m23.4.1959 5h13m22.5.1959 12h56m29.3.1959
20.3.1960 14h43m11.4.1960 20h27m11.5.1960 5h42m9.6.1960 13h02m17.4.1960
20.3.1961 20h32m1.4.1961 5h47m30.4.1961 18h41m30.5.1961 4h37m2.4.1961
21.3.1962 2h30m21.3.1962 7h55m20.4.1962 0h33m19.5.1962 14h32m25.3.196222.4.1962
21.3.1963 8h20m9.4.1963 0h57m8.5.1963 17h23m7.6.1963 8h31m14.4.1963
20.3.1964 14h10m28.3.1964 2h48m26.4.1964 17h50m26.5.1964 9h29m29.3.1964
20.3.1965 20h05m17.3.1965 11h24m15.4.1965 23h02m15.5.1965 11h52m18.4.1965
21.3.1966 1h53m5.4.1966 11h13m4.5.1966 21h01m3.6.1966 7h40m10.4.1966
21.3.1967 7h37m26.3.1967 3h21m24.4.1967 12h04m23.5.1967 20h22m2.4.196726.3.1967
20.3.1968 13h22m13.4.1968 4h52m12.5.1968 13h05m10.6.1968 20h13m14.4.1968
20.3.1969 19h08m2.4.1969 18h45m2.5.1969 5h14m31.5.1969 13h18m6.4.1969
21.3.1970 0h56m23.3.1970 1h53m21.4.1970 16h21m21.5.1970 3h38m29.3.1970
21.3.1971 6h38m10.4.1971 20h10m10.5.1971 11h24m9.6.1971 0h04m11.4.1971
20.3.1972 12h22m29.3.1972 20h06m28.4.1972 12h45m28.5.1972 4h28m2.4.1972
20.3.1973 18h13m18.3.1973 23h34m17.4.1973 13h51m17.5.1973 4h58m22.4.1973
21.3.1974 0h07m6.4.1974 21h01m6.5.1974 8h55m4.6.1974 22h10m7.4.197414.4.1974
21.3.1975 5h57m27.3.1975 10h36m25.4.1975 19h55m25.5.1975 5h51m30.3.1975
20.3.1976 11h50m14.4.1976 11h49m13.5.1976 20h04m12.6.1976 4h15m18.4.1976
20.3.1977 17h43m4.4.1977 4h09m3.5.1977 13h04m1.6.1977 20h31m10.4.1977
20.3.1978 23h34m24.3.1978 16h20m23.4.1978 4h11m22.5.1978 13h17m26.3.1978
21.3.1979 5h22m12.4.1979 13h15m12.5.1979 2h01m10.6.1979 11h56m15.4.1979
20.3.1980 11h10m31.3.1980 15h14m30.4.1980 7h36m29.5.1980 21h28m6.4.1980
20.3.1981 17h03m20.3.1981 15h23m19.4.1981 7h59m19.5.1981 0h04m26.4.198119.4.1981
20.3.1982 22h56m8.4.1982 10h19m8.5.1982 0h45m6.6.1982 16h00m11.4.1982
21.3.1983 4h39m28.3.1983 19h27m27.4.1983 6h31m26.5.1983 18h48m3.4.1983
20.3.1984 10h25m17.3.1984 10h10m15.4.1984 19h11m15.5.1984 4h29m22.4.1984
20.3.1985 16h14m5.4.1985 11h33m4.5.1985 19h53m3.6.1985 3h51m7.4.1985
20.3.1986 22h03m26.3.1986 3h02m24.4.1986 12h47m23.5.1986 20h45m30.3.1986
21.3.1987 3h52m14.4.1987 2h31m13.5.1987 12h51m11.6.1987 20h49m19.4.1987
20.3.1988 9h39m2.4.1988 9h22m1.5.1988 23h41m31.5.1988 10h54m3.4.1988
20.3.1989 15h29m22.3.1989 9h58m21.4.1989 3h14m20.5.1989 18h17m26.3.1989
20.3.1990 21h20m10.4.1990 3h19m9.5.1990 19h31m8.6.1990 11h02m15.4.1990
21.3.1991 3h02m30.3.1991 7h18m28.4.1991 20h59m28.5.1991 11h37m31.3.1991
20.3.1992 8h49m18.3.1992 18h18m17.4.1992 4h43m16.5.1992 16h03m19.4.1992
20.3.1993 14h41m6.4.1993 18h44m6.5.1993 3h34m4.6.1993 13h03m11.4.1993
20.3.1994 20h29m27.3.1994 11h10m25.4.1994 19h45m25.5.1994 3h40m3.4.1994
21.3.1995 2h15m17.3.1995 1h26m15.4.1995 12h09m14.5.1995 20h49m16.4.1995
20.3.1996 8h04m4.4.1996 0h07m3.5.1996 11h49m1.6.1996 20h47m7.4.1996
20.3.1997 13h55m24.3.1997 4h46m22.4.1997 20h34m22.5.1997 9h14m30.3.1997
20.3.1998 19h55m11.4.1998 22h24m11.5.1998 14h30m10.6.1998 4h19m12.4.1998
21.3.1999 1h46m31.3.1999 22h49m30.4.1999 14h55m30.5.1999 6h40m4.4.1999
20.3.2000 7h36m20.3.2000 4h45m18.4.2000 17h42m18.5.2000 7h35m23.4.2000
20.3.2001 13h31m8.4.2001 3h22m7.5.2001 13h53m6.6.2001 1h40m15.4.2001
20.3.2002 19h17m28.3.2002 18h25m27.4.2002 3h00m26.5.2002 11h52m31.3.2002
21.3.2003 1h00m18.3.2003 10h35m16.4.2003 19h36m16.5.2003 3h37m20.4.2003
20.3.2004 6h49m5.4.2004 11h03m4.5.2004 20h34m3.6.2004 4h20m11.4.2004
20.3.2005 12h34m25.3.2005 20h59m24.4.2005 10h07m23.5.2005 20h19m27.3.2005
20.3.2006 18h26m13.4.2006 16h41m13.5.2006 6h52m11.6.2006 18h04m16.4.2006
21.3.2007 0h08m2.4.2007 17h16m2.5.2007 10h10m1.6.2007 1h04m8.4.2007
20.3.2008 5h49m21.3.2008 18h41m20.4.2008 10h26m20.5.2008 2h12m23.3.2008
20.3.2009 11h44m9.4.2009 14h56m9.5.2009 4h02m7.6.2009 18h12m12.4.2009
20.3.2010 17h33m30.3.2010 2h26m28.4.2010 12h19m27.5.2010 23h08m4.4.2010
20.3.2011 23h21m19.3.2011 18h11m18.4.2011 2h45m17.5.2011 11h09m24.4.2011
20.3.2012 5h15m6.4.2012 19h19m6.5.2012 3h36m4.6.2012 11h12m8.4.2012
20.3.2013 11h03m27.3.2013 9h28m25.4.2013 19h58m25.5.2013 4h26m31.3.2013
20.3.2014 16h58m16.3.2014 17h09m15.4.2014 7h43m14.5.2014 19h17m20.4.2014
20.3.2015 22h46m4.4.2015 12h06m4.5.2015 3h43m2.6.2015 16h20m5.4.2015
20.3.2016 4h31m23.3.2016 12h01m22.4.2016 5h24m21.5.2016 21h15m27.3.2016
20.3.2017 10h29m11.4.2017 6h09m10.5.2017 21h43m9.6.2017 13h10m16.4.2017
20.3.2018 16h16m31.3.2018 12h37m30.4.2018 0h59m29.5.2018 14h20m1.4.2018
20.3.2019 21h59m21.3.2019 1h44m19.4.2019 11h13m18.5.2019 21h12m24.3.201921.4.2019
20.3.2020 3h50m8.4.2020 2h36m7.5.2020 10h46m5.6.2020 19h13m12.4.2020
20.3.2021 9h38m28.3.2021 18h49m27.4.2021 3h32m26.5.2021 11h15m4.4.2021
20.3.2022 15h34m18.3.2022 7h18m16.4.2022 18h56m16.5.2022 4h15m17.4.2022
20.3.2023 21h25m6.4.2023 4h35m5.5.2023 17h35m4.6.2023 3h42m9.4.2023
20.3.2024 3h07m25.3.2024 7h01m23.4.2024 23h50m23.5.2024 13h54m31.3.2024
20.3.2025 9h02m13.4.2025 0h23m12.5.2025 16h57m11.6.2025 7h44m20.4.2025
20.3.2026 14h47m2.4.2026 2h13m1.5.2026 17h24m31.5.2026 8h46m5.4.2026
20.3.2027 20h25m22.3.2027 10h44m20.4.2027 22h28m20.5.2027 11h00m28.3.2027
20.3.2028 2h18m9.4.2028 10h27m8.5.2028 19h50m7.6.2028 6h09m16.4.2028
20.3.2029 8h03m30.3.2029 2h27m28.4.2029 10h37m27.5.2029 18h38m1.4.2029
20.3.2030 13h53m19.3.2030 17h57m18.4.2030 3h21m17.5.2030 11h20m21.4.2030
20.3.2031 19h42m7.4.2031 17h22m7.5.2031 3h41m5.6.2031 11h59m13.4.2031
20.3.2032 1h23m27.3.2032 0h47m25.4.2032 15h10m25.5.2032 2h38m28.3.2032
20.3.2033 7h23m16.3.2033 1h38m14.4.2033 19h18m14.5.2033 10h43m17.4.2033
20.3.2034 13h18m3.4.2034 19h20m3.5.2034 12h16m2.6.2034 3h55m9.4.2034
20.3.2035 19h03m23.3.2035 22h43m22.4.2035 13h21m22.5.2035 4h26m25.3.2035
20.3.2036 1h04m10.4.2036 20h23m10.5.2036 8h10m8.6.2036 21h03m13.4.2036
20.3.2037 6h51m31.3.2037 9h54m29.4.2037 18h55m29.5.2037 4h25m5.4.2037
20.3.2038 12h41m21.3.2038 2h10m19.4.2038 10h37m18.5.2038 18h24m28.3.203825.4.2038
20.3.2039 18h33m9.4.2039 2h53m8.5.2039 11h21m6.6.2039 18h48m10.4.2039
20.3.2040 0h12m28.3.2040 15h12m27.4.2040 2h39m26.5.2040 11h48m1.4.2040
20.3.2041 6h08m17.3.2041 20h20m16.4.2041 12h01m16.5.2041 0h53m21.4.2041
20.3.2042 11h54m5.4.2042 14h17m5.5.2042 6h49m3.6.2042 20h49m6.4.2042
20.3.2043 17h29m25.3.2043 14h27m24.4.2043 7h24m23.5.2043 23h38m29.3.2043
19.3.2044 23h21m12.4.2044 9h40m12.5.2044 0h17m10.6.2044 15h17m17.4.2044
20.3.2045 5h08m1.4.2045 18h44m1.5.2045 5h53m30.5.2045 17h53m2.4.20459.4.2045
20.3.2046 10h59m22.3.2046 9h28m20.4.2046 18h22m20.5.2046 3h16m25.3.2046
20.3.2047 16h53m10.4.2047 10h36m9.5.2047 18h25m8.6.2047 2h06m14.4.2047
19.3.2048 22h35m30.3.2048 2h05m28.4.2048 11h14m27.5.2048 18h58m5.4.2048
20.3.2049 4h29m19.3.2049 12h24m18.4.2049 1h05m17.5.2049 11h15m25.4.204918.4.2049
20.3.2050 10h20m7.4.2050 8h13m6.5.2050 22h27m5.6.2050 9h52m10.4.2050