Snake - podział na klasy

0

Witam
Próbuje od kilku dni napisać własną wersje Snake. Staje na tym, że rysuje mi się plansza i tyle.. Postanowiłem więc zacząć od podstaw. Na początku ma to być bardzo uboga wersja tej gry a z czasem chciałbym (w ramach nauki) rozbudowywać program o nowe funkcjonalności. Przerobiłem tutorial (kod pod z tutorialu pod linkiem - https://github.com/Jaryt/SnakeTutorial/tree/master/snake). Generalnie program działa ale nie za bardzo wszystko rozumiem więc postanowiłem napisać coś podobnego od nowa. Myślę, że na początku powinienem skupić się na dobrym podziale na klasy, metody itd. Czy taki będzie odpowiedni?

Myślę, że program powinien posiadać trzy klasy:
1). Klasa Game - zawierająca tylko wątek uruchamiający gre
2). Klasa Snake - implementująca klase ActionListener, KeyListener
3). Klasa Render - dziedzicząca po JPanel

Do tego na pewno należałoby stworzyć enum Kierunek (UP, DOWN, LEFT, RIGHT) itd.
Chciałbym do budowy "ciała" węża użyc ArraListy Pointów.
Czy taki podział na klasy będzie odpowiedni? Jakie metody powinny zawierać poszczególne metody? Proszę bardzo o pomoc. Udzielając odpowiedzi proszę zwrócić uwagę na kod bazowy.

2

Postaraj się porozbijać wszystko na jak najmniejsze kawałki. Czym jest Snake w każdej grze tego typu? Jakie ma cechy?

  • Listę z pozycjami każdego punktu
  • Kierunek głowy

Czy gra na komórkę będzie miała taki sam keyListener? Tam sterowanie może być np. przez żyroskop. Czy to oznacza że te węże są inne w tamtych grach? Nie. On zawsze jest taki sam, a kontrolę możesz śmiało wywalić do innej klasy.

https://pl.wikipedia.org/wiki/SOLID_(programowanie_obiektowe) . Szczególnie pierwsza zasada.

Proponuję Ci napisać grę bez GUI a zamiast tego napisz testu. Snake nie jest grą płynną, tylko ma stałą czasową w której porusza się wąż. Np. na łatwym poziomie przesuwa się co 1 sekundę, a na trudnym 4 razy na sekundę. Stwórz klasę z takiego interfejsu:

interface Game {
 GameState update();
 void changeDirection(Direction d);
}

  • update jest wywoływane co jakiś interwał czasu i oznacza przesunięcie. Zwraca stan gry, czyli zawierasz wszystkie potrzebne informacje (czyKoniec, punkty, długość węża, stan mapy, pozycja węża).
  • changeDirection jest wywoływane w przypadku gdy gracz w jakikolwiek sposób będzie chciał zmienić pozycję węża

I to jest całe sterowanie gry. Teraz gdy stworzysz grę z odpowiednią planszą, możesz przy pomocy tych dwóch metod wykonać jakiś ruch i sprawdzić czy np. wąż się powęszył, czy może uderzył w ścianę.

Po zrobieniu konkretnych testów, możesz podpiać do tego cokolwiek tylko chcesz. Swing? Jasne. AndroidSDK żeby gra działa na komórce? Pewnie. A może od razu LibGDX? Też nie ma problemu.

0

Dzięki. Nie wiem czy dobrze zrozumiałem. Zrobiłem taki "szkielet"

import java.awt.*;
import java.util.ArrayList;

public class Snake {
    public Point head ;
    public ArrayList<Point> snakeBody = new ArrayList<Point>();
    public int direction, UP=1, DOWN=2, LEFT = 3, RIGHT =4;



    void MoveHead() {
        if (direction == UP) {
            System.out.println("Gora");
            snakeBody.add(new Point(head.x, head.y + 1));
            System.out.println("Wspołrzedne glowy: " + head.x + "," + head.y);
        }
        if (direction == DOWN) {
            System.out.println("Dol");
            snakeBody.add(new Point(head.x, head.y - 1));
            System.out.println("Wspołrzedne glowy: " + head.x + "," + head.y);
        }
        if (direction == LEFT) {
            System.out.println("LEWO");
            snakeBody.add(new Point(head.x - 1, head.y));
            System.out.println("Wspołrzedne glowy: " + head.x + "," + head.y);
        }
        if (direction == RIGHT) {
            System.out.println("LEWO");
            snakeBody.add(new Point(head.x + 1, head.y));
            System.out.println("Wspołrzedne glowy: " + head.x + "," + head.y);
        }
    }
        void DrawSnakeBody()
   for (Point point: snakeBody) {
            System.out.println(point);
        }
    } 
oraz klasę główną: 
import java.awt.*;

public class Main {

