Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.
Comment: Migrated to Confluence 5.3
Excerpt

Memory er et spill hvor en prøver å huske og gjenta en så lang sekvens som mulig med tall, bilder eller andre "objekter". Sekvensen blir lengre og lengre og spillet fortsetter helt til en gjentar den feil. Dette kodeeksemplet viser hvordan vi kan implementere en enkel tekstlig versjon av spillet.

Selv om et slikt enkelt spill kan implementeres med én klasse, så legger vi opp til en klassisk todeling i én hovedprogram-klasse, som håndterer kommunikasjone med brukeren, og én logikk-klasse, som håndterer spill-logikken. Dersom logikk-klassen gjøres anvendelig og generell nok, vil en senere kunne gjenbruke den i en grafisk versjon. Todelingen illustreres under med et objektdiagram:

...

Det som ikke kommer frem av dialogen er hvordan MemoryProgram-objektet samspiller med Memory-objektet, dvs. hvilke kall MemoryProgram utfører på Memory og hvilke verdier som flyter frem (argumentverdier) og tilbake (returverdier). Dette er den vanskeligste delen av programmering: å bestemme når og hvordan objektene samhandler, altså å fordele oppgaver mellom objektene og konkretisere oppgavene som metoder med argumenter og returverdier. En måte å komme litt videre på er å legge inn kommentarer i dialogen overtegne et sekvensdiagram, som illustrerer hvordan MemoryProgram bruker Memory underveis. Merk at dette ikke er så ulikt et sekvensdiagram, men det er litt tidlig å bruke en så formell notasjon, siden vi ennå ikke har satt navn på metoder og gitt dem argument- og returtyper.

// Memory genererer nytt tall i sekvensen => 3
Element nr. 1 er 3
Gjenta element nr. 1 av 1
3
// Memory sjekker om 3 er riktig neste tall => ok og ferdig
// Memory genererer nytt tall i sekvensen => 4
Element nr. 2 er 4
Gjenta element nr. 1 av 2
3
// Memory sjekker om 3 er riktig neste tall => ok, men ikke ferdig
Gjenta element nr. 2 av 2
4
// Memory sjekker om 4 er riktig neste tall => ok og ferdig
// Memory genererer nytt tall i sekvensen => 7
...

Vi ser at Memory hovedsaklig tilbyr to tjenester: å generere nye tall i sekvensen og å ta imot et nytt tall (fra brukeren) og sjekke det mot fasiten. Det er dessuten underforstått at Memory må huske både tall-sekvensen og hvor langt brukeren har kommet i å gjenta den. Vi kan nå oppdatere diagrammet vårt med denne innsikten:

PlantUML Macro
class MemoryProgram {
}
class Memory {
	tallsekvens
	sekvensPosisjon
	genererNesteTall()
	sjekkNesteTallFraBruker()
}

MemoryProgram -right-> Memory: memory

Dette er et godt utgangspunkt for å begynne å kode, og selv om det er mulig å fylle ut mer detaljer i diagrammet først, så er erfaringen at koding raskere skaper fremdrift ved å avdekke problemer og mulige løsninger.

Vi har valget mellom tre strategier: 1) skrive MemoryProgram og dermed avdekke mer presist hvilke metoder Memory må tilby, 2) skrive Memory først og tilpasse MemoryProgram deretter, eller 3) en hybrid strategi hvor vi jobber parallelt med begge to. Dette er ofte en smaksak, men jeg foretrekker ofte å jobbe topp-ned på skissestadiet, som vi er på nå, og bunn-opp med selve kodingen. Imidlertid kan en godt jobbe litt mer på papir, for å klargjøre i litt mer detalj hva hver metode er ment å gjøre, og her kan både sekvensdiagrammer og objekttilstandsdiagrammer være nyttige. Førstnevnte illustrerer hvordan objekter utveksler data i metodekall, trigget av interaksjon med brukeren. Her er et slikt diagram basert på eksemplet over:

