Android Coden
Android 5 min lesen

Architektur-Smells erkennen

Architektur-Smells sind Warnsignale im Code, bevor echte Probleme entstehen. Du lernst, God-Classes, Context-Leaks und enge Kopplung früh zu erkennen.

Wenn eine App wächst, schleichen sich strukturelle Probleme ein, die zunächst unsichtbar bleiben. Diese Warnsignale nennen Architekten Smells – kein akuter Fehler, aber ein Zeichen, dass sich der Code in eine Richtung entwickelt, aus der man schwer wieder herauskommt. Drei Smells tauchen in Android-Projekten besonders häufig auf: God-Classes, Context-Leaks und enge Kopplung. Wer sie früh erkennt, verhindert, dass aus kleinen Designschwächen schwer wartbare Monolithen werden.

Was ist das?

Ein Architecture Smell ist ein Muster im Code, das auf ein tiefer liegendes Designproblem hinweist. Der Begriff lehnt sich an Code Smells an, bezieht sich aber auf die übergeordnete Struktur einer App – also wie Klassen, Module und Schichten miteinander kommunizieren, nicht auf einzelne Methoden oder Variablennamen.

God-Classes sind Klassen, die zu viele Verantwortlichkeiten übernehmen. Im Android-Kontext passiert das oft mit Activities oder Fragmenten: Netzwerkaufrufe, Datenbankzugriffe, UI-Logik und Validierungsregeln landen alle in derselben Datei. Mit der Zeit wird die Klasse so groß, dass niemand mehr den Überblick hat – und jede Änderung riskiert, eine andere Stelle zu brechen.

Context-Leaks entstehen, wenn eine Android-Context-Referenz – meistens eine Activity – in einem Objekt mit längerer Lebensdauer gespeichert wird, etwa in einem Singleton oder einem statischen Feld. Der Garbage Collector kann die Activity dann nicht freigeben, obwohl sie längst zerstört wurde. Das Ergebnis ist ein schleichender Memory-Leak, der sich erst unter Last oder nach längerem Betrieb zeigt.

Enge Kopplung bedeutet, dass zwei Komponenten direkt voneinander abhängen, statt über Abstraktionen zu kommunizieren. Wenn ein ViewModel einen konkreten Repository-Typ direkt instanziiert, lässt sich das Repository nicht ohne Weiteres durch eine Testversion ersetzen – und schon leidet die Testbarkeit der gesamten Schicht.

Wie funktioniert es?

Alle drei Smells haben dieselbe Wurzel: fehlende klare Grenzen zwischen Verantwortlichkeiten. Die offizielle Android-Architekturempfehlung beschreibt drei Schichten – UI-Schicht, Domänenschicht und Datenschicht –, die über klar definierte Schnittstellen miteinander kommunizieren. Smells entstehen, wenn diese Grenzen aufgeweicht werden.

Single-Responsibility-Prinzip als Gegenmittel gegen God-Classes: Jede Klasse sollte genau einen Grund haben, sich zu ändern. Eine Activity zeigt UI an und leitet Nutzereingaben weiter – mehr nicht. Geschäftslogik gehört ins ViewModel, Datenzugriff ins Repository. Diese Aufteilung ist kein akademisches Ideal, sondern die Voraussetzung dafür, dass Unit-Tests überhaupt sinnvoll schreibbar sind.

Application-Context statt Activity-Context: Dienste und Singletons, die einen Context benötigen, sollten immer applicationContext verwenden, nicht den Activity-Context. Der Application-Context lebt so lange wie die App selbst und erzeugt keine Leaks. Wann immer du context als Konstruktorparameter in ein langlebiges Objekt übergibst, ist das ein direktes Warnsignal.

Dependency Injection gegen enge Kopplung: Statt Abhängigkeiten selbst zu erstellen, empfängt eine Klasse sie von außen – per Konstruktor oder mit Hilt. Das erlaubt es, im Test eine gefälschte Implementierung einzuspeisen, ohne den Produktionscode zu berühren. Die offizielle Architekturempfehlung nennt DI explizit als eines der wichtigsten Entwurfsprinzipien für wartbare Android-Apps.

In der Praxis

Betrachte dieses Beispiel einer typischen God-Class-Activity:

class MainActivity : AppCompatActivity() {

    // Direkte Instanziierung = enge Kopplung + potenzieller Context-Leak
    private val db = Room.databaseBuilder(
        this, AppDatabase::class.java, "app-db"
    ).build()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        lifecycleScope.launch {
            // Netzwerkaufruf, Datenbankzugriff und UI-Update in einer Funktion
            val response = RetrofitClient.api.getItems()
            db.itemDao().insertAll(response.items)
            updateUi(response.items)
        }
    }

    private fun updateUi(items: List<Item>) { /* … */ }
}

Hier laufen gleich drei Smells zusammen: Die Activity baut selbst die Datenbankinstanz auf (enge Kopplung an die konkrete Room-Klasse), nutzt this als Context in einem Objekt, das potenziell länger als die Activity lebt, und mischt Netzwerk-, Datenbank- und UI-Logik in einer einzigen Klasse (God-Class).

Die korrigierte Variante trennt die Verantwortlichkeiten sauber:

@HiltViewModel
class ItemViewModel @Inject constructor(
    private val repository: ItemRepository  // Interface, kein konkreter Typ
) : ViewModel() {

    val items = repository.observeItems().stateIn(
        viewModelScope,
        SharingStarted.WhileSubscribed(5_000),
        emptyList()
    )
}

// Activity: nur noch UI-Verdrahtung
class MainActivity : AppCompatActivity() {
    private val viewModel: ItemViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        lifecycleScope.launch {
            viewModel.items.collect { updateUi(it) }
        }
    }
}

Das ViewModel kennt kein Android-Framework außer ViewModel selbst. Das Repository ist ein Interface – im Test einfach durch eine Fake-Implementierung ersetzbar. Kein Context wird gehalten.

Typische Stolperfalle: Ein häufiger Fehler ist, keinen Activity-Context direkt zu speichern, aber dennoch einen impliziten Leak zu erzeugen: Ein Lambda oder ein anonymes Objekt schließt über this (die Activity) und wird in einem Singleton abgelegt. LeakCanary erkennt solche Fälle zuverlässig – binde es als Debug-Dependency ein und prüfe den Leak-Bericht nach jeder Rotation oder nach dem Navigieren zurück.

Fazit

Architektur-Smells sind keine abstrakten Code-Qualitätsmetriken – sie zeigen sich in konkreten Symptomen: Klassen, die niemand mehr anfassen will; Memory-Nutzung, die nach und nach steigt; Änderungen, die an drei Stellen gleichzeitig Fehler produzieren. Wenn du die drei Muster – God-Classes, Context-Leaks, enge Kopplung – einmal verinnerlicht hast, erkennst du sie in fremdem und eigenem Code zuverlässig. Nimm dir jetzt ein laufendes Projekt vor, öffne die größte Activity oder den umfangreichsten ViewModel, und stelle dir drei Fragen: Wie viele Verantwortlichkeiten hat diese Klasse wirklich? Welche Abhängigkeiten werden direkt instanziiert statt injiziert? Wird irgendwo ein Activity-Context außerhalb der Activity gehalten? Drei Fragen, die dir sofort zeigen, wo Refactoring den größten Hebel hat.

Quellen (2)
Redaktion

Geschrieben von

Redaktion

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