    public static void main(String[] args) {
	Snake snake = new Snake();
        snake.head  = new Point(0,0);
        snake.direction=1;
        snake.direction=1;
        snake.direction=1;
        snake.MoveHead();
        snake.DrawSnakeBody();
    }
}

i myślałem, że uzyskam efekt listy punktów. Niestety nie. Idę w dobrym kierunku? Oprócz tych klasy stworzyłem (ale póki co puste) klasy Board, Cherry. Niestety wyswietla się tylko:

Dol
Wspołrzedne glowy: 0,0
java.awt.Point[x=0,y=-1]

Process finished with exit code 0

A liczyłem na to, ze wyświetli mi się jakaś lista. Proszę o porady.

2

Na początek, poczytaj sobie o LinkedList. Jest to najbardziej odpowiednia struktura danych do tego zadania, ponieważ ruch węża polega na zabraniu jednego klocka z końca i dołożenie nowego na początku. Cała reszta się nie zmieni.

  • Stwórz enum Direction który będzie określał kierunek. Kierunek nie jest właściwością węża. Cała masa innych przedmiotów tez posiada kierunek i gdybyś chciał dodać inne do programu, to dublowałbyś kod. Dlatego dzielenie na klasy zacznij od zrobienia enuma z kierunkiem.
  • Głowa nie jest osobnym bytem. Jest po prostu pierwszym elementem na liście. Jeżeli zmienisz strukturę danych na LinkedList, będziesz mógł łatwo pobrać pierwszy element. Czyli pole head odpadnie.

Po zastosowaniu się do powyższych wskazówek, zostaną Ci tylko 2 pola:

public class Snake {
    public LinkedList<Point> body;
    public Direction headDirection;
}

To jest stanem węża. Podstawie tego kodu, możesz określić gdzie jest każda część węża, oraz przewidzieć jaki będzie następny ruch. Do rysowania wystarczy samo ciało, więc stwórz metodę która je zwróci. Powinna to być kopia tej listy, tak żeby nikt z zewnątrz nie mógł sam zmienić stanu węża. Może on tylko zobaczyć w jakim stanie jest wąż.

Na początku wąż będzie jednak bez ciała. Musisz stworzyć konstruktor tego obiektu, który jakoś zainicjuje wszystkie wartości. Może przykładowo przyjąć listę punktów początkowych a kierunek przyjąć sam. Jeżeli to zrobisz, to będziesz mógł stworzyć i zobaczyć węża, ale nie będziesz mógł się nim ruszyć. Czas więc na to.

Po pierwsze, trzeba umożliwić graczowi zmianę kierunku. Wystarczy setter ustawiający pole, chociaż możesz go nazwać jakoś lepiej.
Po drugie, musisz zrobić metodę update(), która przesunie węża. Coś, co obecnie miało robić MoveHead(). Zauważ jednak że działa ona dobrze, tylko gdyby na polu była wisienka, bo nie usuwa ogona. To też musisz przerobić.

Kilka uwag na koniec.

  • Musisz napisać własną klasę Point. Ta z AWT służy do określenia punktu w kontrolce, a nie punktu na Twojej planszy. Jeżeli kiedyś zechcesz przeportować tą grę na Androida, to pojawi się problem, bo tam tej klasy nie ma.
  • Ustawiaj jak najmniejszy dostęp. Wszystkie pola powinny być prywatne. Wyobraź sobie, że masz zrobić obiekt węża na podstawie klasy którą napisałeś, dać go swojemu koledze, a on ma go zniszczyć w jakikolwiek sposób mając dostęp do Twojej klasy. Teraz możliwości ma mnóstwo. Może ustawić pusty lub zły (np. 400) kierunek, pododawać losowe punkty w ciele, lub nawet je usunąć ciało. Jeżeli dobrze napiszesz klasę, to będzie mógł co najwyżej go przesuwać według Twoich reguł, ale nic nie zepsuje. Serio zrób taki test.
  • Żadna metoda w środku węża nie powinna niczego wypisywać na ekran. Jeżeli chcesz się dowiedzieć co się dzieje aktualnie z wężem, to pobierasz jego stan i wtedy możesz go wypisać na ekran.
  • Metody w javie piszemy w camelCase, czyli zaczynasz małą literą.

Zrób węża z tymi 2 polami i 4 metodami i wklej tutaj. Tylko zanim zabierzesz się za pisanie przestudiuj czym jest i dokładnie w jaki sposób działa LinkedList. Nie musisz szukać tylko w javowych źródłach bo to ogólna struktura danych która znajduje się w prawie każdym języku.

0

Dziękuje za pomoc. Staram się dostosować do Twoich wskazówek. Jednak napotykam na kolejne problemy. Oto co zrobiłem:

Direction:

package mati;

