Entwurfsprinzip: Best Effort
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:
- Das Internetprotokoll verzichtet bewusst auf zuverlässige Paketzustellung, um die Leitungskapazitäten effizienter zu nutzen und die Komplexität des Protokolls zu reduzieren.
- Viele NoSQL-Datenbanksysteme nehmen Inkonsistenzen zu Gunsten der Verfügbarkeit gezielt in Kauf.
- Heuristische und Approximationsalgorithmen optimieren die Laufzeit für Berechnungen zu Lasten der Genauigkeit.
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.