Android Coden
Android 7 min lesen

Strategy Pattern in Android: Verhalten austauschbar machen

Du lernst, wie das Strategy Pattern Verhalten kapselt. So bleibt Android-Code testbar und erweiterbar.

Das Strategy Pattern hilft dir, Verhalten auszutauschen, ohne überall if, when oder Sonderfälle zu verteilen. In Android ist das besonders nützlich, wenn dieselbe App je nach Kontext andere Regeln braucht: eine andere Sortierung, eine andere Eingabeprüfung, eine andere Retry-Policy oder eine andere Entscheidung vor dem Speichern von Daten.

Was ist das?

Das Strategy Pattern ist ein Entwurfsmuster, bei dem du eine Aufgabe nicht fest in eine Klasse einbaust, sondern hinter eine gemeinsame Schnittstelle legst. Jede konkrete Strategie implementiert diese Schnittstelle anders. Der aufrufende Code kennt nur den Vertrag, nicht die Details.

Das mentale Modell ist: Du trennst das „Was soll passieren?“ vom „Wie genau wird es entschieden?“. Eine Liste soll sortiert werden, aber die Sortierregel kann wechseln. Ein Formular soll geprüft werden, aber die Prüfregel kann je nach Feld oder Produktvariante anders sein. Eine Netzwerkoperation soll wiederholt werden, aber die Wartezeiten und Abbruchbedingungen können verschieden sein.

Im Android-Kontext passt dieses Muster gut zu Kotlin, ViewModels, Repositorys und Use Cases. Du kannst zum Beispiel eine Validierungsstrategie in ein ViewModel injizieren, statt die Regeln direkt in die UI zu schreiben. In Jetpack Compose bleibt deine Composable dadurch schlank: Sie zeigt Zustand an und sendet Events, während die Strategie außerhalb entscheidet, ob ein Wert gültig ist oder wie Daten sortiert werden.

Wichtig ist die Grenze des Musters: Eine Strategie ist kein allgemeiner Ersatz für Architektur. Sie löst ein konkretes Problem: austauschbares Verhalten, oft als Policy oder Regel bezeichnet. Wenn sich nur ein Wert ändert, brauchst du keine Strategie. Wenn sich aber ein kompletter Entscheidungsalgorithmus ändern kann, ist das Pattern oft passend.

Wie funktioniert es?

Das Pattern besteht meistens aus drei Teilen. Erstens definierst du eine Schnittstelle oder ein fun interface, das den gemeinsamen Vertrag beschreibt. Zweitens implementierst du mehrere konkrete Strategien. Drittens bekommt der nutzende Code eine Strategie über den Konstruktor, eine Factory oder eine Dependency-Injection-Konfiguration.

In Kotlin ist das oft sehr kompakt. Für einfache Fälle reicht ein fun interface, weil es genau eine abstrakte Funktion hat. Für komplexere Regeln mit mehreren Methoden kann ein normales interface besser lesbar sein.

Eine typische Struktur sieht so aus:

  • ValidationStrategy: definiert, wie ein Text geprüft wird.
  • EmailValidationStrategy: prüft E-Mail-Adressen.
  • PasswordValidationStrategy: prüft Passwortregeln.
  • ProfileViewModel: nutzt eine Strategie, kennt aber nicht jede konkrete Regel.

Der Vorteil liegt nicht nur in schönerem Code. Das Muster verbessert auch Testbarkeit und Qualität. Die Android-Testdokumentation betont, dass du Logik möglichst isoliert prüfen solltest. Eine Strategie ist dafür ein guter Kandidat: Du kannst sie ohne Activity, Fragment oder Compose-UI testen. In Continuous Integration laufen solche lokalen Tests schnell und geben dir früh Rückmeldung, wenn eine Regel kaputtgeht.

Im Alltag taucht das Pattern oft unscheinbar auf. Du übergibst einen Comparator, eine Retry-Regel, einen Parser, eine Filterfunktion oder eine Validierung. Nicht jeder dieser Fälle muss als Klasse modelliert werden. Kotlin erlaubt dir auch Lambdas. Entscheidend ist nicht die Form, sondern die Idee: Der aufrufende Code soll nicht wissen müssen, welche konkrete Regel gerade aktiv ist.

Eine gute Strategy hat klare Eingaben und klare Ausgaben. Sie sollte möglichst wenig versteckten Zustand besitzen. Wenn eine Strategie selbst Datenbankzugriffe, Navigation und UI-Zustand mischt, ist sie zu groß. Dann wird sie schwer testbar und du verlierst den eigentlichen Nutzen des Musters.

In der Praxis

Stell dir vor, du baust eine kleine Aufgabenliste. Nutzer können Aufgaben nach Fälligkeit, Priorität oder Titel sortieren. Eine schnelle Lösung wäre ein großes when direkt im ViewModel. Das funktioniert am Anfang, wird aber unübersichtlich, sobald weitere Sortierungen, Feature-Flags oder Tests dazukommen.

Mit dem Strategy Pattern kapselst du jede Sortierregel:

data class Task(
    val title: String,
    val dueAtMillis: Long?,
    val priority: Int
)

fun interface TaskSortStrategy {
    fun sort(tasks: List<Task>): List<Task>
}

class SortByDueDate : TaskSortStrategy {
    override fun sort(tasks: List<Task>): List<Task> =
        tasks.sortedWith(
            compareBy<Task> { it.dueAtMillis == null }
                .thenBy { it.dueAtMillis }
        )
}