public enum Direction {
    UP, DOWN, LEFT, RIGHT;
}
Klasa Point:
package mati;

import java.awt.*;

public class Point {
    private int x;
    private int y;


    void setX(int x){
        this.x = x;
    }
    void setY(int y){
        this.y = y;
    }

    int getX(){
        return x;
    }
    int getY(){
        return y;
    }
    void drawPoint (Graphics g) {
        Graphics2D g2d = (Graphics2D) g;{
            g.setColor(Color.BLACK);
            g.fillRect(getX()*10,getX(),10,10); //zmienić 10 na stałą.
        }
    }
}

oraz klasa Snake:

package mati;

 import java.util.LinkedList;

 public class Snake {
    public LinkedList<Point> body;
    public Direction headDirection;

    Snake(){
        Point head = new Point();
        head.setX(1);
        head.setY(1);
        body.addFirst(head.getX(),head.getY());
        headDirection = Direction.DOWN;
    }
    void returnBody(){
        LinkedList SnakeBody = new LinkedList<Point>();
        SnakeBody = (LinkedList) body.clone();
        System.out.println("SnakeBody:" + SnakeBody);
    }
}

W tym miejscu coś jest nie tak.

body.addFirst(head.getX(),head.getY());

Podejrzewam, że chodzi o typ danych ale nie wiem jak to przebrnąć.

0

lista body reprezentuje zbiór obiektów klasy Point, nie możesz w metodzie addFirst przekazać dwóch intów wyciągniętych z getterów. ponadto jedynie deklarujesz listę - nigdzie jej nie inicjalizujesz.

Snake(){
    Point head = new Point();
    head.setX(1);
    head.setY(1);
    body = new LinkedList<Point>();
    body.addFirst(head);
    headDirection = Direction.DOWN;
}

metoda returnBody w obecnej formie także nie robi zupełnie niczego. jeżeli chcesz, by zwracała listę punktów - zmień jej typ na odpowiedni i dodaj returna.

0

przerobiłem klasę na:
i

mport java.util.LinkedList;



public class Snake {
    public LinkedList<Point> body;
    public Direction headDirection;

    Snake(){

        Point head = new Point();
        body = new LinkedList<Point>();
        body.addFirst(head);
        headDirection = Direction.DOWN;

    }

    LinkedList returnBody(){
        LinkedList<Point> SnakeBody;
        SnakeBody = (LinkedList) body.clone();
        return (LinkedList) SnakeBody;
    }

    void update() {
        if (headDirection==Direction.DOWN) {
            //??
        }
        if (headDirection==Direction.UP) {
            //
        }
        if (headDirection==Direction.LEFT) {
            //
        }
        if (headDirection==Direction.RIGHT) {
            //
        }
        }
    }

Jak spowodować ruch węża? Rozumiem, że jeżeli kierunek DOWN to powinien zostać narysowany/stworzony punkt poniżej. Wiem też ze sam kierunek nie jest jedynym warunkiem, powinienem dodać jeszcze, że jeżeli wąż porusza się w dół to nagle nie zmieni kierunku w gore. To rozumiem. Kwestia jak przełożyć myśl w kodzie. Może klasę Point powinienem jakoś przerobić? Bo nie wiem jak napisać fragment kodu który w powinien robić: Jeżeli kierunek DOWN to przesuń głowę węża w dół i usuń ostatni element. Na razie pomijam fakt , że jak wąż zje to musi się powiększyć. Usunięcie z listy ostatniego elementu nie jest problem ale nie wiem jak zrobić dodanie nowego elementu do listu o współrzędnych poprzedniego np. +1 w x czy 1 w y. Coś co robiło wcześniej: snakeBody.add(new Point(head.x, head.y - 1));

1

Klony Snake zostały chyba już obrobione z każdej strony w każdym języku programowania. Naprawdę, w sieci masz mnóstwo przykładów tej gry. Wystarczy poszukać.

0

O to chodziło?

import java.util.LinkedList;

public class Snake {
    public LinkedList<Point> body;
    private Direction headDirection;
    
Snake(){
   startGame();
}

    void startGame() {
        Point head = new Point();
        head.setX(0);
        head.setY(0);
        body = new LinkedList<Point>();
        //body.addFirst(head);
        headDirection = Direction.DOWN;
        Controller controller = new Controller();

    }
    void setDirection(Direction headDirection){
        this.headDirection = headDirection;
    }


    LinkedList returnBody() {
        LinkedList<Point> SnakeBody;
        SnakeBody = (LinkedList) body.clone();
        return (LinkedList) SnakeBody;
    }

