Entwurfsprinzip: Best Effort

von Hubert Schmid vom 2014-01-12

Der Ausdruck Best Effort steht in der Informatik für das Prinzip der Wertsteigerung durch bewusst geringere Güte aufgrund beschränkter Kapazitäten. Das hört sich kompliziert an, ist aber ganz einfach. Ein paar Beispiele:

Dieses Prinzip hat sich in vielen Bereichen bewährt. Bei der Konzeption von Softwarearchitekturen wird es jedoch häufig vernachlässigt. Dabei kann es gerade in diesem Bereich viele Probleme vereinfachen. Genau das möchte ich anhand eines Beispiels verdeutlichen.

Man stelle sich einen Webservice vor, der die Abspielposition eines Musiktitels oder Hörbuchs speichert, damit man die Wiedergabe einfach von der letzten Position auf einem anderen Gerät fortsetzen kann. Dazu übergibt der Client dem Webservice alle fünf Sekunden die aktuelle Position. Die Implementierung des Webservices in Java ist im folgenden Listing skizziert. Die Variable persistence steht dabei für eine Persistenzschicht zur Kapselung der Datenbankzugriffe.

class PlaybackPositionService { public void setPlaybackPosition(String userId, PlaybackPosition position) { persistence.storePlaybackPosition(userId, position); } public PlaybackPosition getPlaybackPosition(String userId) { return persistence.loadPlaybackPosition(userId); } }

Bei solchen Implementierungen ist in der Regel die Datenbank der begrenzende Faktor. Anstatt nun das Datenbanksystem mit der Anzahl der Nutzer zu skalieren, sollte man sich in diesem Fall überlegen, ob wirklich jede Aktualisierung direkt persistiert werden muss. Eine Alternative wäre, die Änderungen zunächst im Hauptspeicher durchzuführen und asynchron im Hintergrund zu speichern. Die beiden obigen Methoden sähen dann wie folgt aus.

private Map<String, PlaybackPosition> pending = new LinkedHashMap<>(); public void setPlaybackPosition(String userId, PlaybackPosition position) { synchronized (pending) { pending.put(userId, position); pending.notify(); } } public PlaybackPosition getPlaybackPosition(String userId) { synchronized (pending) { if (pending.containsKey(userId)) { return pending.get(userId); } } return persistence.loadPlaybackPosition(userId); }

Diese Implementierung hat natürlich den Nachteil, dass einige Daten beim Absturz der Anwendung verloren gehen. Andererseits ist sie um Größenordnungen schneller. Wenn man sich die Relevanz der Daten vor Augen hält, und gleichzeitig die Häufigkeit unkontrollierter Programmabbrüche berücksichtigt, ist eine Abwägung zwischen Performance und Persistenz also durchaus sinnvoll.

Es fehlt noch die Funktion für die Hintergrundspeicherung. Dank der LinkedHashMap ist sie vergleichsweise einfach zu realisieren. Die Methode processPending wird von einem oder mehreren Threads ausgeführt. Sobald ein neuer Eintrag hinzukommt wird ein Thread aufgeweckt, um das Element aus der LinkedHashMap zu entfernen und in der Datenbank zu speichern. Die Implementierung ist im folgenden Listing zu sehen. Dabei ist lediglich die Ausnahmebehandlung für produktiven Einsatz noch zu verfeinern.

private void processPending() { for (;;) { Map.Entry<String, PlaybackPosition> entry; synchronized (pending) { while (pending.isEmpty()) { pending.wait(); } entry = poll(pending.entrySet().iterator()); } persistence.storePlaybackPosition(entry.getKey(), entry.getValue()); } } private <T> T poll(Iterator<T> iterator) { T element = iterator.next(); iterator.remove(); return element; }

Das Beispiel soll zum Nachdenken anregen. Beim Softwareentwurf gehen viele Entwickler implizit davon aus, dass einige Qualitätsmerkmale – wie beispielsweise die Persistenz – unabdingbar sind. In vielen Fällen lohnt es sich jedoch, genau solche Anforderungen zu hinterfragen. Am Ende zählt nicht die Perfektion im technischen Detail sondern das Gesamtergebnis. Und das ist manchmal für alle Beteiligten besser, wenn man an einzelnen Stellen Abstriche macht.