5 svar
164 visningar
Ygolopot behöver inte mer hjälp
Ygolopot 215
Postad: 11 mar 2022 19:26 Redigerad: 11 mar 2022 20:41

Java klass-struktur

Hej!

Har klasserna:

CLI

Game

Player

HumanPlayer extends Player

Computer extends Player

I CLI körs en spel-loop och den enda klassen CLI interagerar med är Game. Först hade spelet bara två objekt av klassen Players men jag vill lägga till möjligheten att spela mot datorn. I Game har jag initialized:

Player current;

Som är lika med det objekt vars tur det är att göra ett drag. Nu när jag dock har skapat två subklasser HumanPlayer och Computer får jag problem. Jag har en metod som bara finns för Computer som automatiserar Computers val av drag, men jag vet inte hur jag ska nå den metoden.

Jag vill använda: current.makeChoice();

Men makeChoice finns bara för Computer-objekt så jag vet inte hur jag ska nå metoden via en variabel som bara alternerar mellan: current = compuer och current = humanplayer.

Listan players har ett Computer objekt och ett Player objekt, men kan bestå av vad som helst, n st players och n st computers (är tanken iaf).

Hur löser jag det här?

Tack på förhand! :)

Edit: La till alla klasser här för tydlighetens skull. Spelet jag bygger är skittråkigt, målet är att bli bekväm med inheritence osv. Hade ju kunnat lägga metoden som nu är unik för Coputer i Player-klassen och bara kalla på de när spelaren är en Computer men det känns som en rätt ful lösning.

public class Game {

    private final int winPts;
    private final Dice dice;
    private final List<Player> players;
    private Player current;


    public Game(Dice dice, List<Player> players, int winPts){
        this.dice = dice;
        this.players = players;
        this.winPts = winPts;
        current = players.get(0);
    }


    public boolean roll(){
        int result = dice.roll();
        if(result != 1){
            current.addRoundPoints(result);
            return true;
        }
        return false;
    }

    public void clearCurrent(){
        current.clear();
    }

    public void next(){
        current = players.get((players.indexOf(current)+1)%players.size());
    }

    public int getLastDiceResult(){
        return dice.getLastResult();
    }

    public void updateCurrentTotalPoints(){
        current.updateTotalPoints();
    }

    public Player getCurrent(){
        return current;
    }

    public List<Player> getPlayers(){
        return players;
    }

    public boolean hasWinner(){
        return current.isWinner(winPts);
    }

    public String computerChoice(){
        return current.makeChoice(winPts);
    }
}
public class Player {
    // A player will never change name during the game
    private final String name;
    private int roundPoints;
    private int totalPoints;


    public Player(String name){
        this.name = name;
    }

    public boolean isWinner(int winPoints){
        return roundPoints + totalPoints > winPoints;
    }

    public int getRoundPoints(){
        return roundPoints;
    }

    public int getTotalPoints() {return totalPoints; }

    public void addRoundPoints(int result){
        roundPoints += result;
    }

    public void updateTotalPoints(){
        totalPoints = totalPoints + roundPoints;
        roundPoints = 0;
    }

    public void clear(){
        roundPoints = 0;
    }


    //For the CLI:
    public String getName(){
        return name;
    }

    @Override
    public String toString(){
        return "{" + name + " roundPoints = " + roundPoints + " and totalPoints = " + totalPoints + "}";
    }

    @Override
    public boolean equals(Object o){
        if (this == o) return true;
        if(o == null || getClass() != o.getClass()) return false;
        Player player = (Player) o;
        return  Objects.equals(name, player.name);
    }

    @Override
    public int hashCode(){
        return Objects.hash(name);
    }


}
public class Computer extends Player {
    final Random rand = new Random();
    public Computer(String name) {
        super(name);
    }

    //Computer make a choice depending on rolling score
    public String makeChoice(int winPoints) {
        int index;
        if (getRoundPoints() < 7) {
            char[] choices = {'r', 'r', 'r', 'r', 'r', 'r', 'n'};
            index = rand.nextInt(choices.length);
            return Character.toString(choices[index]);
        } else if (getRoundPoints() >= 7 && getRoundPoints() < 24) {
            char[] choices = {'r', 'r', 'n'};
            index = rand.nextInt(choices.length);
            return Character.toString(choices[index]);
        } else {
            return "n";
        }
    }
}
public class HumanPlayer extends Player{

    public HumanPlayer(String name) {
        super(name);
    }
}
Programmeraren 3390
Postad: 11 mar 2022 21:55

Inte satt mig i all kod men om makeChoice() är vettigt för Player bör den finnas där. Den kan ha en tom implementation om inte alla Player-subtyper ska göra något. Computer har sin egen implementation som gör det den ska.

Om just makeChoice bara passar på Computer och inte på alla Player-typer kanske det finns någon annan mer generisk metod som passar för Player. t ex step() son anropas en gång per iteration för varje Player.