    void moveSnake(int dx, int dy){
        if(headDirection==Direction.DOWN && headDirection!=Direction.UP){
                dx = 0;
                dy = -1;
        }
        if(headDirection==Direction.UP && headDirection!=Direction.DOWN){
            dx = 0;
            dy = 1;

        }
        if(headDirection==Direction.LEFT && headDirection!=Direction.RIGHT){
            dx = -1;
            dy = 0;
        }
        if(headDirection==Direction.RIGHT && headDirection!=Direction.LEFT){
            dx = 1;
            dy = 0;
        }
    }

}

klasa Controller:

import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;

public class Controller implements KeyListener {
        Snake snake = new Snake(); 
    
    @Override
    public void keyTyped(KeyEvent e) {
    }

    @Override
    public void keyPressed(KeyEvent e) {
        int i = e.getKeyCode();

        if ((i == KeyEvent.VK_W || i == KeyEvent.VK_UP))
        {
            snake.setDirection(Direction.UP);
        }

        if ((i == KeyEvent.VK_S || i == KeyEvent.VK_DOWN))
        {
            snake.setDirection(Direction.DOWN);
        }

        if ((i == KeyEvent.VK_A || i == KeyEvent.VK_LEFT))
        {
            snake.setDirection(Direction.LEFT);
        }


        if ((i == KeyEvent.VK_D || i == KeyEvent.VK_RIGHT))
        {
            snake.setDirection(Direction.RIGHT);
        }
    }

    @Override
    public void keyReleased(KeyEvent e) {

    }
}

co dalej :(?

0
public class Snake {
    private LinkedList<Point> body;
    private Direction headDirection;

    public Snake(List<Point> startState){}
    
    public List<Point> getState(){}
    public void changeDirection(Direction newDirection){}

    public void update(){}
}

To jest klasa o której pisałem. Nie musisz dodwać nic więcej i nie może być niczego mniej. Wystarczy że napiszesz te metody i będziesz miał w pełni testowalnego węża.
Próbujesz zrobić za dużo rzeczy naraz. Pisałem że GUI jest Ci na początku nie potrzebne, a Ty uparcie dorzucasz klasy z Swinga. Dopiero raczkujesz, więc nie porywaj się z motyką na słońce. Najpierw skończ tą klasę i napisz konkretne testy do niej. Jak skończysz i będziesz pewny że nikt z zewnątrz nie jest w stanie zepsuć stanu już stworzonego węża, ani stworzyć nieprawidłowego, to wtedy zacznij pisać kolejną klasę.

Direction - jest ok
Point - jest ok (dlaczego, wyczytaj powyżej)
Snake - uzupełnij to co jest tutaj.

Uzupełnij klasę i dopisz konkretne testy. Jak to zrobisz, wrzuć kod i pomyślimy co dalej.

0

Nie za bardzo czuje czym miałyby się różnic metody changeDirection a update?

1

changeDirection zmienia kierunek węża i będzie wywoływana w jakiś sposób przez gracza. Jedyne co ma zrobić to zmienić kierunek głowy.
update ma przesunąć węża na podstawie obecnego stanu i kierunku w którym zwrócona jest głowa.

0

Napisałem metody update oraz changeDirection i chciałbym je przetestować. Jak to zrobić? Utworzyć metodę main i tam probować zrobić jakieś "symulacje"? Problem mam też z :

  public Snake(List<Point> startState){
        startState = new LinkedList<>();
        headDirection=Direction.DOWN;
    }

    public List<Point> getState(){

    return body;
}

Gdzie powinna znajdować się linijka kodu "List<Point> body = new LinkedList<>();" . Patrze na kod i wszędzie mam tylko deklarację tych list.. I jak robie coś w metodzie main to mam wrażenie , że odnosi sie to do innych list niż powinno.

0

Jeżeli korzystasz z tego kodu od @krzysiek050

public class Snake {
    private List<Point> body;
    private Direction headDirection;
 
    public Snake(List<Point> startState){}
 
    public List<Point> getState(){}
    public void changeDirection(Direction newDirection){}
 
    public void update(){}
}

To body powinno chyba być ustawione przez parametr w konstruktorze. Czyli

List<Point> state = new LinkedList<>();
new Snake(state);

Wtedy musiałbyś zrobić żeby Snake dostawał LinkedList w konstruktorze.

0

Nie za bardzo zrozumiałem. Mam taki kod. Oczywiście metoda update pierwotnie w ogole inaczej wyglądała ale przestała działać więc chciałem sprawdzić tylko czy doda mi sie punkt do listy.. Niestety wyskakuje komunikat: Exception in thread "main" java.lang.NullPointerException
at mati.Snake.update(Snake.java:51)
at mati.Snake.main(Snake.java:59)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)

import java.util.LinkedList;
import java.util.List;

public class Snake {
    private LinkedList<Point> body;
    private Direction headDirection;

    public Snake(List<Point> startState){

        startState=body;
        headDirection=Direction.DOWN;


    }

