Android-arkitekturmønstre, del 1: Model-View-Controller
For et år siden, da størstedelen af det nuværende Android-team begyndte at arbejde på upday, var applikationen langt fra at være den robuste, stabile app, som vi ønskede, at den skulle være. Vi forsøgte at forstå, hvorfor vores kode var i så dårlig stand, og vi fandt to hovedskyldige: løbende ændring af brugergrænsefladen og manglen på en arkitektur, der understøttede den fleksibilitet, som vi havde brug for. Appen var allerede på sit fjerde redesign på seks måneder. Det valgte designmønster syntes at være Model-View-Controller, men var allerede dengang en “mutant”, langt fra hvordan det burde være.
Lad os sammen finde ud af, hvad Model-View-Controller-mønsteret er; hvordan det er blevet anvendt i Android gennem årene; hvordan det bør anvendes, så det kan maksimere testbarheden; og nogle af dets fordele og ulemper.
I en verden, hvor brugergrænsefladelogikken har en tendens til at ændre sig oftere end forretningslogikken, havde desktop- og webudviklerne brug for en måde at adskille brugergrænsefladens funktionalitet på. MVC-mønsteret var deres løsning.
- Model – datalaget, der er ansvarlig for forvaltningen af forretningslogikken og håndteringen af netværks- eller database-API.
- View – brugergrænsefladelaget – en visualisering af dataene fra modellen.
- Controller – logiklaget, der får besked om brugerens adfærd og opdaterer modellen efter behov.
Det betyder altså, at både Controller og View er afhængige af Model: Controller for at opdatere dataene, View for at hente dataene. Men det vigtigste for desktop- og webudviklerne på det tidspunkt var, at modellen var adskilt og kunne testes uafhængigt af brugergrænsefladen. Der dukkede flere varianter af MVC op. De mest kendte er relateret til, om modellen er passiv eller aktivt meddeler, at den er blevet ændret. Her er flere detaljer:
Passiv Model
I den passive modelversion er Controller den eneste klasse, der manipulerer Model. På baggrund af brugerens handlinger skal Controller ændre modellen. Når modellen er blevet opdateret, meddeler controlleren View’en, at den også skal opdateres. På det tidspunkt vil View anmode om dataene fra modellen.
Aktiv Model
I de tilfælde, hvor Controller ikke er den eneste klasse, der ændrer Model, har Model brug for en måde at underrette View og andre klasser om opdateringer. Dette opnås ved hjælp af Observer-mønstret. Modellen indeholder en samling af observatører, der er interesseret i opdateringer. View implementerer observatørgrænsefladen og registrerer sig som observatør for Model.
Hver gang Model opdaterer, vil den også iterere gennem samlingen af observatører og kalde update
-metoden. Implementeringen af denne metode i View’en vil derefter udløse anmodningen om de seneste data fra Model.
Model-View-Controller i Android
Omkring 2011, da Android begyndte at blive mere og mere populær, dukkede der naturligvis arkitekturspørgsmål op. Da MVC var et af de mest populære UI-mønstre på det tidspunkt, forsøgte udviklere også at anvende det på Android.
Hvis du søger på StackOverflow efter spørgsmål som “How to apply MVC in Android”, stod der i et af de mest populære svar, at i Android er en aktivitet både View og Controller. Set i bakspejlet lyder det næsten vanvittigt! Men på det tidspunkt var hovedvægten lagt på at gøre modellen testbar, og normalt var implementeringsvalget for View og Controller afhængig af platformen.
Hvordan skal MVC anvendes i Android
I dag har spørgsmålet om, hvordan MVC-mønstrene skal anvendes, et svar, der er nemmere at finde. Activities, Fragments og Views bør være Views i MVC-verdenen. Controllerne bør være separate klasser, der ikke udvider eller bruger nogen Android-klasse, og det samme gælder for Modellerne.
Et problem opstår, når man forbinder Controlleren med Viewet, da Controlleren skal fortælle Viewet, at det skal opdateres. I den passive Model MVC-arkitektur skal Controlleren have en reference til View’et. Den nemmeste måde at gøre dette på, samtidig med at man fokuserer på testning, er at have en BaseView-grænseflade, som Activity/Fragment/View’en ville udvide. Controlleren vil således have en reference til BaseView.
Fordelene
Model-View-Controller-mønstret understøtter i høj grad adskillelse af bekymringer. Denne fordel øger ikke kun testbarheden af koden, men gør den også lettere at udvide, hvilket giver mulighed for en forholdsvis nem implementering af nye funktioner.
Model-klasserne har ikke nogen reference til Android-klasser og er derfor ukomplicerede at enhedsteste. Controller udvider eller implementerer ingen Android-klasser og bør have en reference til en grænsefladeklasse i View’en. På denne måde er det også muligt at foretage enhedstest af Controlleren.
Hvis Views overholder princippet om enkeltansvar, er deres rolle blot at opdatere Controlleren for hver brugerbegivenhed og blot at vise data fra Model uden at implementere nogen forretningslogik. I dette tilfælde bør UI-tests være tilstrækkelige til at dække visningens funktionaliteter.
View’et afhænger af controlleren og modellen
View’ets afhængighed af modellen begynder at være en ulempe i komplekse visninger. For at minimere logikken i visningen bør modellen være i stand til at levere testbare metoder for hvert element, der skal vises. I en aktiv Model-implementering øger dette eksponentielt antallet af klasser og metoder, da der vil være behov for Observers for hver type data.
Givet at View afhænger af både Controller og Model, kan ændringer i UI-logikken kræve opdateringer i flere klasser, hvilket mindsker mønsterets fleksibilitet.
Hvem håndterer UI-logikken?
I henhold til MVC-mønsteret opdaterer Controller modellen, og View får de data, der skal vises, fra modellen. Men hvem beslutter, hvordan dataene skal vises? Er det Model eller View? Overvej følgende eksempel: Vi har en User
, med fornavn og efternavn. I View’en skal vi vise brugernavnet som “Lastname, Firstname” (f.eks. “Doe, John”).
Hvis modellens rolle blot er at levere de “rå” data, betyder det, at koden i View’en ville være:
String firstName = userModel.getFirstName();
String lastName = userModel.getLastName(); nameTextView.setText(lastName + ", " + firstName)
Det betyder altså, at det ville være View’ens ansvar at håndtere UI-logikken. Men det gør UI-logikken umulig at enhedsteste.
Den anden fremgangsmåde er at lade modellen kun eksponere de data, der skal vises, og skjule enhver forretningslogik fra visningen (View). Men så ender vi med Modeller, der håndterer både forretnings- og UI-logik. Det ville kunne enhedstestes, men så ender det med, at Model implicit er afhængig af View.
String name = userModel.getDisplayName(); nameTextView.setText(name);
Konklusion
I de tidlige dage af Android syntes Model-View-Controller-mønstret at have forvirret mange udviklere og ført til kode, der var svær, hvis ikke umulig at enhedsteste.
Afhængigheden af View fra Model og det at have logik i View styrede vores kodebase til en tilstand, som det var umuligt at komme ud af uden at refaktorisere appen fuldstændigt. Hvad var den nye tilgang i arkitekturen og hvorfor? Find ud af det ved at læse dette blogindlæg.