PlantUML Macro
actor bruker
MemoryProgram -> Memory: genererNesteTall()
Memory --> MemoryProgram: 3
MemoryProgram -> bruker: "Element nr. 1 er 3"
MemoryProgram -> bruker: "Gjenta element nr. 1 av 1"
bruker --> MemoryProgram: 3
MemoryProgram -> Memory: sjekkNesteTallFraBruker()
Memory --> MemoryProgram: ok og ferdig
MemoryProgram -> Memory: genererNesteTall()
Memory --> MemoryProgram: 4
MemoryProgram -> bruker: "Element nr. 2 er 4"
MemoryProgram -> bruker: "Gjenta element nr. 1 av 2"
bruker --> MemoryProgram: 3
MemoryProgram -> Memory: sjekkNesteTallFraBruker()
Memory --> MemoryProgram: ok, men ikke ferdig
MemoryProgram -> bruker: "Gjenta element nr. 2 av 2"
bruker --> MemoryProgram: 4
MemoryProgram -> Memory: sjekkNesteTallFraBruker()
Memory --> MemoryProgram: ok og ferdig
MemoryProgram -> Memory: genererNesteTall()
Memory --> MemoryProgram: 4

 

, trigget av interaksjon med brukeren.

PlantUML Macro
border1
class MemoryProgram {
}
class Memory {
	expectedItems	// list of numbers 1-9
	acceptedCount	// position in list
	nextItem()		// adds item to list
	acceptItem()	// accepts (or rejects) item
}

MemoryProgram -right-> Memory: memory
PlantUML Macro
actor bruker
MemoryProgram -> Memory: nextItem()
Memory -> MemoryProgram: 3
MemoryProgram -> bruker: "Element nr. 1 er 3"
MemoryProgram -> bruker: "Gjenta element nr. 1 av 1"
bruker --> MemoryProgram: 3
MemoryProgram -> Memory: acceptItem()
Memory --> MemoryProgram: ok og ferdig
MemoryProgram -> Memory: nextItem()
Memory --> MemoryProgram: 4
MemoryProgram -> bruker: "Element nr. 2 er 4"
MemoryProgram -> bruker: "Gjenta element nr. 1 av 2"
bruker --> MemoryProgram: 3
MemoryProgram -> Memory: acceptItem()
Memory --> MemoryProgram: ok, men ikke ferdig
MemoryProgram -> bruker: "Gjenta element nr. 2 av 2"
bruker --> MemoryProgram: 4
MemoryProgram -> Memory: acceptItem()
Memory --> MemoryProgram: ok og ferdig
MemoryProgram -> Memory: nextItem()
Memory --> MemoryProgram: 4

 

 

PlantUML Macro
object "Memory" as memory1 {
	expectedItems = []
	acceptedCount = 0
}
object "Memory" as memory2 {
	expectedItems = [3]
	acceptedCount = 0
}
memory1 -down-> memory2: nextItem() => 3
object "Memory" as memory3 {
	expectedItems = [3]
	acceptedCount = 1
}
memory2 -down-> memory3: acceptItem() => ok og ferdig
object "Memory" as memory4 {
	expectedItems = [3, 4]
	acceptedCount = 0
}
memory3 -down-> memory4: nextItem() => 4
object "Memory" as memory5 {
	expectedItems = [3, 4]
	acceptedCount = 1
}
memory4 -down-> memory5: acceptItem() => ok, men ikke ferdig
object "Memory" as memory6 {
	expectedItems = [3, 4]
	acceptedCount = 2
}
memory5 -down-> memory6: acceptItem() => ok og ferdig
object "Memory" as memory7 {
	expectedItems = [3, 4, 7]
	acceptedCount = 0
}
memory6 -down-> memory7: nextItem() => 7

 

 

I sekvensdiagrammet har vi satt navn på to tjenester som Memory-objektet må tilby: nextItem() genererer nye tall i sekvensen, og acceptItem() tar imot et nytt tall (fra brukeren) og sjekker det mot fasiten. Det er dessuten underforstått at Memory må huske både tall-sekvensen og hvor langt brukeren har kommet i å gjenta den, som angitt i klassediagrammet med henholdsvis attributtene expectedItems og acceptedCount. To av de mulige svarene fra acceptItem() er angitt som ok og ferdig og ok, men ikke ferdig. Dette er ikke gyldig java, så når vi koder metoden, så må vi finne passende verdier å returnere for disse to tilfellene, i tillegg til den tredje muligheten, som er feil.

Dette er et godt utgangspunkt for å begynne å kode, og for noen vil det være det meste effektive. Men er man fortsatt usikker på hvordan den interne tilstanden håndteres av metodene, så kan et objekttilstandsdiagram være nyttige, siden det illustrerer effekten av metodekall på den interne tilstanden, ikke bare sekvensen av kall. Et slikt diagram er vist til høyre for tilstandsdiagrammet, basert på samme sekvens av kall.