    public List<Point> getState(){

    return body;
}

    public void changeDirection(Direction newDirection){
       if(newDirection==Direction.LEFT && headDirection!=Direction.RIGHT){
           headDirection=Direction.LEFT;

       }

        if(newDirection==Direction.RIGHT && headDirection!=Direction.LEFT){
            headDirection=Direction.RIGHT;

        }

        if(newDirection==Direction.UP && headDirection!=Direction.DOWN) {
            headDirection = Direction.UP;

        }

        if (newDirection == Direction.DOWN && headDirection != Direction.UP) {
            headDirection = Direction.DOWN;

            }

        }

    public void update(){

        Point head = new Point();
        head.setX(1);
        head.setY(1);
        body.addFirst(head);

    }


    public static void main(String[] args) {
        List<Point> state = new LinkedList<>();
        Snake snake = new Snake(state);
        snake.update();
    }


}
0

LinkedList body, powinna być stworzona w konstruktorze na podstawie listy podanej w parametrze. Parametr nie musi być jednak koniecznie typu LinkedList (co sugeruje sygnatura konstruktora), bo i tak nie możesz użyć tej listy bezpośrednio. Powody są 2.

  1. Ktoś stworzy węża przy pomocy listy, a potem może ją modyfikować z zewnątrz psując stan węża.
  2. Ktoś może podać totalne bzdury, np. wąż w 2 częściach nie połączonych ze sobą lub losowy zestaw punktów.

Ok, widzę że jesteś totalnie zielony, to zaczniemy od początku. Po pierwsze, testy. Poczytaj o nich na wiki https://pl.wikipedia.org/wiki/Test_jednostkowy. Następnie ściagnij i dodaj do swojego projektu bibliotekę TestNG http://testng.org/doc/index.html, oraz oczywiście przerób tutorial.

Teraz skupmy się tylko na tworzeniu węża. Usuń wszystkie metody z tej klasy, zostaw konstruktor i oba pola. Do konstruktora dodaj też parametr na początkowy układ głowy, bo DOWN nie zawsze jest prawidłowy. Jak zacząłem myśleć o testach to na to wpadłem ;). Stwórz nową też klasę wyjątku SnakeCreationException dziedziczącą po RuntimeException. Będzie ona wyrzucana z konstruktora jeżeli ktoś przekaże do niego bzdury. Teraz stwórz przy pomocy TestNG klasę SnakeTest która będzie testować czy wszystko co chciałeś by działo się z wężem się dzieje. W klasie SnakeTest stwórz 2 metody testowe.

  • testSucceedCreation - metoda tworzy różne warianty węży które posiadają poprawne dane wejściowe i tworzenie się udaje.
  • testFailedCreation - metoda tworzy różne warianty węży które posiadają niepoprawne dane wejściowe, tworzenie nie udaje się i oczekują że zostanie wyrzucony SnakeCreationException

Zrób kilka zestawów testowych dla obu metod i dopiero wtedy zacznij pisać konstruktor. Jeżeli oba testy dla wszystkich zestawów przejdą, to znaczy że wąż tworzy się poprawnie.

Przykłady danych testowych:

  • [ [1, 1], [1, 2], [1, 3] ], DOWN) - Poprawny
  • [ [1, 1], [1, 2], [1, 3] ], RIGHT) - Niepoprawny - ciało ok, ale głowa skierowana w stronę reszty tłowia
  • [ [1, 1], [1, 2], [1, 4] ], DOWN) - Niepoprawny - rozszczepione ciało
  • [ [1, 1], [1, 2], [1, 2] ], DOWN) - Niepoprawny - 2 kawałki ciała w tym samym miejscu.
0

@krzysiek050

  1. Testy dla kogoś kto ma problem z pisaniem węża? Nie komplikuj mu.
  2. Wyjątek rzucany w konstruktorze? Co to za pomysł?
  3. Twoje 2 podpunkty to dziwny pomysł. Inversion of Control? Skoro piszesz o testach to masz pomysł jak wtedy stestujesz węża skoro wąż sam sobie tworzy ciało? Ehh..
1

Ad1. A czemu nie testy? Przecież to jest podstawa podstaw. Dzięki temu chociaż nauczy się pisać poprawnie. Zaczynając od testów będzie dobrze projektował klasy, czego bez testów nie da się tak łatwo osiągnąć. IMHO testowanie jednostkowe to jedna z najważniejszych czynności których powinni uczyć w książkach przed wprowadzeniem wątków, GUI, Strumieni czy czegokolwiek poza podstawami.
Ad2. Co jest złego z rzucania wyjątku w konstruktorze? Jak inaczej ustrzec się przed zbudowaniem błędnie zbudowanego obiektu?
Ad3. Tutaj nie ma IoC. Konstruktor ma przyjąć listę, sprawdzić czy jest prawidłowa, i jeżeli tak to stworzyć jej kopię żeby nikt z zewnątrz nie mógł popsuć obiektu. Testować jest mega prosto. Jeżeli dla ''new Snake(list, direction)" wyrzuci wyjątek dla błędnych parametrów, a poprawnie utworzy obiekt dla poprawnych, to konstruktor jest ok.

