Events zur Umsetzung einer Finanzverwaltung (Teil 4)

Im letzten Teil habe ich die kleine Finanzverwaltung mit Hilfe einer relationalen Datenbank und der Java Persistence API realisiert. Diesmal möchte ich eine Alternative zu diesem gängigen Setup umsetzen. Ich versuche eine Umsetzung mit Events, die das Programm einfach in einer Datei speichert.

Warum eine Alternative mit Events?

Die Programmierung gegen eine Datenbank hat aufgrund der “objektrelationalen Unverträglichkeit” ihre Tücken. Erweiterungen z.B. Vererbungen machen Abfragen komplex und machmal langsamer.

Für die heimische Applikation ist eine Datenbank möglicherweise übertrieben. Zumal die die Datenmenge ist überschaubar ist. Sind es in einem Monat 25 Buchungen, so scheint mir das viel zu sein. Auf Sicht von 10 Jahren kommen so gerade mal 3000 Buchungen zusammen. Die passen heutzutage entspannt in den Speicher. Folglich reicht es die Buchungen in einer Datei zu speichern und beim Programmstart wieder zu lesen.

Dieses Vorgehen ist auch Event Sourcing, wofür Buchhaltung ein Paradebeispiel ist. Die Buchungen bilden die Kontosalden, praktisch der Status des Systems zu einem Zeitpunkt. Die Finanzverwaltung kann ihn durch erneutes “abspielen” der Buchungen jederzeit wieder herstellen.

Ein Status kann als Snapshot gespeichert sein. Dieser historischer Zustand – in diesem Beispiel die Kontostände – ist dann schnell gelesen, ohne alle Buchungen erneut lesen zu müssen. Architektonisch geht dieser Ansatz in Richtung CQRS.

Ein “Events” basiertes System: Redux

In der Webprogrammierung gibt es mit Redux ein Event-basiertes Vorgehen, um den Status im Client zentral zu verwalten. Mir gefällt dieser Ansatz sehr gut, da er das Event-Prinzip sehr schlicht umsetzt.

Eine Action ist der Anstoß zu einer Änderung des Status. Den ermittelt der Reducer, bei dem es sich um eine Funktion handelt. Die Argumente dieser Funktion sind die Action und der aktuelle State. Das Ergebnis ist ein neuer State. Dieser berücksichtigt die Änderungen, die eine Action auslöst.

Redux-Prinzip

Die Action ist ein unveränderliches Objekt, welches für die Umsetzung erforderliche Daten enthalten kann.
Wie bereits erwähnt ist der Reducer eine Funktion. Ein definierter Status führt mit der gleichen Action zu einem immer gleichen neuen State, wodurch der Reducer leicht testbar ist.
Speichere ich also die Actions und ermittle – ausgehend von einem initialen Status – sukzessive den aktuellen Status, betreibe ich mit wenigen Elementen letztlich nichts anderes als Event Sourcing.

Die Schnittstelle für die Programmierung ist der Store.

Der Store speichert den State und stellt die Dispatch-Operation bereit. Sie ermittelt mit Hilde des Reducers und der übergebenen Action den neuen State und ersetzt den alten.
Der Stores ermöglicht dem Programmierer auch Zugriff auf den Status. Ebenfalls bietet der Store das verfolgen von Änderungen an (Observer-Pattern). In Javascript-artigen Sprachen steht dafür die Möglichkeit Fallback-Funktionen zu registrieren (“subscribe()”). Die Java-Konvention ist dagegen eher das registrieren eines Listeners.

In Webclient-Implementierungen lösen die Abarbeitung von Actions sogenannte Effekte aus. Die setzen dort Seiteneffekte um, insbesondere der Zugriff auf ein Backend. In der Finanzverwaltung spielt das erst mal keine Rolle, denn das Redux-Prinzip soll ja gerade im Backend umgesetzt werden.

Eine einfache Redux-Implementierung

Als Programmierschnittstelle dient ein Store, dessen Schnittstelle ich – wie zuvor beschrieben – erst mal allgemein definiere:

public abstract class Store<T extends State> {

	protected T state;
	protected List<Listener> listeners = new ArrayList<>();
	
	public Store(StateInitializer<T> initializer) {
		state = initializer.initialState(this);
	}
	
	public abstract void dispatch(Action<?> action);

	public T getState() {
		return state;
	}
	
	public void addListener(Listener listener) {
		listeners.add(listener);
	}
}

Der erste Teil dieser Reihe hat drei Anwendungsfälle beschrieben. Buchungen zu erstellen ist die einzige schreibende Funktion. Sie ist als Action zu implementieren, da sie den Status der Anwendung ändert.

Die Umsetzung erfolgt in der Dispatch-Methode. Sie könnte z.B. generisch registrierten Reducer-Funktionen weiterleiten. Die damit verbundene Komplexität ist für eine erste Implementierung jedoch unnötig.

	@Override
	public void dispatch(Action<?> action) {
		switch (action.getType()) {
		case BuchenAction.TYPE:
			state = BuchhaltungReducer.buche(state, (BuchenAction)action);
			break;
		default:
			throw new IllegalArgumentException("ungekannte Action: "+action.getType());
		}
		listeners.forEach( listener -> listener.fireAction(action) );
	}

