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:
I et spill som dette vil hovedprogrammet hovedsaklig bestå av en løkke som utvider sekvensen med tall og deretter ber brukeren om å gjenta hele sekvensen. For hvert tall brukeren gir inn, må det sjekkes om tallet stemmer med neste tall i sekvensen. Vi kan tenke oss dialogen som følger (system-output i kursiv og bruker-input i fet skrift):
Element nr. 1 er 3 |
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 å tegne et sekvensdiagram, som illustrerer hvordan MemoryProgram bruker Memory underveis, trigget av interaksjon med brukeren.
|
|
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 Memory:
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 } }
Vi har valgt å representere tallsekvensen (expectedItems) som en liste (List) av heltallsobjekter (Integer). Vi bruker en liste og ikke en Java-tabell (int[]), siden sekvensen skal utvides, og vi bruker heltallsobjekter, siden lister bare kan spesialiseres til objekt-typer og ikke verdi-typer som int. Vi har latt metoden nextItem legge det nye tallet til sekvensen og returnere det tallet, siden vi ser fra dialogen med brukeren at det skal vises frem og at MemoryProgram derfor trenger verdien. Målet for hvor langt brukeren har kommet er en indeks (int acceptedCount) inn i tallsekvensen. Returverdien fra metoden som sjekker et nytt tall fra brukeren (acceptItem(int)), må kunne skille mellom tre tilfeller: 1) tallet er feil, 2) tallet er riktig og sekvensen er komplett, og 3) tallet er riktig, men sekvensen er ikke ferdig. Derfor bruker vi et Boolean-objekt, og lar Boolean.FALSE, Boolean.TRUE og null representere de tre tilfellene. Her er et oppdatert diagram over implementasjonen så langt:
Med Memory-klassen på plass, er det lettere å skrive hovedprogram-klassen MemoryProgram, siden den kan bygge på tjenestene som Memory tilbyr og heve seg litt over detaljer om datarepresentasjon. Som nevnt over er spillet hovedsaklig en dobbel løkke, hvor den ytre forlenger tallsekvensen med et nytt element og den indre ber om et nytt tall inntil brukeren skriver feil eller har skrevet inn alle tallene i sekvensen. Dersom en skal kunne spille flere spill i samme slengen, så legges enda en løkke utenpå der igjen, som kjører gjennom ett spill og spør om en vil prøve på nytt. Her er koden:
Scanner scanner = new Scanner(System.in); do { 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 = scanner.nextInt(); 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.next().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:
public class MemoryProgram { private Memory memory; public void init() { memory = new Memory(); // new game instance } public 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.