0
krzysiek050 napisał(a):

Ad1. A czemu nie testy? Przecież to jest podstawa podstaw. Dzięki temu chociaż nauczy się pisać poprawnie. Zaczynając od testów będzie dobrze projektował klasy, czego bez testów nie da się tak łatwo osiągnąć. IMHO testowanie jednostkowe to jedna z najważniejszych czynności których powinni uczyć w książkach przed wprowadzeniem wątków, GUI, Strumieni czy czegokolwiek poza podstawami.

Ja też uważam, że nie ma co uszczęśliwiać na siłę. Jestem ciekaw, czy Ty pisałeś od samego początku z testami? Poza tym myślę, że 95% (nie poparte żadnymi badaniami :P) początkujących i pisałoby te testy źle.

1

Nie pisałem. Dlatego pisałem kod tak słaby, że to forum i tak ocenzuruje prawdziwy przymiotnik określający jego jakość ;). Zgodzę się że wiele osób miałoby problem z tym jak powinny wyglądać testy, ale tutaj na forum, sam podpowiadam ich zawartość, a jak zobaczę implementację to wypowiem się co jest ok a co nie.

2
krzysiek050 napisał(a):

Ad2. Co jest złego z rzucania wyjątku w konstruktorze? Jak inaczej ustrzec się przed zbudowaniem błędnie zbudowanego obiektu?

Odwrócę pytanie - a co jest dobrego w rzucaniu wyjątku w konstruktorze?

Konstruktor ma na celu utworzenie obiektu, czyli ustawienie jego początkowego stanu, wartości pól i zależności do innych obiektów. W konstruktorze nie powinno być nic poza przypisaniami, nie da się tu nic schrzanić, więc nie ma nawet jak rzucić wyjątku.

A jeśli do utworzenia obiektu potrzebne są jakieś dane z zewnątrz tudzież przeprowadzenie jakichś obliczeń, to wszystkie tego typu operacje powinny się znaleźć w metodzie tworzącej albo fabryce. Konstruktor to nie miejsce na czytanie z bazy, plików albo pytanie użytkownika o dane.

0

Jeszcze dodam że ten kod

public void changeDirection(Direction newDirection){
       if(newDirection==Direction.LEFT && headDirection!=Direction.RIGHT){
           headDirection=Direction.LEFT;
 
       }
        if(newDirection==Direction.RIGHT && headDirection!=Direction.LEFT){
            headDirection=Direction.RIGHT;
 
        }
        if(newDirection==Direction.UP && headDirection!=Direction.DOWN) {
            headDirection = Direction.UP;
 
        }
        if (newDirection == Direction.DOWN && headDirection != Direction.UP) {
            headDirection = Direction.DOWN;
        }
}

Można by fajnie przekształcić w to

        if (newDirection != headDirection.opposite()) {
            headDirection = newDirection;
        }

Tutaj kod enuma Direction http://4programmers.net/Pastebin/5797

0

Widzę, że niezła dyskusja się tu toczy :).
wklejam póki co kod:

import java.util.LinkedList;
import java.util.List;

public class Snake {
    private LinkedList<Point> body;
    private Direction headDirection;

    public Snake(List<Point> startState){
        body= (LinkedList<Point>) startState;
        headDirection=Direction.DOWN;
    }
    public List<Point> getState(){
    return body;
}

    public void changeDirection(Direction newDirection){
        if (newDirection != headDirection.opposite()) {
            headDirection = newDirection;
        }
        }

    public void update(){

        Point cherry = new Point();
        Point head = new Point();
        cherry.setX(-0);
        cherry.setY(-1);

        if(headDirection==Direction.DOWN) {

            head.setX(head.getX());
            head.setY(head.getY() - 1);

            if (head.getX() != cherry.getX() && head.getY() != cherry.getY()) {
                body.addFirst(head);
                body.removeLast();
            } else {
                body.addFirst(head);
            }
        }
        if(headDirection==Direction.UP){

            head.setX(head.getX());
            head.setY(head.getY()+1);
            if (head.getX() != cherry.getX() && head.getY() != cherry.getY()) {
                body.addFirst(head);
                body.removeLast();
            } else {
                body.addFirst(head);
            }
        }
        if(headDirection==Direction.LEFT){

            head.setX(head.getX()-1);
            head.setY(head.getY());
            if (head.getX() != cherry.getX() && head.getY() != cherry.getY()) {
                body.addFirst(head);
                body.removeLast();
            } else {
                body.addFirst(head);
            }
        }
        if(headDirection==Direction.RIGHT){

            head.setX(head.getX()+1);
            head.setY(head.getY());
            if (head.getX() != cherry.getX() && head.getY() != cherry.getY()) {
                body.addFirst(head);
                body.removeLast();
            } else {
                body.addFirst(head);
            }
        }

    }