Der BuchhaltungState unterscheidet zwischen Grundbuch und Hauptbuch, wodurch die typische Sicht auf die Buchungen und Konten gegeben sind. Diese sind Listen und zeigen auf Objekte, deren Relationen wiederum auf Objekte zeigen. Durch diese Pointer liegen die Konten und Buchungen nur einmal miteinander verknüpft im Speicher vor. Deshalb entfallen Redundanzen und die Speicherbelastung ist vergleichsweise gering.
Der State ist als unveränderliches Objekt vorgesehen. Im Ideal sollte ein neuer State eine Kopie mit Änderungen sein. Zumindest ist dieses Vorgehen sinnvoll, wenn der Store parallel Angesteuert würde.

Im Code der Dispatch-Methode ist die statische Reducer-Funktion zu erkennen. Sie übernimmt eine Buchung aus der Action, baut alle Relationen zu den beteiligten Objekten auf und sorgt für die Anpassung der Salden. Die Programmierung für den Prototyp ist dabei recht “schmutzig”:

	public static BuchhaltungState buche(BuchhaltungState state, Action<?> action) {
		BuchenAction buchenAction = (BuchenAction)action;
		Buchung buchung = buchenAction.getPayload();

		state.getGrundbuch().add(buchung);
		
		buchung.getUmsaetze().stream().forEach( umsatz -> {
			umsatz.setBuchung(buchung);
			
			Konto konto;
			try {
				konto = selectKontoByName(state, umsatz.getKonto().getName());
			} catch (UnkownEntityException e) {
				konto = new Konto();
				konto.setName(umsatz.getKonto().getName());
				state.getHauptbuch().add(konto);
			}
			konto.verbuche(umsatz);
		});
		
		return state;
	}

Einmal erzeugt sie den State nicht neu. Das wäre technisch aufwendig und würde den Code erst mal nur aufblähen.
Nicht vorhandene Konten legt die Methode direkt an. Eine konsequente Umsetzung müsste mit eine “Konto-Anlegen-Action” arbeiten, wobei diese wiederum fachlich zu hinterfragen ist. Diese einfache Implementierung ist erst mal ausreichend, um mit der relationalen Implementierung verglichen zu werden.
BuchenAction sind wieder unveränderliches Objekte, die eine Buchung aufnehmen. Die Action verwendet Modell-Klassen. Die Buchung bildet die Spitze einer Objekt-Hierarchie. Sie ist leicht zu serialisieren, indem über diesen Baum iteriert wird.

Aus dem Status sind nun die Konten und deren Buchungen zu lesen, um lesenden Anwendungsfälle zu realisieren. Filter sind wieder statische Funktionen, die den Status und ggf. Parameter erhalten.

	public static Konto selectKontoByName(BuchhaltungState state, String kontoName) {
		return state.getHauptbuch().stream()
				.filter( konto -> konto.getName().equals(kontoName))
				.findFirst()
				.orElseThrow( () -> new UnkownEntityException("Konto mit Namen: "+kontoName));
	}
	
	public static List<Konto> selectKonten(BuchhaltungState state) {
		return state.getHauptbuch();
	}
	
	public static List<Buchung> selectBuchungByKontoName(BuchhaltungState state, String kontoName) {
		return selectKontoByName(state, kontoName)
			.getUmsaetze().stream()
			.map( umsatz -> umsatz.getBuchung())
			.collect(Collectors.toList());
	}

Redux in die API einbauen

Der Controller benötigt einen Store. Mit Filter und Action bildet er dann die API ab.

	@GetMapping("konto")
	public List<Konto> findAllKonten() {
		return kontoMapper.domainKontoListToApiKontoList(
				selectKonten(store.getState()));
	}
	
	@GetMapping("konto/{kontoName}/umsatz")
	public List<Buchung> findKontoBuchungenByKontoName(@PathVariable("kontoName") String kontoName) {
		return buchungMapper.domainBuchungListToApiBuchungListe(
				selectBuchungByKontoName(store.getState(), kontoName));
	}

	@PostMapping("buche")
	public ResponseEntity<String> buche(@RequestBody Buchung buchung) {
		
		try {
			store.dispatch(
					BuchhaltungActions.buche(
							buchungMapper.apiBuchungToDomainBuchung(buchung)));
		} catch (Exception e) {
			return ResponseEntity.unprocessableEntity().body("Fehler: " + e.getMessage());
		}
		
		return ResponseEntity.accepted().body("Verarbeitet");
	}

Speichern und Laden der Daten

Der Store ist die zentrale Redux-API. Er realisiert auch das speichern und lesen von Daten. Für den BuchhaltungStore ist ein LogListener pflicht. Dieser hängt eine ausgeführte Action an eine Action-Datei an.

Sobald der Store intern initialisiert ist, liest er diese Datei ein und führt die gelesenen Actions aus, wodurch er den bei Programmende erreichten Status wieder hergestellt.

finally

Nun existieren zwei Prototypen für eine Finanzverwaltung, die es zu vergleichen und bewerten gilt. Davon beim nächsten mal mehr.

Happy coding!

 

 

 

Schreibe einen Kommentar

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.