Når vi begynner å kode, har vi valget mellom tre strategier. 1) Ved topp-ned-koding så skriver en først MemoryProgram og lar detaljene i Memory bli avgjort av hva som gjør MemoryProgram enklest å skrive. 2) Ved bunn-opp-koding skriver en Memory først og tilpasser MemoryProgram deretter. 3) Med en blandet (hybrid) strategi jobber vi parallelt med begge to. Dette er ofte en smaksak, men jeg foretrekker ofte å jobbe topp-ned på skissestadiet, og bunn-opp med selve kodingen. Her er koden for MemoryHer er kode for å representere tallsekvensen, generere et nytt tall, huske hvor langt i sekvensen brukeren har kommet og sjekke et nytt tall mot riktig tall i sekvensen:

Code Block
languagejava
public class Memory {

    private List<Integer> expectedItems = new ArrayList<Integer>();
    private int acceptedCount = 0;
    
    public int nextItem() {
        int nextItem = (int) (Math.random() * 9) + 1; 	// new number value
        expectedItems.add(nextItem);					// add to number sequence
        acceptedCount = 0;								// reset counter
        return nextItem;								// return new number value
    }

    public Boolean acceptItem(int item) {
        if (! expectedItems.get(acceptedCount).equals(item)) {	// compare the number input by the user with the corresponding sequence value
            return Boolean.FALSE;								// if they are not the same, we indicate this by returning false
        }
        acceptedCount++;										// correct number, so increment counter
        if (acceptedCount == expectedItems.size()) {			// if this was the last number
            return Boolean.TRUE;								// return true
        }
        return null;											// otherwise return null, indicating correct value, but not finished with sequence 
    }
} 

...

Code Block
Scanner scanner = new Scanner(System.in);
do {
	Memory memory = new Memory();				// new game instance
	while (true) {								// repeat as long as the user does not make a mistake
		int nextItem = memory.nextItem();		// extend sequence with another value
		System.out.println("\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n");	// scroll previous dialog away
		System.out.println("Element nr. " + memory.getItemCount() + " er " + nextItem);
		Boolean result = null;
		do {									// get another number from the user
			System.out.println("Gjenta element nr. " + (memory.getAcceptedCount() + 1) + " av " + memory.getItemCount());
			int nextInt = Integer.valueOf(scanner.nextLine().trimnextInt());
			result = memory.acceptItem(nextInt);	// check item
		} while (result == null);					// repeat until there is a definite result, either a mistake or a complete and correct sequence
		if (result == Boolean.FALSE) {				// if the user made a mistake, break out of loop
			System.out.println("Feil, den lengste sekvensen du klarte var på " + (memory.getItemCount() - 1) + " elementer.");
			break;
		}
	}
	System.out.println("Vil du prøve på nytt (ja/nei)?");
} while (scanner.nextLine().trimnext().equals("ja"));	// repeat as long as the user wants to play another game
scanner.close();

Merk at vi her har brukt to getter-metoder i Memory som ikke er angitt i diagrammene eller koden, nemlig getItemCount() og getAcceptedCount().

Dette er ren prosedyre-orientert kode og må plasseres inn i en metode i hovedprogram-klassen MemoryProgram. Strengt tatt kan dette være main-metoden som startes opp ved kjøring av Java-programmer, men i et objektorientert hovedprogram bruker vi main-metoden til kun å opprette hovedprogram-objektet, som her er en instans av MemoryProgram og så kjøre en run-metoden, som inneholder koden over:, som inneholder koden over:

Code Block
public class MemoryProgram {

	private Memory memory;

    public void init() {
		memory = new Memory();	// new game instance
    }
Code Block
public class MemoryProgram {

    privatepublic void run() {
		... // insert the code above
    }

	// entry point for Java program    
    public static void main(String[] args) {
        new MemoryProgram().run();	// instantiate program object and call its run() method
    }
}

I versjon 2 av Memory-programmet så ser vi på hvordan koden kan gjøres litt mer generell og gjenbrukbar. I versjon 3 bytter vi ut det tekstlige brukergrensesnittet med et minimalistisk grafisk grensesnitt bygget på JavaFX-rammeverket. Dette videreutvikles i versjon 4 til et rikere brukergrensesnitt, hvor flere JavaFX-mekanismer prøves ut.