class SortByPriority : TaskSortStrategy {
    override fun sort(tasks: List<Task>): List<Task> =
        tasks.sortedByDescending { it.priority }
}

class SortByTitle : TaskSortStrategy {
    override fun sort(tasks: List<Task>): List<Task> =
        tasks.sortedBy { it.title.lowercase() }
}

class TaskListViewModel(
    private var sortStrategy: TaskSortStrategy
) : ViewModel() {

    private val tasks = MutableStateFlow<List<Task>>(emptyList())

    val sortedTasks: StateFlow<List<Task>> =
        tasks
            .map { sortStrategy.sort(it) }
            .stateIn(
                viewModelScope,
                SharingStarted.WhileSubscribed(5_000),
                emptyList()
            )

    fun changeSortStrategy(strategy: TaskSortStrategy) {
        sortStrategy = strategy
        tasks.update { it.toList() }
    }
}

Dieses Beispiel zeigt die Grundidee, nicht die einzige mögliche Architektur. In einer echten App würdest du die Auswahl der Sortierung wahrscheinlich als Zustand modellieren und die passende Strategie über eine Factory oder Dependency Injection bereitstellen. Wichtig ist: Die Sortierlogik steht nicht in der Compose-UI. Eine Composable sollte nicht wissen, ob null-Fälligkeiten oben oder unten stehen. Sie sollte nur die bereits sortierten Daten anzeigen und Nutzeraktionen melden.

Eine passende Compose-Schicht könnte so denken: Der Nutzer tippt auf „Priorität“, das UI sendet ein Event an das ViewModel, und das ViewModel wählt die passende Strategie. Dadurch bleibt die UI stabil, auch wenn sich die Sortierregel später ändert.

Für Tests ist das angenehm. Du kannst jede Strategie direkt prüfen:

class SortByPriorityTest {

    @Test
    fun highestPriorityComesFirst() {
        val tasks = listOf(
            Task("Normal", dueAtMillis = null, priority = 1),
            Task("Wichtig", dueAtMillis = null, priority = 5)
        )

        val result = SortByPriority().sort(tasks)

        assertEquals("Wichtig", result.first().title)
    }
}

Solche Tests passen gut zu Android-Qualitätspraktiken, weil sie schnell laufen und konkrete Regeln absichern. Wenn du später Continuous Integration nutzt, sollten diese lokalen Tests bei jedem Pull Request laufen. So merkst du früh, ob eine Änderung an einer Policy unerwartete Folgen hat.

Eine praktische Entscheidungsregel lautet: Verwende das Strategy Pattern, wenn mindestens zwei echte Varianten desselben Verhaltens existieren und der aufrufende Code von den Details entkoppelt bleiben soll. Verwende es nicht nur, weil eine Methode lang wirkt. Manchmal reicht eine private Hilfsfunktion oder ein klarer when-Block.

Eine typische Stolperfalle ist Überabstraktion. Wenn du für jede winzige Bedingung sofort eine neue Strategie baust, entsteht zu viel indirekter Code. Anfänger verlieren dann den Überblick, weil die eigentliche Regel auf mehrere Dateien verteilt ist. Das Pattern lohnt sich erst, wenn der Austausch des Verhaltens ein echter Teil des Designs ist.

Eine zweite Falle ist ein undichter Vertrag. Wenn der aufrufende Code nach der Strategie fragt und danach trotzdem konkrete Typen prüft, ist die Abstraktion beschädigt:

if (strategy is SortByPriority) {
    // Sonderlogik
}

Solcher Code zeigt meist, dass die Schnittstelle nicht vollständig ist oder dass du zwei verschiedene Verantwortlichkeiten vermischt hast. Besser ist, den Vertrag so zu gestalten, dass der Aufrufer die konkrete Klasse nicht kennen muss.

Achte außerdem auf Zustand. Eine Retry-Strategie, die interne Zähler, Zeitmessung und Netzwerkdetails hält, kann korrekt sein, muss aber sehr bewusst getestet werden. Für viele Android-Fälle ist eine zustandsarme Strategie leichter zu verstehen: Eingabe rein, Ergebnis raus. Wenn Nebenwirkungen nötig sind, sollten sie klar benannt und begrenzt sein.

Im Code-Review kannst du gezielt nach drei Fragen suchen: Gibt es austauschbares Verhalten? Ist die gemeinsame Schnittstelle klein und verständlich? Kann ich jede Strategie unabhängig testen? Wenn du diese Fragen mit Ja beantworten kannst, ist das Pattern wahrscheinlich sauber eingesetzt.

Fazit

Das Strategy Pattern ist ein nüchternes Werkzeug für austauschbares Verhalten in Android-Code. Du nutzt es, wenn Regeln wie Sortierung, Validierung oder Retry-Logik variieren sollen, ohne dass du Verzweigungen über ViewModels, Repositorys und UI verteilst. Prüfe dein Verständnis aktiv: Nimm eine Stelle mit mehreren when-Zweigen, extrahiere zwei echte Strategien, schreibe lokale Tests dafür und besprich im Code-Review, ob die Abstraktion den Code klarer gemacht hat.

Quellen (3)
Redaktion

Geschrieben von

Redaktion

Das Redaktionsteam recherchiert und schreibt Artikel zu aktuellen Themen rund um Tech, Lifestyle und Ratgeber.