19.1 De tre dele af MVC 320
19.1.1 Modellen 320
19.1.2 Præsentationen 320
19.1.3 Kontrol-delen 321
19.2 Relationer mellem delene 321
19.2.1 Informationsstrøm gennem MVC 321
19.2.2 Opdatering af præsentationen - tre muligheder 322
19.2.3 A - Præsentationer undersøger modellen 322
19.2.4 B - Kontroldel underretter præsentationer 322
19.2.5 C - Modellen underretter præsentationer 323
19.3 Eksempel - bankkonti 323
19.3.1 Modellen 324
19.3.2 Præsentationer 325
19.3.3 Kontroldel 326
19.4 Model-View - "den lille MVC" 328
19.4.1 Adskillelse af præsentation og programlogik 328
19.5 Opgaver 328
19.5.1 Løsning 329
Det er en god idé at kigge i dette kapitel før man læser kapitel 6, Grafiske brugergrænseflader (Swing).
De fleste programmer med en brugergrænseflade kan inddeles i tre dele, nemlig:
datamodellen, som repræsenterer data og de bagvedliggende beregninger
præsentationen af data over for brugeren
brugerens mulighed for at ændre i disse data gennem forskellige handlinger.
Ofte præsenteres brugeren ikke for alle data, måske kan han ikke ændre dem frit, og måske er der konsekvenser for andre data i det samme eller i andre skærmbilleder.
Model-View-Controller-arkitekturen (forkortet MVC) er et designmønster beregnet til programmer med en brugergrænseflade.
Den anbefaler at man opdeler programmet (i hvert fald mentalt) i tre dele: En model, en præsentation, og en kontrol-del:
Model-View-Controller-arkitekturen
i dens grundform. Pilene viser, hvilke
dele der kender til
hverandre (der kan være flere - se afsnit 19.2.2 og frem).
Datamodellen indeholder data og registrerer, hvilken tilstand den pågældende del af programmet er i. Oftest er data indkapslet sådan, at konsistens sikres. I så fald er der kun adgang til at spørge og ændre på data gennem metodekald.
Modellen bør være uafhængig af, hvordan data præsenteres over for brugeren, og er der flere programmer, der arbejder med de samme slags data, kan de i princippet have den samme datamodel, selvom de i øvrigt er helt forskellige.
Eksempel: En bankkonto har navn på ejer, kontonummer, kort-ID, saldo, bevægelser, renteoplysninger etc. Saldoen kan ikke ændres direkte, men med handlingerne overførsel, udbetaling og indbetaling kan saldoen påvirkes (se eksempelvis klassen Kontomodel i afsnit 19.3.1).
Bemærk, hvordan modellen for en bankkonto er universel. Modellen kunne f.eks. anvendes både i et program til en pengeautomat, i et netbank-system og i programmet, som ekspeditionsmedarbejderen anvender ved skranken.
Præsentationen (eng.: View) henter relevante data fra modellen og viser dem for brugeren i en passende form. Selvom to præsentationer deler model (viser data fra samme model), kan de være meget forskellige, da de er beregnet på en bestemt brugergrænseflade.
Eksempel: Bankkontoen præsenteres meget forskelligt. I en pengeautomat vises ingen personlige oplysninger overhovedet. I et netbank-system kan saldo og bevægelser ses (det kunne være en webløsning i HTML, f.eks. en servlet eller JSP-side). Ved skranken kan medarbejderen se endnu mere, f.eks. filial og kontaktperson i banken (det kunne være implementeret som en grafisk applikation, der kører hos brugeren).
Kontroldelen (eng.: controller) definerer, hvad programmet kan. Den omsætter brugerens indtastninger, museklik mv. til handlinger, der skal udføres på modellen.
Eksempel: I pengeautomat kan man kun hæve penge. I et netbank-system kan brugeren måske lave visse former for overførsel fra sin egen konto. Ved skranken kan medarbejderen derudover foretage ind- og udbetalinger.
Forestil dig, at modellen, præsentationen og kontroldelen udgøres af hver sin klasse. Hvad er så relationerne mellem klasserne?
Det er klart, at præsentationen og kontroldelen, for at kunne fremvise hhv. manipulere med modellen, skal kende til modellen og dens metoder. Hvilke andre bindinger er der?
Figuren herunder illustrerer, hvordan strømmen af information går fra modellen via præsentationen til brugeren (symboliseret ved et øje). Brugeren foretager nogle handlinger (symboliseret ved musen), som via kontrol-delen fortolkes som nogle ændringer, der foretages på modellen, hvorefter de nye data vises for brugeren.
Præsentationen er normalt1 nødt til på en eller anden måde at få at vide, når der er sket en ændring i modellens data, så den kan opdatere skærmbilledet.
Det kunne ske på tre måder: Enten må præsentationen regelmæssigt undersøge modellen for at opdage ændringer, eller også må kontroldelen eller modellen fortælle præsentationen, at noget er ændret. Lad os se på alle tre muligheder.
Hvis præsentationen regelmæssigt skal undersøge model-klassen for at opdage ændringerne, vil ændringerne naturligvis dukke op på brugerens skærm med en vis forsinkelse, der kan virke forvirrende for brugeren. Forsinkelsen kan naturligvis mindskes ved at undersøge modellen meget ofte (dette kaldes polling), men det er en ineffektiv løsning, der kræver meget processortid.
Denne mulighed er dog velegnet i de tilfælde, hvor der skal ske opdateringer af skærmen så ofte som overhovedet muligt (f.eks. i et computerspil, hvor animationerne skal være så flydende som muligt). Her vil præsentationen konstant undersøge modellen (for at tegne skærmbilledet), og ændringer vil derfor blive synlige næsten omgående.
I andre tilfælde ligger det i sagens natur, at der altid sker en fremvisning lige efter en opdatering. Det gælder f.eks. webløsninger som servletter/JSP beskrevet i afsnit 14.2, Webservere (servletter og JSP), hvor netlæseren altid foretager en anmodning (der indeholder formulardata, som kontroldelen omsætter til kald til modellen) og får HTML-koden til et nyt skærmbillede tilbage som svar (genereret af præsentationen ud fra modellen).
En oplagt mulighed er, at kontroldelen, efter hver ændring på modellen, underretter præsentationen om, at noget er ændret (og dermed opdaterer skærmen).
Det er en fin løsning til mindre systemer, men hvad nu hvis der er flere præsentationer (og kontroldele) af den samme model? For at holde visningen over for brugeren korrekt skal hver kontroldel holde styr på samtlige præsentationer. Hver gang der kommer en ny præsentation til, skal kontroldelene opdateres til at medtage den. I et lidt større scenario kan det kan give et ret uoverskueligt program.
En anden, mere avanceret (men i længden mere enkel) løsning er at lægge opdateringsopgaven hos modellen, sådan at den fortæller det til præsentationen (og andre interessenter), når den ændres:
MVC med præsentationer, som observerer modellen - den oftest brugte variant af MVC.
Da modellen skal være uafhængig af præsentationerne (f.eks. for at modellen kan genbruges med en anden præsentation i et andet program), kan den nødvendigvis ikke vide noget om præsentationerne direkte.
I stedet observerer præsentationerne modellen på samme måde som i Javas hændelsessystem: Præsentationerne registrerer sig som lyttere hos modellen, og modellen sender en hændelse til alle registrerede lytter når den ændres. Den stiplede linje (underretning om ændring) illustrerer dette.
Dette er designmønstret Observatør (beskrevet i afsnit 17.5) og kan også udtrykkes i dette designmønsters ordvalg: Præsentationerne er observatører, der observerer modellen. Modellen underretter sine observatører, når den ændres.
Da den mere avancerede mulighed C stemmer overens med Javas hændelsesmodel og er den mest generelle, er det den, der oftest tages i anvendelse. Mulighed B er velegnet til simplere systemer.
Her følger et tænkt eksempel på en bankkonto. Hver konto har en ejer, og man kan indsætte, hæve og overføre penge til en anden konto.
Mulighed C er valgt her, dvs. præsentationer skal registrere sig hos modellen (ActionEvent er anvendt som hændelse, så det vil sige, at lyttere skal implementere ActionListener-interfacet).
Modellen har metoder, der svarer til forretningslogikken i programmet, såsom overfør(), hæv() og indsæt().
Derudover har den metoderne addActionListener() og removeActionListener() til at registrere lyttere (observatører) på den.
import java.util.*; import java.awt.event.*; public class Kontomodel { private String ejer; private double saldo; private List bevægelser = new ArrayList(); // til historik public Kontomodel(String ejer1) { ejer = ejer1; } public double getSaldo() { return saldo; } public String getEjer() { return ejer; } public String toString() { return ejer + ": "+saldo+" kr"; } public void overfør(Kontomodel til, double beløb) { if (beløb<0) throw new IllegalArgumentException( "Beløb kan ikke være negativt eller nul."); saldo = saldo - beløb; til.saldo = til.saldo + beløb;// privat variabel kan ses i samme klasse String ændring = "Overført "+beløb+" fra "+ejer+" til "+til.ejer; bevægelser.add(ændring); til.bevægelser.add(ændring); fortælLyttere(ændring); // besked til alle visninger af denne konto til.fortælLyttere(ændring); // besked til alle visninger af beløbsmodtager } public void hæv(double beløb) { if (beløb<0) throw new IllegalArgumentException( "Beløb kan ikke være negativt eller nul."); saldo = saldo - beløb; String ændring = "Hævet "+beløb; bevægelser.add(ændring); fortælLyttere(ændring); // send besked til alle visninger } public void indsæt(double beløb) { if (beløb<0) throw new IllegalArgumentException( "Beløb kan ikke være negativt eller nul."); saldo = saldo + beløb; String ændring = "Indsat "+beløb; bevægelser.add(ændring); fortælLyttere(ændring); // Send besked til alle visninger } // // Underetning af hændelses-lyttere. // /** Lyttere til denne model */ private List lyttere = new ArrayList(2); /** Tilføjer en lytter */ public synchronized void addActionListener(ActionListener l) { lyttere.add(l); } /** Fjerner en lytter */ public synchronized void removeActionListener(ActionListener l) { lyttere.remove(l); } /** Fortæller alle lytter om en ændring i modellen */ private void fortælLyttere(String ændring) { // opret hændelse, der beskriver ændringen ActionEvent hændelse = new ActionEvent(this, 0, ændring); for (Iterator i=lyttere.iterator(); i.hasNext(); ) { ActionListener l = (ActionListener) i.next(); l.actionPerformed(hændelse);// underret l om hændelsen } } }
Herunder to eksempler på præsentationer af modellen. Den første er ret enkel, da den 'viser' modellen ved at udskrive på System.out, hver gang der sker en ændring:
import java.awt.event.*; public class KontovisningTekst implements ActionListener { private Kontomodel model; public KontovisningTekst(Kontomodel model1) { model = model1; model.addActionListener(this); // registrér som lytter på modellen } public void actionPerformed(ActionEvent hændels) { // getActionCommand() giver beskrivelsen af hændelsen System.out.println("Konto "+model.getEjer()+": "+hændels.getActionCommand()); System.out.println("Konto "+model.getEjer()+": Saldo er: "+model.getSaldo()); } }
Uddata fra denne klasse kunne være:
Konto Jacob: Indsat 20.0
Konto Jacob: Saldo er nu: 20.0
Konto Jacob: Indsat 20.0
Konto Jacob: Saldo er nu: 40.0
Konto Jacob: Overført 50.0 fra Jacob til Brian
Konto Jacob: Saldo er nu: -10.0
Den anden er et grafisk panel (i figuren til højre er det vist i et applet-vindue). Ved hver ændring i modellen bliver panelet gentegnet.
import javax.swing.*; import java.awt.*; import java.awt.event.*; public class KontovisningPanel extends JPanel implements ActionListener { private Kontomodel model; private String meddelelse; public void paint(Graphics g) { super.paint(g); if (model == null) return; g.drawString("Konto "+model.getEjer(),10,10); if (meddelelse != null) { g.drawString(meddelelse,10,25); // næste gang der gentegnes skal meddelelsen ikke vises meddelelse = null; } if (model.getSaldo()<0) g.setColor(Color.red); else g.setColor(Color.darkGray); g.drawString("saldo: "+model.getSaldo(),10,40); } public void setModel(Kontomodel model1) { if (model != null) model.removeActionListener(this); model = model1; if (model != null) model.addActionListener(this); // lytter på modellen } public void actionPerformed(ActionEvent hændelse) { meddelelse = hændelse.getActionCommand(); repaint(); } }
Herunder har vi lavet en applet, der opretter en Jacob og en Brian-kontomodel. I appletten er der et antal KontovisningPanel'er, der viser kontiene, og nogle knapper, der kan ændre på kontiene (disse knapper udgør kontrol-delen af programmet).
import javax.swing.*; import java.awt.*; import java.awt.event.*; public class KontokontrolApplet extends JApplet { Kontomodel jacobsKonto = new Kontomodel("Jacob"); Kontomodel briansKonto = new Kontomodel("Brian"); KontovisningPanel panelJacob = new KontovisningPanel(); KontovisningPanel panelBrian = new KontovisningPanel(); KontovisningPanel panelJacobAndenVisning = new KontovisningPanel(); JButton buttonJtilB50kr = new JButton(); JButton buttonJ20krind = new JButton(); JButton buttonB30krud = new JButton(); public void init() { try { jbInit(); } catch(Exception e) { e.printStackTrace(); } panelJacob.setModel(jacobsKonto); // panelJacob lytter på jacobsKonto panelJacobAndenVisning.setModel(jacobsKonto); // ditto panelBrian.setModel(briansKonto); // panelBrian lytter på briansKonto new KontovisningTekst(jacobsKonto); // tekstvisning på jacobsKonto new KontovisningTekst(briansKonto); // tekstvisning på briansKonto JFrame f = new JFrame("Brian"); // lav også separat vindue til Brian KontovisningPanel panelBrianAndenVisning = new KontovisningPanel(); panelBrianAndenVisning.setModel(briansKonto); f.getContentPane().add(panelBrianAndenVisning); f.setSize(150,100); f.validate(); f.setVisible(true); } private void jbInit() throws Exception { this.getContentPane().setLayout(null); panelJacob.setBounds(new Rectangle(0, 0, 139, 102)); panelBrian.setBounds(new Rectangle(250, 0, 150, 101)); panelJacobAndenVisning.setBounds(new Rectangle(130, 105, 140, 84)); buttonJtilB50kr.setText("50 kr ->"); buttonJtilB50kr.setBounds(new Rectangle(140, 33, 100, 25)); // når der affyres en hændelse fra buttonJtilB50kr, så kald // metoden buttonJtilB50kr_actionPerformed, defineret nedenfor buttonJtilB50kr.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(ActionEvent e) { buttonJtilB50kr_actionPerformed(e); } }); buttonJ20krind.setText("Indsæt 20 kr"); buttonJ20krind.setBounds(new Rectangle(6, 104, 124, 25)); buttonJ20krind.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(ActionEvent e) { buttonJ20krind_actionPerformed(e); } }); buttonB30krud.setText("Hæv 30 kr"); buttonB30krud.setBounds(new Rectangle(272, 105, 124, 25)); buttonB30krud.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(ActionEvent e) { buttonB30krud_actionPerformed(e); } }); this.getContentPane().add(panelBrian, null); this.getContentPane().add(panelJacob, null); this.getContentPane().add(buttonJtilB50kr, null); this.getContentPane().add(buttonJ20krind, null); this.getContentPane().add(buttonB30krud, null); this.getContentPane().add(panelJacobAndenVisning, null); } // kontroldelen af programmet - ændr i modellen efter brugerens handlinger void buttonJ20krind_actionPerformed(ActionEvent e) { jacobsKonto.indsæt(20); } void buttonB30krud_actionPerformed(ActionEvent e) { briansKonto.hæv(30); } void buttonJtilB50kr_actionPerformed(ActionEvent e) { jacobsKonto.overfør(briansKonto,50); } }
Adskillelsen mellem præsentation og kontroldel kan være besværlig eller uhensigtsmæssig at gennemføre i praksis.
Det gælder for eksempel, når man programmerer grafiske brugergrænseflader i Java: Her er det hensigtsmæssigt at lægge præsentation (paint()-metode og grafiske komponenter) og kontrol-del (hændelseslyttere på komponenterne) i samme klasse.
Derfor begrænses MVC undertiden til "Model-View"-arkitekturen, hvor præsentation og kontrol-del er slået sammen.
Model-View-arkitekturen er idéen om at adskille præsentation og programlogik, sådan at programlogikken (modellen) kan genbruges i andre sammenhænge.
I denne begrænsede udgave af MVC vil der ofte kun være én præsentation af data, og man kan derfor se bort fra diskussionen i afsnit 19.2.2 om opdateringen af præsentationen.
Hent Konto-eksemplet fra
http://javabog.dk/VP/kode/.
Prøv at køre det.
Hvordan kan det være, at
visningerne af Kontomodel bliver opdateret?
Opret KontovisningMedTekstArea, der er en
klasse (JPanel eller JFrame), der viser (og opdaterer) en
beskrivelse af en konto i et tekstfelt.
1I nogle tilfælde bliver præsentationen altid kaldt efter en opdatering. Det gælder f.eks. JSP-sider og servletter.