    public static void main(String[] args) {
        List<Point> state = new LinkedList<>();
        Snake snake = new Snake(state);

        snake.changeDirection(Direction.DOWN);
        snake.update();
        System.out.println(snake.body.size());
    }

}

@TomRiddle Dzięki, fajny myk.

Co do kodu. To na pewno trzeba dodać metodę losując jedzenie dla węża. Skrócić jakoś metodę update (dużo powtarzającego się kodu) oraz dodać gdzieś poza metodą punkt head, bo w tym momencie to przy każdym wywołaniu metody update pojawi się nowa głowa. A przecież update ma być wywoływana co jakiś interwał czasu. W dodatku jeżeli ustawiłbym się jedzeniem na punkt np. [-2,-2] to wpisując dwa razy kierunek DOWN i dwa razy kierunek LEFT to i tak za każdym razem będzie tworzyła się głowa o punktach 0,0 i potem dopiero zmieniał kierunek o jeden krok i znowu tworzył głowe 0,0 itp. Po prostu póki co działa to niestety tylko na systemie 0-1. Może jednak wygodniejszym rozwiązaniem byłoby dodanie Head jako pola klasy Snake?

Co do testów to trochę mnie zakręciliście. Jedno jest pewne, na pewno lepsze to niż testowanie tego w metodzie main jak dotąd. Czy testować należy konstruktor to skoro Wynie jesteście zgodni co do tego to ja tym bardziej nie wiem :). Z tego co dzisiaj przeczytałem, zobaczyłem na temat testów to myślę, że w wężu powinno się porobić testy typu: czy jeżeli wąż znajdzie jedzeni to czy jego długość się powiększa, czy wąż może znaleźć się poza planszą itd. Wydawało mi się, że Snake będzie się tworzyło tylko raz.

Co do parametru Direction w konstruktorze Snake to oczywiście mogę dodać. Zakładem, że kierunek DOWN jest kierunkiem początkowym a każdy kolejny jest wywoływany przez gracza. A kwestia, że kiedy wąż porusza się w lewo to nagle w prawo skręcić nie może to już jest chyba rozwiązana.

2

Wiesz że z tych 4ech ifów mógłbyś wyciągnąć ten kod..
.

if (head.getX() != cherry.getX() && head.getY() != cherry.getY()) {
                body.addFirst(head);
                body.removeLast();
            } else {
                body.addFirst(head);
            }

...i dać go niżej? Ponieważ masz 4 razy ten sam kod wklejony.

0

@somekind Nikt nigdy nie powiedział że w konstruktorze ma się dziać tylko przypisanie pól. Konstruktor ma za zadanie stworzyć spójny obiekt a jeżeli nie sprawdzisz tego co wywołujący nam przekazał to nie masz takiej pewności. Z bazy nie powinien ciągnąć, bo łamie single responsibility principle, ale sprawdzenie czy ktoś nie podał błędnych danych inicjalnych już nie. A co do Twoich rozwiązań, w obu trzeba zmienić konstruktor na prywatny żeby mieć pewność że to Ty wywołasz go z prawidłowymi parametrami.

  • Można to wywalić do osobnej konstruującej metody statycznej a konstruktor uczynić prywatnym i wywoływać go gdy metoda zwaliduje parametry. Tylko po co? Ta sama klasa, ten sam efekt.
  • Można też wywalić do fabryki. W Javie nie ma friends czy innych przydatnych przy tworzeniu fabryk składni, więc jedyna opcja żeby skorzystać z prywatnego konstruktora to zrobienie statycznej klasy wewnętrznej fabryki. Oczywiście nie będzie to fabryka abstrakcyjna, bo tutaj to w ogóle by były schody i zabawa z dostępem pakietowym, ale na szczęście w tym wypadku snejk będzie jeden.
    W obu przypadkach powstanie sporo bezsensownego kodu, bo to samo uzyskasz używając konstruktora do konstrukcji poprawnego obiektu zamiast dowolnego.
0

@krzysiek050 chyba nigdy nie pisałeś nic dużego, nie?

Właśnie o to chodzi że konstruktor ma tylko ustawiać pola i nie mieć ani logiki ani żadnych wyjątków w sobie. To nie miejsce na jakąś walidacje danych. Jeżeli uważasz że konstruktor przyjmuje int, a Ty chcesz tylko liczby naturalne to ten wyjątek ma iść poziom wyżej, a nie do konstruktora.

