Diskuze: Game of life - začátečník
V předchozím kvízu, Online test znalostí Java, jsme si ověřili nabyté zkušenosti z kurzu.


Předně palec nahoru za dobrý přístup. Místo fňukání jdeš do učení s elánem;) šikula
a teď teda věcně:
- nervi všechno do metody main. Lepší je využít konstruktor (už jen proto, že jej můžeš v budoucnu přetěžovat). Main metodu využívej jednak ke spuštění projektu, ale hlavně když ještě potřebuješ před spuštěním nastavit nějaké procedury (využívat statických metod)
- + za používání dokuemtačních komentářů. To je veliké a významné plus.
- + určitě kladné body za koncepci oop. Krásně jsi to rozdělil do class a metod, což považuju za dobrý krůček. Pochopitelně malá poznámečka:) zkus už teď používat balíky (třeba jen jeden), ale také si na to zvykej už ze začátku. Ať máš později lehčí práci.
- + za používání camelNotice. Jestli jsi předtím dělal v C#, kde je víc zavedená konvence podtržítek v názvech, tak supr, že si zvykáš rychle na velbloudí notaci. V Javě sice lze používat i podtržítka, ale všichni programátoři vyžadují nepsané pravidlo camelNotice, takže supr
- + za používání anglických název proměnných
- za velmi stručné názvy proměnných:) rd, alive, dead.... slova sice chápu, ale k čemu vlastně jsou? (to si budeš říkat za půl roku, až se na tento projekt podíváš). Pochybuju, že budeš psát kód v texťáku. Vždycky na to budeš využívat nějaké IDE. Dneska všechny IDE bez výjimky používají dokončování slov. Takže se neboj psát velmi dlouhé proměnné. Př.:
private int number = 5; // stručné, jasné a lehké na zapamatování.
o dva roky později...
private int number = 5; // co to kurva je? k čemu ta proměnná je????
pracnější, ale po letech se takhle ptát nebudeš:
private int numberForCountingTotalValueOfMembersInABank = 5; // a když zmáčkneš CTRL+Space, tak ti IDE vždy takový název dokončí samo, ale ty se v tom vyznáš
Pokud budeš psát kód, aby nějak fungoval, tak na to ti stačí třeba i baltík. Dobrý kód poznáš tak, že se v něm nejpozději do dvou minut orientuješ.
- ta jednoduchá ifka, kterou máš v metodě GameLogic.RandomSeed(int) by se dala napsat třeba i takhle:
int rand = rd.nextInt(randOccur);
if (rand < 1) {
gameBoard1[j] = alive;
} else {
gameBoard1[j] = dead;
}
//je to samé, jako
int rand = rd.nextInt(randOccur) < 1 ? gameBoard1[j] = alive : gameBoard1[j] = dead;
je to podmínka na jeden řádek, která dělá to samé, ale.... přece jen jeden řádek je lepší než 5 se závorkama:) takže tohle je spíš info, že se to dá zapsat i jinak
- + zase chválím. Na začátečníka jak říkáš používáš for-each, což je supr;)
- ta metoda aliveNextTurn(int, char) je vcelku dost zmatkovitě napsaná. Myslím, že by to šlo napsat do jedné ifky a podmínku přetížit. Navíc koukám (kdyžtak mě oprav), že proměnná cell nabývá pouze dvou hodnot (alive a dead). Nebylo by jednodušší použít boolean a ty dva znaky X a _ nastavovat přímo tady v té podmínce? Boolean přece jen pracuje rychleji než char (v řádu milisekund) a zjednodušili by se ti rovnou dvě podmínky - v metodě aliveNextTurn(int, char) a v metodě randomSeed(int)
- předpokládám, že random nenastavuješ nikde jinde, než v konstruktoru. Obvykle v celém projektu většinou budeš používat jen jednu proměnnou třídy random (na co taky dělat více, když budou vždycky dělat to samé). Tak ji odstraň z konstruktoru, inicializuj ji přímo v deklaraci a nastav jako třídní konstantu (final static).
- v metodě newGeneration(char[][]) používáš lokální proměnné s
upperCase. Troška konvence:
- u deklarace lokálních proměnných se používá modifikátor final. Je to takové zabezpečení a máš jistotu, že tu proměnnou nikde nepřepíšeš, kde bys neměl. Pochopitelně pokud s přepsáním počítáš, tak ji tak nedaklaruj:)
- upperCase se používá pouze při třídních konstantách s podtržítkovou syntaxí. Nikde jinde se nevyužívá.
Co dále?
trošku bych Tě nasměroval, čím by ses mohl zabývat a jak rozšířit
program a případně jak myslet:)
- celá aplikace ti jede v jednom vlákně. Zkus si s tím pohrát a třeba se zaměř na funkci, aby jsi mohl dělat při běhu pauzu. (pochopitelně jestli jsi nedělal s vlákny, tak začít nejdřív tím:) )
- Pochopitelně převést celou aplikaci do desktopového programu
- Aplikuj přetěžování (minimálně v tom konstruktoru - přetěžování je jedna z nejpoužívanějších praktik OOP v praxi)
- Zabal celý projekt do balíku (extra easy, co ti může trvat max. 10 sekund:) )
- Rozepiš se v proměnných
- Komentáře ponechej jak jsou, ale připisuj k nim úpravy;) Klidně vždycky na další řádek pod to vypisuj, co jsi změnil případně proč, ale fakt Tě moc chválím za ty komenty. Šikula - nemusel jsem vůbec bádat nad tím, proč tam ta či ona metoda je. To je fakt perfektní úspěch a buď na to pyšný.
A pochopitelně rozšiřovat se dál a dál. Jestli jsi student - věnuj se tomu, jestli jsi dělňas, chvilku to vydrž v práci, věnuj se tomu a pak hledej práci v IT, protože ten potenciál máš slušný, snahu očividně taky (což je to nejdůležitější) a rozhodně se za ten kód nemusíš stydět. Rozhodně to není nic, co by sesmolil absolutní začátečník. Jen tak dál;)
+20 Zkušeností
+2,50 Kč

