I Memory-eksempel versjon 1 laget vi en enkel tekstbasert versjon av et hukommelsesspill, hvor brukeren skulle prøve å huske og gjenta en så lang sekvens med tall som mulig. Koden var delt i to klasser, Memory og MemoryProgram, hvor Memory implementerte logikk som var ment å være uavhengig av om spillet var tekstlig eller grafisk, og ble brukt av MemoryProgram som håndterte kommunikasjonen med brukeren. Før vi prøver oss på en grafisk versjon basert på JavaFX så er det lurt å se om Memory-klassen kan gjøres litt mer generell og fleksibel. Dette kan både gjøre det enklere å lage neste versjon og gi mulighet til variasjoner i spillet.

versjon 1 inneholdt sekvensen tall mellom 1 og 9, men det er ikke noe i veien for å tillatte flere enn disse ni verdiene. Verdiene trenger egentlig ikke være tall heller, det kan være hva som helst bare en kan sjekke dem mot det brukeren gjentar. I en grafisk versjon kan det f.eks. være aktuelt å klikke på figurer med ulike fasonger, farger og/eller motiver. Logikken i Memory-objektet blir essensielt den samme. Forskjellen er hovedsaklig at når en skal utvide sekvensen, så genereres et tilfeldig tall (som før) som brukes til å plukke et element fra en liste med alle mulige verdier. Denne lista er det naturlig å oppgi når Memory-objekter opprettes. Klassediagrammet for Memory under, viser hva som er endret:

MemoryList<?> possibleItemsList<?> expectedItemsint acceptedCountMemory(Collection<?>)int getItemCount()int getAcceptedCount()? nextItem()Boolean acceptItem(?)

Hva skal ? erstattes med?

Vi ser at Memory-klassen nå inneholder en liste over alle mulige verdier kalt possibleItems, som elementene i expectedItems skal plukkes fra. Vi har også lagt til en konstruktør som krever at en oppgir samlingen av mulig verdier, når Memory-objekter opprettes.

Et viktig spørsmål er valg av typer for attributtene. Den generelle regelen er å bruke så generelle typer som mulig og velge grensesnitt-klasser fremfor implementasjoner, for å gi størst frihet ved kodingen. Derfor har vi valgt å bruke List fremfor Collection for både possibleItems og expectedItems. List utvider Collection med metoder for å hente ut elementer basert på posisjon, og dette er nyttig når vi skal velge ut tilfeldige elementer fra possibleItems i nextItem() og sammenligne et nytt element fra brukeren med siste element i expectedItems i acceptItem(...). Argumentet til Memory-konstruktøren er imidlertid av typen Collection, fordi vi kun trenger å gå sekvensielt gjennom det, når vi kopierer elementene inn i den interne lista.


Både List og Collection er generiske typer, dvs. typer som kan spesialiseres til (å bare kunne inneholde) bestemte element-typer, og spørsmålet er nå hva skal disse spesialiseres til? Det mest generelle er å velge Object, slik det er vist under, fordi vi da kan støtte hukommelsesspill med alle typer objekter. Ulempen er at vi da ikke får noen typesjekk på verdiene vi gir som argumenter til konstruktøren og acceptItem. Vi får på en måte for mye frihet: Ikke var kan vi spille med alle mulige objekter, men vi kan blande ulike typer i samme spill og dermed skape rot for oss selv. Det vil f.eks. være lov å putte String-objekter inn i possibleItems og kalle acceptItem(...) med Integer-objekter, selv om det ikke gir mening:

MemoryList<Object> possibleItemsList<Object> expectedItemsint acceptedCountMemory(Collection<Object>)int getItemCount()int getAcceptedCount()Object nextItem()Boolean acceptItem(Object)

Bruker Object, så alle mulige objekter er tillatt