Poza tym jak byś to chciał testować? Pisać ochydnego new Snake() w try catchu? Mockito tego nie umie, więc jak?

2

Przez parę lat w pracy siedzę i piszę snejki. Nic większego nie napisałem.

Z takim podejściem po co w ogóle hermetyzacja obiektów, skoro można je stworzyć i przechowywać w nieprawidłowym stanie? Równie dobrze mógłbyś wszystkie pola ustawić publicznymi skoro i tak można zrobić co się chce, a wtedy wyjdzie Ci zwykły struct i programowanie strukturalne. Programowanie obiektowe ma tą zaletę że operuje na obiektach spójnych. Masz obiekt Snake, to wiesz że jest prawidłowy, a nie być może o ile ktoś kto go skonstruował podał poprawne dane.

Poza tym jak byś to chciał testować? Pisać ochydnego new Snake() w try catchu? Mockito tego nie umie, więc jak?

I właśnie po to sugeruję żeby pisał testy od początku. Przynajmniej nauczy się czym są testy jednostkowe i do czego służą odpowiednie narzędzia. Bo Mockito, jest do izolowania testowanej klasy od zewnętrznych zależności. Widzisz w tym konstruktorze jakąkolwiek klasę którą trzeba byłoby mockować?

Ale odpowiadając na pytanie.

    @DataProvider("invalidInitialData")
    @Test(expectedExceptions = SnakeCreationException .class)
    public void testFailedCreation(List<Point> initialBody, Direction initialHeadDirection) {
        new Snake(initialBody, initialHeadDirection);
    }

    @DataProvider("validInitialData")
    @Test
    public void testSucceedCreation (List<Point> initialBody, Direction initialHeadDirection) {
        new Snake(initialBody, initialHeadDirection);
    }

Skoro klasa nie jest zależna od innej, wystarczy użyć samego TestNG. Wypełnij providery i test gotowy.

0

Zgodzę się z tym, że w konstruktorze nie powinno być logiki. Konstruktor ma stworzyć obiekt. Ale nie zgodzę się, że konstruktor nie może rzucać wyjątków. Otóż może, w przypadku otrzymania niepoprawnych parametrów. Może i stacktrace w przypadku tworzenia przez kontener CI nie wygląda pięknie, ale konstruktor gwarantuje poprawny stan obiektu. Tak na prawdę to częściej mi się zdarzy, że CI wyrzuca wyjątek ze względu na brak potrzebnej zależności, więc to on mi zwykle waliduje parametry, ale na jedno wychodzi.

Dodam jeszcze, że nie opakowuję każdego wywołania konstruktora w try..catch. Łapię wyjątki tam gdzie mogę je obsłużyć, czyli najczęściej na początku łańcucha wywołań.

1

Mój błąd chyba, że włączyłem się do tej dyskusji w trakcie, nie czytając tego co było wcześniej.

Oczywiście - konstruktor ma stworzyć spójny obiekt, i zgadzam się z tym, że może walidować argumenty wejściowe - jak i rzucać wyjątki, jeśli argumenty są niepoprawne. Mam tu na myśli wyjątku w rodzaju ArgumentNullException, ArgumentException, ArgumentRangeException, czyli te związane z wartościami argumentów. Ale nic poza tym, żadnej logiki w rodzaju sprawdzania czy argument userLogin istnieje w bazie, albo czy filePath istnieje na dysku albo zawiera poprawne dane. Celem konstruktora jest poprawne ustawienie wartości pól, nie zaawansowana walidacja danych, nic więcej.

Na przykładzie - klasa Bitmap niech ma:

  1. konstruktor przyjmujący tablicę bajtów, który sprawdza czy podana tablica nie jest nullem;
  2. konstruktor, który przyjmuje szerokość oraz wysokość bitmapy, i sprawdza czy obie te wartości nie są ujemne.
    Ale nie powinno być konstruktora, który przyjmuje np. ścieżkę do pliku, odczytuje ten plik i na jego podstawie tworzy bitmapę. W takiej sytuacji, potrzebna jest statyczna metoda FromFile, która zajmie się operacjami plikowymi, a jeśli wszystko będzie ok, zwróci obiekt korzystając z konstruktora nr 1.
0

Nie czuję tego testng. Nie mogłem znaleźć jakiegoś prostego turorialu, który od początku do końca zrobił by test jednostkowy.. przynajmniej dla mnie w zrozumiały sposób. Nie wiem czy to istotna informacja ale korzystam z IDEA intellij. Można to samo osiągnąć w junit? Bo konstrukcja arrange, act, assert jeszcze jakoś dla mnie zrozumiale wygląda.

1 użytkowników online, w tym zalogowanych: 0, gości: 1