Jo a ještě jsem chtěl jednu věc, kterou bys mohl praktikovat.
Vždycky, když vytvoříš nějakou třídu, tak ji vyjmi z projektu, založ
nový projekt (třeba v netbeanech s zázvem JavaApplicationXY) klidně do
samého nebo do defaultního balíku a zkus, že ti tam ta třída bude
fungovat. Je to taková kontrola a jistota, že programuješ objektově.
V OOP totiž nemůžeš přemýšlet jen tak, že například podlaha drží
židli, ale že židle stojí na podlaze.
Takže když bys třeba právě tyto dvě třídy programoval, tak bys
otestoval, že na podlaze bude držet i obdelník, trojúhelník, vlak, barák,
maminka prostě všechno a nic jí tim nepropadne.
Současně tak si ověříš, že židle bude stát na stole, druhé židli, na
jablku, na slonovi prostě že se nikdy nebude vznášet nebo nebude propadat
texturou.
To byl velmi jednoduchý příklad, ale smyslem toho je, že ať už
naprogramuješ cokoliv, neměl bys to moc specifikovat a vázat na jednu
třídu. Pomocné třídy je lepší dělat soukromými;) Když třída ke
svému chodu vyžaduje nějakou jinou třídu, tak tě to právě upozorní na
to, že je čas zamyslet se, jestli neimplementovat nějaké rozhraní nebo
jestli vážně nikdo jiný než ta jedna třídu tu druhou využívat
nebude.
To jen taková finta, jak nabýt jistotu, že programuješ objektově.
Štefan Melich:16.8.2017 10:23
Ide to aj jednoduchšie. Program môže mať metódu main v každej svojej triede a iba ty rozhoduješ, ktorý main sa pri spustení bude volať. Lokálne spustenie mainu je v Netbeanse cez skratku Shift+F6. Je to rýchlejšie, ako vytvárať vždy nový projekt. Ale myšlienka je dobrá testovať sa dá aj takto zjednodušene.
Jo, ale main metoda ti nezaručí, že je třída objektová. Stejně pořád pracuješ ve stejném projektu a neupozorní tě to na nebezpečné vázání na jiné třídy. Navíc sám neuznávám více main metod v celém projektu. Ano, když máš 5 tříd, tak se v tom zorientuješ, ale hlavně v práci se s tak extrémně malým projektem nikdy nesetkáš. Pokud nemáš v sources aspoň 30 class, tak děláš pořád soukromý projekt:) (a to nepočítám testovací classy). A v každé z nich dělat main metody? by ses z toho zbláznil
Tú závislosť si vieš odsledovať a myslím si, že vždy sa to aj tak nedá. Ak to dokážeš v každom prípade tak ťa uznávam, ako skutočného odborníka. Niekedy viac mainov práve že uľahčuje orientáciu vo veľkom projekte, ale to je vec názoru (pre mňa to tak platí) a každý vytvorený main si vieš po odtestovaní odstrániť, ak je ďalej v projekte zbytočný. Mne osobne to príde logickejšie a jednoduchšie riešenie a podal som to len ako alternatívu.
Tak jsem se v tom trošku povrtal a aplikoval některé Luborovy rady. Rozhodně to vypadá líp a kód je o něco kratší ale problém je v tom, že tato verze běží o něco pomaleji než původní (pro plochu 1000x1000 při 10generacích: stará verze 66sec a nová 77sec). Otázka tedy zní: kde mám hledat problém? Zatím největší podezřelý je podle mě metoda drawGame() kde podle hodnoty prvků pole které jsou boolean vypisuje patřičný symbol.
package gameoflife;
import java.util.Random;
/**
*
* @author David Petlák
*/
public class GameLogic {
final static Random RANDOM_NUMBER_GENERATOR = new Random();
private final char alive = 'X';
private final char dead = '.' ;
private final int width;
private final int height;
private final int randOccur;
private final int numberOfGenerations;
private boolean[][] gameBoard;
public GameLogic(int randOccur, int width, int height, int numberOfGenerations){
this.randOccur = randOccur;
this.width = width;
this.height = height;
this.numberOfGenerations = numberOfGenerations;
}
/** Vytvoří a odehraje novou hru podle pravidel definovaných v konstruktoru
*
*/
public void newGame(){
/*char[][]*/ gameBoard = randomSeed(randOccur, width, height);
drawGame(gameBoard);
for(int generation = 0; generation < numberOfGenerations; generation++){
System.out.println();
System.out.println(String.format("%d generace. ", generation+1));
gameBoard = newGeneration(gameBoard);
drawGame(gameBoard);
try{
Thread.sleep(200);
}
catch(InterruptedException e){
System.out.println("Proces byl probuzen předčasně");
}
}
}
/** Vytvoří novou herní plochu a náhodně umístí živé buňky.
*
* @param randOccur poměr četnosti 1:randOccur
* @param width šířka herní plochy
* @param height výška herní plochy
* @return boolean[][] s herní plocou a generací 0
*/
private boolean[][] randomSeed(int randOccur, int width, int height){
gameBoard = new boolean[height][width];
for (boolean[] gameBoard1 : gameBoard) {
for (int j = 0; j < gameBoard[0].length; j++) {
int randomNumber = RANDOM_NUMBER_GENERATOR.nextInt(randOccur);
gameBoard1[j] = randomNumber < 1;
}
}
return gameBoard;
}
/** Vykreslí herní plochu do konzole
*
* @param gameBoard 2D pole k vykreslení
*/
private void drawGame(boolean[][] gameBoard){
for (boolean[] gameBoard1 : gameBoard) {
for (int j = 0; j < gameBoard[0].length; j++) {
char symbolToPrint = gameBoard1[j] ? alive : dead;
System.out.print(symbolToPrint + " ");
}
System.out.println();
}
}
/** Vytvoří pole pro novou generaci.
*
* @param previousGen výchozí generace
* @return 2D pole s příští generací
*/
private boolean[][] newGeneration(boolean[][] previousGen){
int newGenHeight = previousGen.length;
int newGenWidth = previousGen[0].length;
boolean[][] newGen = new boolean[newGenHeight][newGenWidth];
for(int i = 0; i < newGenHeight; i++){
for(int j = 0; j < newGenWidth; j++){
int aliveAround = checkSurroundings(previousGen, i, j);
newGen[i][j] = aliveNextTurn(aliveAround, previousGen[i][j]);
}
}
return newGen;
}
/** Projde postupně okolí jedné buňky kolem právě posuzované a sečte
* všechny živé buňky v tomto okolí
* @param previousGen Matice současné generace
* @param r Souřadnice prověřované buňky
* @param c -"-
* @return Počet živých buňěk v okolí
*/
private int checkSurroundings(boolean[][] currentGeneration, int row, int column){
int noOfCellsAlive = 0;
for(int k = row-1; k <= row+1; k++){
for(int l = column-1; l <= column+1; l++){
if(((k < 0) || (l < 0)) || ((k >= currentGeneration.length) || (l >= currentGeneration[0].length)) || ((k == row)&&(l == column))){
continue; //pokud je prověřovaná souřadnice mimo pole nebo stejná jako prověřovaná buňka tak pokračuje na další souřadnici
}
if(currentGeneration[k][l]){
noOfCellsAlive++; //pokud je buňka na souřadnicích naživu inkrementuje proměnnou
}
}
}
return noOfCellsAlive;
}
/** Posoudí, zda je buňka v příští generaci naživu.
* Pokud je živá a má 2 nebo 3 živé sousedy žije.
* Pokud je mrtvá a má 3 živé sousedy obživne
* @param aliveAround počet živých buněk v okolí
* @param cell posuzovaná buňka
* @return
*/
private boolean aliveNextTurn(int aliveAround, boolean currentGenCell){
if(currentGenCell){
return !((aliveAround < 2) || (aliveAround > 3));
}else{
return aliveAround == 3;
}
}
}
a kód třídy s main()
package gameoflife;
/**
*
* @author David Petlák
*/
public class GameOfLife {
/**
* @param args the command line arguments
*/
public static void main(String[] args) {
int randOccur = 2;
int width = 1000;
int height = 1000;
int numberOfGenerations = 10;
GameLogic gl = new GameLogic(randOccur, width, height, numberOfGenerations);
gl.newGame();
}
}
Matěj Kripner:21.8.2017 14:54
Zběžně jsem ten kód prošel a mám pár připomínek.
K návrhu:
- Smyčka by určitě neměla být v konstruktoru. Konstruktor slouží k připravení objektu pro použití.
- drawGame a newGeneration by neměly přijímat parametr gameBoard (měly by využívat atribut) a neměly by nic vracet (jen upravovat atribut gameBoard). V konstruktoru měla být logika stávající metody randomSeed, takže po vytvoření by každá hra už byla náhodně inicializovaná. Samotnou smyčku bych umístil do metody main.
- Buď konzistentní v tom co je a není konstanta. Konstanty (static final) jsou stejné pro všechny instance. To se hodí i pro alive a dead, což jsou nyní atributy (i když neměnné) a všechny 'magická' čísla, která se ti v programu objevují (200 = STEP_DELAY, 2 = MIN_ALIVE_AROUND, 3 = MAX_ALIVE_AROUND, 3 = AROUND_TO_BORN)
Drobnosti:
- Nech si kód čas od času zformátovat od IDE (bude na to zkratka). Pak se koukni jak má být formátovaná dokumentace, závorky, atd.
- Používej popisnější názvy. Např. row místo gameBoard1 v metodě randomSeed.
- Vypisuješ přes 10 * 1000 * 1000 * 3 = 30 000 000 znaků, což bude pomalé vždycky. Teď vypisuješ dvě mezery místo jedné, to by mohlo způsobit zpomalení. Optimalizace ostatních věcí je trošku těžší.
Komentář k radám od Lubor Pešek (nekompletní):
- numberForCountingTotalValueOfMembersInABank je špatný název. membersTotalValue je lepší.
Boolean přece jen pracuje rychleji než char (v řádu milisekund)
Ne
- Vlákny se zatím nemusíš zabývat.
To zní poměrně rozumně, ale pár věcí nechápu.
- Ať hledám sebevíc tak v konstruktoru žádnou smyčku nemůžu najít. (Nebo se mé pochopení toho co to je konstruktor zásadně odchyluje od reality, podle mě je konstruktor níže uvedený úsek kódu)
public GameLogic(int randOccur, int width, int height, int numberOfGenerations){
this.randOccur = randOccur;
this.width = width;
this.height = height;
this.numberOfGenerations = numberOfGenerations;
}
- Toto celkem smysl dává, matici současné generace nemusím předávat jako paramer Díky za tip.
- Takže v praxi platí, že jakákoliv konstanta je lepší zapsaná jako static proměnná? I pokud je jako zde využívaná pouze jednou jednoúčelovou metodou?
- Samotné vypisování jsem o něco málo zrychlil tím, že místo
System.out.print(symbolToPrint + " ");
používám
System.out.print(symbolToPrint);
System.out.print(" ");
Vycházím z informace, že první varianta před samotným vypsáním defacto nejdříve tyto 2 znaky sloučila do jednoho stringu a až poté vypsala což je (evidentně) o něco málo pomalejší, než vypsání stejných 2 znaků zvlášť.
- numberForCounting...... Myslím si, že šlo jen o do extrému vyhnaný
příklad
- Možná že ne, ale po převedení na boolean je ten kód čitelnější a zároveň kratší
- Vlákna momentálně neřeším příliš do hloubky, (asi bych se z toho zbláznil) ale chci aspoň rámcově vědět jak se s nimi pracuje.
Matěj Kripner:21.8.2017 18:02
Promiň, s tím konstruktorem jsem se přehlídl. Ohledně konstant to záleží. Určitě bys je měl vytvořit pokud je používáš na více místech. Jinak to můžeš udělat taky, nic tím nezkazíš a bude snazší kód pochopit. Třeba u těch 200 ms číslo asi nevadí, protože každý ví co dělá Thread.sleep a jaké přebírá argumenty. Naopak v metodě aliveNextTurn by konstanta ulehčila pochopit co ty podmínky dělají.
Neználek:22.8.2017 22:19
Také bych tipoval, že nejpomalejší bude výpis.
Mohlo by pomoci vypisovat řádek do pomocného bufferu (StringBuilder) a ten
pak vypsat najednou na standardní výstup.
Příklad:
private void drawGame(boolean[][] gameBoard) {
StringBuilder row = new StringBuilder();
for (boolean[] gameBoard1 : gameBoard) {
row.setLength(0);
for (int j = 0; j < gameBoard[0].length; j++) {
char symbolToPrint = gameBoard1[j] ? alive : dead;
row.append(symbolToPrint);
row.append(' ');
}
row.append(System.lineSeparator());
System.out.print(row.toString());
}
}
Jedná se ale o optimalizaci. Výsledný kód je hůře čitelný.
Doporučoval bych se spíše zaměřit na čitelnost programu.
Zobrazeno 11 zpráv z 11.