Lite abstrakt svar men orkade inte läsa koden :-)

CurtJ 1203
Postad: 12 mar 2022 07:22

Tanken med arv är att liknande klasser delar på gemensam funktionalitet. I ditt fall innehåller klassen Player allt som är gemensamt för HumanPlayer och Computer. Om du nu vill göra spelmotorn oberoende av vilken typ av spelare som finns i listan så ska spelmotorn hantera objekt av typen Player och alla metoder den använder måste då finnas i den klassen. Om någon av subklasserna har ett avvikande beteende för någon metod så "skriver man över" den metoden i subklassen.

Exempel:

Din spelmotor har en lista av Player som den initierar mha en klass som läser in en textfil och skapar olika spelare. Spelmotorn vet inget om de specifika spelarna utan den kör spelet med Player. När den ska flytta spelarens "gubbe" på spelplanen så anropar den Player.makeChoice() som returnerar positionen den vill flytta till. Basklassen Player kanske inte gör något i den metoden utan den förutsätter att subklasserna har en implementation som utför arbetet. Detta kan tvingas fram genom att basklassen Player har en abstract metod makeChoice men den kan  också ha en riktig implementation som gör något vettigt för de flesta subklasserna men för de få som har ett avvikande beteende så måste de implementera en metod för detta.

Vi förutsätter nu att Computer slumpar fram en förflyttning och implementerar det i makeChoice(). För HumanPlayer kanske man frågar en människa vad den väljer att göra i den metoden. För spelmotorn spelar det ingen roll, den gör vad den ska baserat på svaret. Det här är ett sätt att renodla ansvaret så att spelmotorn kan logiken i spelet, Computer vet hur  den flyttar sin "gubbe" och HumanPlayer vet hur den flyttar sin. 

Blir det lite klarare så?

Ygolopot 215
Postad: 12 mar 2022 07:42 Redigerad: 12 mar 2022 07:44

Grymt, tack för svaren båda! Blir mycket klarare så. En fundering kring abstrakta klasser, borde jag göra Player till en abstrakt klass här? Och i så fall, är det enda syftet med att göra den abstrakt att jag inte ska glömma av mig själv och råka skapa ett Player objekt eller finns det någon djupare teknisk betydelse mer än att man inte kan skapa någon objekt av en abstrakt klass? 

Jag har nu även ändrat koden så att jag har makeChoice() i Player och en speciell implementering av metoden i Computer och HumanPlayer, makeChoice är tom i Player, borde jag göra metoden abstrakt då för att markera att den ska vara tom?

Är generellt ute efter om det är good practice eller viktigt av andra mer tekniska skäl

CurtJ 1203
Postad: 12 mar 2022 07:52 Redigerad: 12 mar 2022 08:14

En klass blir abstract när den innehåller en abstract metod. Anledningen till det är att du vill tvinga alla som ärver den att implementera just den metoden. Den abstrakta metoden i basklassen har ingen implementation utan finns där bara för att den som använder basklassen ska kunna anropa den.  Implementationen finns sen i subklasserna och där är den specifik för respektive subklass.

Jag skulle gjort din klass Player abstract om du har någon metod som inte innehåller gemensam implementation för subklasserna. Har du en metod som alla subklasser implementerar olika så bör den va abstract, annars inte.

Se basklassen som ett kontrakt mellan spelmotorn och spelarna. Spelmotorn spelar mha basklassen (Player) och använder alla metoder i den.  Behöver du, som i ditt fall, göra något specifikt för en viss typ av spelare så använder du arv som du gjort.

Man kan uppnå det här med interface också, då har man "bara" ett kontrakt avseende vilka metoder man ska implementera för de klasser som implementerar interfacet och spelmotorn i det här fallet har en lista av just det interfacet som är populerad med objekt som implementerar interfacet. Då har man ingen gemensam kod utan alla implementerande klasser måste ha alla metoder i sig. Det går naturligtvis at4t kombinera arv och interface och jag skulle säga att det är "best practice". Arv för att dela gemensam kod och interface för att specificera ett kontrakt mellan den som använder objekten  och objektens funktionalitet. 

Det finns en avart i interface som innebär att ett interface innehåller en "default"-implementation av en metod. Den tog främst fram för att kunna utöka interface utan att slå sönder tidigare användning av interfacet.

När jag funderar på en sån här sak så brukar jag ta på mig olika "hattar". När jag skapar spelmotorn så gör jag det mot ett interface som innehåller precis det jag behöver för spelmotorn. När jag sen implementerar spelarna så skapar jag klasser som implementerar interfacet och där och då funderar jag på om de implementationerna har något gemensamt och i så fall så skapar jag basklass(-er) och subklasser, ibland i flera nivåer.  Tänk ansvar för de olika klasserna i systemet.

Ygolopot 215
Postad: 12 mar 2022 18:45

Fantastiskt svar! Tack så jättemycket!

Svara
Close