Memory memory = new Memory(Arrays.asList("En", "To", "Tre"));
...
Integer userInput = ...
// meningsløst, uten at det gir feil
if (memory.acceptItem(userInput) ...

Et alternativ er å gjøre Memory generisk ved å introdusere et typeparameter T og bruke typeparameteret i attributt-deklarasjonene, som vist under. Dette vil la oss spille med alle typer objekter, men hvert enkelt hukommelsesspill kan bare spilles med en bestemt type, som bestemmes (statisk) når Memory-objektet opprettes. En spesialisering med f.eks. String vil ha effekten at listene og argumentene blir spesialisert til String (der vi har brukt T), tilsvarende klassen under til høyre. Hvis vi da prøver å blande ulike typer, så vil det gi typefeil (i editoren og ved kjøring):

MemoryTList<T> possibleItemsList<T> expectedItemsint acceptedCountMemory(Collection<T>)int getItemCount()int getAcceptedCount()T nextItem()Boolean acceptItem(T)

Introduserer typeparameter T, slik at alle mulige typer kan brukes, men bare en bestemt type for hver instans.

Memory<String> memory = new Memory<String>(Arrays.asList("En", "To", "Tre"));
...
Integer userInput = ...
// meningsløst, og vil gi typefeil siden acceptItem er spesialisert til String
if (acceptItem(userInput) ... 
MemoryStringList<String> possibleItemsList<String> expectedItemsint acceptedCountMemory(Collection<String>)int getItemCount()int getAcceptedCount()String nextItem()Boolean acceptItem(String)

Effekten av å spesialisere med T=String

Her er fullstendig kode for Memory-klassen og en oppdatert MemoryProgram-klasse:

public class Memory<T> {
    private List<T> possibleItems;
    private List<T> expectedItems;
    private int acceptedCount;
    
    public Memory(Collection<T> possibleItems) {
        this.possibleItems = new ArrayList<>(possibleItems);
        expectedItems = new ArrayList<T>();
        acceptedCount = 0;
    }
    public Memory(T... possibleItems) {
        this(Arrays.asList(possibleItems));
    }
    
    public T nextItem() {
        int index = (int) (Math.random() * possibleItems.size());    // generate random index
        T nextItem = possibleItems.get(index);                        // look up value
        expectedItems.add(nextItem);    // add to sequence
        acceptedCount = 0;                // reset accepted counter
        return nextItem;                // return new item
    }
    public Collection<T> nextItems(int count) {
        Collection<T> items = new ArrayList<T>();
        while (count > 0) {
            items.add(nextItem());
        }
        return items;
    }
    
    public int getItemCount() {
        return expectedItems.size();
    }
    
    public int getItemsLeft() {
        return expectedItems.size() - acceptedCount;
    }
    public Boolean acceptItem(T item) {
        // is acceptItem called after sequence is completed
        if (acceptedCount >= expectedItems.size()) {
            return false;
        }
        // is the number input by the user correct
        if (! expectedItems.get(acceptedCount).equals(item)) {
            // if they are not the same, we indicate this by returning false object
            return Boolean.FALSE;
        }
        acceptedCount++;    // correct number, so increment counter
        // is this the last number
        if (acceptedCount == expectedItems.size()) {
            // return true object
            return Boolean.TRUE;
        }
        // otherwise return null, indicating correct value, but not finished
        return null;
    }
}

public class MemoryProgram {
    private void run() {
        Scanner scanner = new Scanner(System.in);
        do {
            Memory<Integer> memory = new Memory<Integer>(1, 2, 3, 4, 5, 6, 7, 8, 9);
            while (true) {
                int nextItem = memory.nextItem();
                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");
                System.out.println("Next item is " + nextItem);
                System.out.println("Please input " + memory.getItemCount() + " items");
                Boolean result = null;
                do {
                    int nextInt = scanner.nextInt();
                    result = memory.acceptItem(nextInt);
                } while (result == null);
                if (result == Boolean.FALSE) {
                    System.out.println("Wrong, game over");
                    break;
                }
            }
            System.out.println("Another game (true/false)?");
        } while (scanner.nextBoolean());
        scanner.close();
    }
    
    public static void main(String[] args) {
        new MemoryProgram().run();
    }
}

Her har vi lagt til to ekstra metoder i Memory, for å gjøre klassen litt mer fleksibel og enklere å bruke:

  • nextItems(int)-metoden gir muligheten til å utvide sekvensen med flere elementer om gangen
  • konstruktøren Memory(T...) bruker såkalte varargs og gjør det enklere å lage et Memory-objekt med et spesifikt sett mulige verdier

I versjon 3 brukes denne Memory-klassen i et hukommelsesspill med grafisk grensesnitt basert på JavaFX.