Android arkitekturmönster del 1: Model-View-Controller

Florina Muntenescu
Florina Muntenescu

Follow

Nov 1, 2016 – 6 min read

För ett år sedan, när majoriteten av det nuvarande Android-teamet började jobba på upday, var applikationen långt ifrån den robusta, stabila appen som vi ville att den skulle vara. Vi försökte förstå varför vår kod var i så dåligt skick och vi hittade två huvudansvariga: ständiga ändringar av användargränssnittet och avsaknaden av en arkitektur som stödde den flexibilitet som vi behövde. Appen var redan inne på sin fjärde omarbetning på sex månader. Designmönstret som valdes verkade vara Model-View-Controller men var då redan en ”mutant”, långt ifrån hur det borde vara.

Låt oss tillsammans upptäcka vad Model-View-Controller-mönstret är, hur det har tillämpats i Android genom åren, hur det bör tillämpas för att maximera testbarheten och några av dess för- och nackdelar.

I en värld där användargränssnittslogiken tenderar att ändras oftare än affärslogiken, behövde utvecklare av skrivbordssystem och webben ett sätt att separera funktionaliteten i användargränssnittet. MVC-mönstret var deras lösning.

  • Model – datalaget, som ansvarar för att hantera affärslogiken och hantera nätverks- eller databas-API:er.
  • View – användargränssnittsskiktet – en visualisering av data från modellen.
  • Controller – logikskiktet, får information om användarens beteende och uppdaterar modellen vid behov.

Modell-View-Controller-klassstruktur

Detta innebär alltså att både Controller och View är beroende av Model: Controller för att uppdatera data, View för att få data. Men, viktigast för dator- och webbutvecklarna på den tiden: modellen var separerad och kunde testas oberoende av användargränssnittet. Flera varianter av MVC dök upp. De mest kända är relaterade till om modellen är passiv eller om den aktivt meddelar att den har ändrats. Här finns mer information:

Passiv modell

I den passiva modellversionen är Controller den enda klass som manipulerar modellen. Baserat på användarens åtgärder måste Controller ändra modellen. När modellen har uppdaterats meddelar Controller View att den också måste uppdateras. Vid den tidpunkten kommer View att begära data från modellen.

Model-View-Controller – passiv modell – beteende

Aktiv modell

För de fall då Controllern inte är den enda klassen som ändrar modellen behöver modellen ett sätt att meddela vyn och andra klasser om uppdateringar. Detta uppnås med hjälp av Observer-mönstret. Modellen innehåller en samling observatörer som är intresserade av uppdateringar. View implementerar observatörsgränssnittet och registrerar sig som observatör till modellen.

Model-View-Controller – aktiv modell – klasstruktur

Varje gång modellen uppdateras, kommer den också att iterera genom samlingen av observatörer och anropa metoden update. Genomförandet av denna metod i vyn kommer sedan att utlösa begäran av de senaste uppgifterna från modellen.

Model-View-Controller – aktiv modell – beteende

Model-View-Controller i Android

Omkring 2011, när Android började bli allt mer populärt, dök det naturligtvis upp arkitekturfrågor. Eftersom MVC var ett av de mest populära UI-mönstren på den tiden försökte utvecklare tillämpa det även på Android.

Om du söker på StackOverflow efter frågor som ”How to apply MVC in Android”, så uppgav ett av de populäraste svaren att i Android är en aktivitet både View och Controller. När jag ser tillbaka så låter detta nästan galet! Men vid den tidpunkten låg huvudvikten på att göra modellen testbar och vanligtvis var implementeringsvalet för View och Controller beroende av plattformen.

Hur ska MVC tillämpas i Android

Nuförtiden har frågan om hur MVC-mönstren ska tillämpas ett svar som är lättare att hitta. Aktiviteterna, fragmenten och vyerna bör vara vyerna i MVC-världen. Controllers bör vara separata klasser som inte förlänger eller använder någon Android-klass, och detsamma gäller Models.

Ett problem uppstår när man ansluter Controller till View, eftersom Controller måste tala om för View att uppdatera. I den passiva modellen MVC-arkitekturen måste kontrollanten ha en referens till vyn. Det enklaste sättet att göra detta, samtidigt som man fokuserar på testning, är att ha ett BaseView-gränssnitt, som Activity/Fragment/View skulle förlänga. Controller skulle alltså ha en referens till BaseView.

Fördelar

Mönstret Model-View-Controller stöder i hög grad separering av problemställningar. Denna fördel ökar inte bara testbarheten av koden utan gör den också lättare att utöka, vilket möjliggör en ganska enkel implementering av nya funktioner.

Model-klasserna har inga referenser till Android-klasser och är därför okomplicerade att enhetstesta. Controller utökar eller implementerar inga Android-klasser och bör ha en referens till en gränssnittsklass för View. På så sätt är enhetstestning av Controller också möjlig.

Om Views respekterar principen om ett enda ansvar är deras roll bara att uppdatera Controller för varje användarhändelse och bara visa data från Model, utan att implementera någon affärslogik. I det här fallet bör UI-testerna vara tillräckliga för att täcka vyens funktioner.

Vyn är beroende av kontrollanten och modellen

Vyns beroende av modellen börjar bli en nackdel i komplexa vyer. För att minimera logiken i vyn bör modellen kunna tillhandahålla testbara metoder för varje element som ska visas. I en aktiv Model-implementation ökar detta exponentiellt antalet klasser och metoder, med tanke på att det skulle krävas observatörer för varje typ av data.

Med tanke på att View är beroende av både Controller och Model kan ändringar i UI-logiken kräva uppdateringar i flera klasser, vilket minskar mönstrets flexibilitet.

Vem hanterar UI-logiken?

Enligt MVC-mönstret uppdaterar Controller modellen och View hämtar de data som ska visas från modellen. Men vem bestämmer hur data ska visas? Är det modellen eller vyn? Tänk på följande exempel: Vi har en User, med förnamn och efternamn. I vyn måste vi visa användarnamnet som ”Lastname, Firstname” (t.ex. ”Doe, John”).

Om modellens roll bara är att tillhandahålla de ”råa” uppgifterna betyder det att koden i vyn skulle vara:

String firstName = userModel.getFirstName(); 
String lastName = userModel.getLastName(); nameTextView.setText(lastName + ", " + firstName)

Det betyder alltså att det skulle vara vyns ansvar att hantera UI-logiken. Men detta gör UI-logiken omöjlig att enhetstesta.

Det andra tillvägagångssättet är att låta modellen endast exponera de data som behöver visas och dölja all affärslogik från vyn. Men då får vi modeller som hanterar både affärslogik och UI-logik. Det skulle vara enhetstestbart, men då slutar det med att modellen implicit är beroende av vyn.

String name = userModel.getDisplayName(); nameTextView.setText(name);

Slutsats

I början av Android verkar Model-View-Controller-mönstret ha förvirrat många utvecklare och lett till kod som var svår, om inte omöjlig att enhetstesta.

Vyns beroende av modellen och att ha logik i vyn styrde vår kodbas till ett tillstånd som det var omöjligt att återhämta sig från utan att refaktorisera appen helt och hållet. Vad var det nya tillvägagångssättet i arkitekturen och varför? Ta reda på det genom att läsa det här blogginlägget.