Právě jsem dokončil implementaci verze hry Conway Život v Javě.
Jelikož jsem jen studentem vysoké školy, jsem si jist, že můj kód není téměř dokonalý, a přemýšlel jsem, jestli byste se na můj kód mohli podívat. V čem se mohu zlepšit? Existují rychlejší způsoby implementovat určité oblasti mého kódu? Existuje přebytečný kód, který mohu oříznout? Existuje chytřejší způsob implementace Conwayovy Hry o život?
EDIT:
V naději na získání další zpětné vazby je zde teorie, která stojí za mou implementací:
Pro informaci, zde jsou pravidla pro hru Conwayovy hry o život (převzato z wikipedie):
- Jakákoli živá buňka s méně než dvěma živými sousedy zemře, jako by byla nedostatečně obydlena.
- Jakákoli živá buňka s koudelem nebo třemi živými sousedy žijí z další generace.
- Jakákoli živá buňka s více než třemi živými sousedy zemře rpopulation
- Jakákoli mrtvá buňka s přesně třemi živými sousedy se stane živou buňkou, jako by to byla reprodukce.
Přehled:
- A odlišný pohled na hru Conwayovy hry o život
- nevyslovená pravidla
- vysvětlení důležitých metod (a použitých datových struktur)
Jiný pohled na hru Conwayovy hry o život
Představme si nejprve hru života jako úzkostnou mřížku (my bude také předpokládat, že tato mřížka má souřadnice takové, že levý dolní roh je označen jako (0,0) a pravý horní roh je označen jako (n, n), kde n je kladné celé číslo). Tato 2-dimenzionální mřížka představuje skupinu n * n počtu buněk. Každý blok mřížky lze považovat za buňku, která nejen ukládá booleovskou hodnotu (mrtvou nebo živou) k popisu stavu buňky, ale také podrobně popisuje její polohu pomocí souřadnic. Současný stav všech buněk navíc určuje, které buňky zemřou, budou dál žít nebo se narodí v příští generaci v souladu s pravidly, která jsou uvedena výše.
Z jiné perspektivy však hra Conwaye of life is very similar to the game minesweeper. Můžeme si představit živou buňku jako minu a její sousedy, kteří ukládají počet dolů, které jsou nejblíže k ní. Tímto způsobem jsme schopni snadno použít výše uvedená pravidla k určení budoucí generace (zejména které buňky zemřou a které buňky se zrodí).
A co buňky, které jsou aktuálně naživu, můžete mít zeptat se? Můžeme je snadno představovat jako celé číslo větší než 10, kde místo jednoho označuje, kolik živých sousedů má aktuálně živá buňka, a místo deseti označuje, že buňka je naživu.
Nevyřčená pravidla
Jeden postřeh, který mě napadl, je, že hra života se týká pouze živých buněk. Pouze buňky, které jsou naživu, mohou zemřít, buňky, které nadále žijí, již musí žít a buňky se mohou narodit, pouze pokud mají živé sousedy. Výsledkem je, že kontrola celé mřížky (časová složitost: O (n ^ 2)), aby se určila budoucí generace buněk, by byla úplným odpadem. Bylo by mnohem rychlejší, kdybych uložil všechny aktuálně živé buňky a zkontroloval každou živou buňku spolu s jejich sousedy, abych určil další generaci (což je přesně to, co jsem udělal).
Vysvětlení důležitých metod (a použitých datových struktur)
birth (): iteruje přes HashMap obsahující pár klíč – hodnota všechny živé buňky spolu se svými sousedy. Pokud se pár klíč – hodnota řídí výše uvedenými pravidly života, klíč (celočíselná hodnota, která představuje umístění buňky) se poté vloží do zásobníku, který obsahuje další generaci živých buněk. Po každé iteraci se hodnota mřížky resetuje na 0 a pár klíč – hodnota se odebere z HashMap.
insertAlive (): zobrazí zásobník a vloží živou buňku do mřížky. Vložení živé buňky sleduje strukturu hledání min (sousedé živé buňky se zvýší o 1 a živá buňka se zvýší o 10, což znamená, že je živá). Všichni sousedé a živé buňky se poté vloží do HashMap, aby se funkce birth () mohla správně spustit.
printBoard () (měl by být pojmenován boardToString): používá formátor řetězce k formátování mřížky na řetězec.
Poznámka : většina komentářů byla odstraněna, protože příliš nepřispívají k čitelnosti kód
CellularAutomaton.java
package first; public abstract class CellularAutomaton{ public abstract String lifeCycle(); public abstract boolean rules(int num); }
GameOfLife.java
package first; import java.util.Stack; import java.util.HashMap; import java.util.Iterator; import java.util.Map; public class GameOfLife extends CellularAutomaton { int board[][]; int dim; Stack<Integer> stackCells; HashMap<Integer, Integer> hmapCells; public gameOfLife(int d, Stack<Integer> s){ board = new int[d][d]; dim = d; stackCells = s; hmapCells = new HashMap<>(); } public boolean rules(int num){ return num == 3 || num == 12 || num == 13; } private void birth() { Iterator<Map.Entry<Integer,Integer>> it=hmapCells.entrySet().iterator(); while(it.hasNext()) { Map.Entry<Integer,Integer> pair = it.next(); int key = pair.getKey(); if(rules(pair.getValue())){ stackCells.add(key); } board[key/dim][key%dim] = 0; it.remove(); } } private void insertAlive() { while(!stackCells.isEmpty()) { int cell = stackCells.pop(); int x = cell / dim; int y = cell % dim; int startX = (x <= 0) ? 0 : x - 1; int startY = (y <= 0) ? 0 : y - 1; int endX = (x >= dim - 1) ? x + 1 : x + 2; int endY = (y >= dim - 1) ? y + 1 : y + 2; for(int i = startX; i < endX; ++i) { for(int j = startY; j < endY; ++j) { hmapCells.put(i * dim + j, ++board[i][j]); } } hmapCells.put(cell, board[x][y] += 9); } } private String printBoard() { StringBuilder s = new StringBuilder(); for(int elements[] : board) { for(int element : elements) { if(element >= 10){ s.append("* "); } else { s.append(" "); } } s.append("\n"); } return s.toString(); } public String lifeCycle() { birth(); insertAlive(); return printBoard(); } }
Simulation.java
package first; import java.util.Stack; public class Simulation { public static void main(String args[]) throws InterruptedException{ int dim = 70; Stack<Integer> init = new Stack<>(); //all vals pushed to init is of the form: xPos * dim + yPos init.push(351); init.push(352); init.push(421); init.push(422); init.push(245); init.push(246); init.push(315); init.push(316); init.push(361); init.push(431); init.push(501); init.push(292); init.push(572); init.push(223); init.push(643); init.push(224); init.push(644); init.push(435); init.push(296); init.push(576); init.push(367); init.push(437); init.push(507); init.push(438); init.push(231); init.push(301); init.push(371); init.push(232); init.push(302); init.push(372); init.push(163); init.push(443); init.push(165); init.push(445); init.push(95); init.push(515); GameOfLife gOL = new GameOfLife(dim, init); while(true) { System.out.print(gOL.lifeCycle()); Thread.sleep(100); System.out.print("\033[H\033[2J"); } } }
Komentáře
- Vytvořili jste ‚ bod, který říká, že se jedná o jinou implementaci, ale neexistuje nic, co by vysvětlovalo teorii vaší implementace nebo relativně nejasné algoritmy a vzorce, které ‚ znovu používáte.
- Toto je populární cvičení, proto bych vám také doporučil podívat se na implementace, které najdete online: je to ‚ velmi poučné. Obzvláště se mi líbí tento přehled , který ukazuje reaktivní implementaci v prostředí Java 8 (pomocí RxJava) – ne ale byl by to dobrý produkční kód.
Odpověď
První z všichni si myslím, že algoritmus je docela chytrý, což pro mé skromné zkušenosti není pro studenta tak běžné. Gratulujeme, pokud jste na to přišli sami! Pokud hledáte inteligentní implementace, doporučil bych funkční, např. v Haskellu ; viz také nejkratší hra života .
Pozor na chytrost. Dobrý kód by měl být snadno čitelný , snadno pochopitelné . To samozřejmě není vždy možné při řešení složitého algoritmu, ale domnívám se, že by to mělo být cíl.
jjjjjjjjjjjjj řekl:
Poznámka: většina komentářů byla odstraněna, protože nepřidávají hodně k čitelnosti kódu
Smyslem komentářů je pomoci lidem porozumět vašemu kódu (obecně řečeno, zaměřte se na “ proč “ spíše než “ co „). Tady, aby lidé pochopili, že jste do svého příspěvku museli přidat hodně textu. V ideálním případě to není nutné, protože kód je:
- self-documented,
- komentován, aby se vyčistil složitý / implicitní obsah.
Například zde je rychlé přepsání vašeho kódu, aby byl kód expresivnější:
GameOfLife.java
/** * Computes the next state of the automaton by using Conway"s original rules. */ public class GameOfLife extends CellularAutomaton { /** * Stores all cells in a two-dimensional matrix. The value stored is * the number of live neighbors of the cell, +10 if the cell is alive. */ private int board[][]; private int dim; /* * index(cell) = cellX * dim + cellY */ private Stack<Integer> indexesOfCellsAliveAtNextGeneration; private HashMap<Integer, Integer> cellsMaybeAliveAtNextGeneration; public GameOfLife(int d, Stack<Integer> s){ board = new int[d][d]; dim = d; indexesOfCellsAliveAtNextGeneration = s; cellsMaybeAliveAtNextGeneration = new HashMap<>(); } public String newGeneration() { populateWorldWithAliveCellsFromPreviousGeneration(); computeCellsMaybeAliveAtNextGeneration(); return boardAsString(); } private void populateWorldWithAliveCellsFromPreviousGeneration() { for (Map.Entry<Integer, Integer> cell : cellsMaybeAliveAtNextGeneration.entrySet()) { int cellIndex = cell.getKey(); int cellValue = cell.getValue(); if(willBeAlive(cellValue)){ indexesOfCellsAliveAtNextGeneration.add(cellIndex); } board[cellIndex/dim][cellIndex%dim] = 0; } } private static boolean willBeAlive(int cell){ return (!isAlive(cell) && nbOfNeighbors(cell) == 3) || (isAlive(cell) && (nbOfNeighbors(cell) == 2 || nbOfNeighbors(cell) == 3)); } private static boolean isAlive(int cell) { return cell >= 10; } private static int nbOfNeighbors(int cell) { return cell % 10; } private void computeCellsMaybeAliveAtNextGeneration() { cellsMaybeAliveAtNextGeneration.clear(); while(!indexesOfCellsAliveAtNextGeneration.isEmpty()) { int cellIndex = indexesOfCellsAliveAtNextGeneration.pop(); int cellX = cellIndex / dim; int cellY = cellIndex % dim; int topLeftNeighbourX = (cellX <= 0) ? 0 : cellX - 1; int topLeftNeighbourY = (cellY <= 0) ? 0 : cellY - 1; int bottomRightNeighbourX = (cellX >= dim - 1) ? cellX + 1 : cellX + 2; int bottomRightNeighbourY = (cellY >= dim - 1) ? cellY + 1 : cellY + 2; // Iterate through every cell"s neighbor to increate their neighbor number for(int i = topLeftNeighbourX; i < bottomRightNeighbourX; ++i) { for(int j = topLeftNeighbourY; j < bottomRightNeighbourY; ++j) { boolean isNeighbor = i != cellX || j != cellY; if (isNeighbor) { int neighborIndex = i * dim + j; cellsMaybeAliveAtNextGeneration.put(neighborIndex, incrementedNumberOfNeighbors(i, j)); } } } cellsMaybeAliveAtNextGeneration.put(cellIndex, makeAlive(cellX, cellY)); } } private int incrementedNumberOfNeighbors(int x, int y) { return ++board[x][y]; } private int makeAlive(int x, int y) { return board[x][y] += 10; } private String boardAsString() { StringBuilder s = new StringBuilder(); for(int[] cells : board) { for(int cell : cells) { if(isAlive(cell)){ s.append("* "); } else { s.append(" "); } } s.append("\n"); } return s.toString().trim(); } }
Většinou jsem některé proměnné / metody přejmenoval a představil některé metody obsluhy. Kód je o něco delší a cítí se více upřímný, ale je IMHO také srozumitelnější. Je to stále velmi procedurální (což samo o sobě není špatné, zvláště pro takový jednoduchý program), ale možná budete chtít zkusit přidat více expresivity zavedením nových tříd, jako je Board
nebo Cell
. Takovéto OO implementace najdete na GitHubu .
Váš kód může také narazit na problémy s pamětí u velkých desek. Vaše proměnná board[][]
skutečně ukládá všechny buňky, dokonce i mrtvé. S deskou 10 000 x 10 000, která obsahuje pouze ~ 5/6 buněk, ztratíte spoustu paměti. Řešením je použít řídké pole (v zásadě sada obsahující pouze živé buňky).
Jako vedlejší poznámku jsem se před několika lety také pokusil modelovat vysoce konfigurovatelný GoL v “ čistém “ OO way; můj kód je na GitHubu , pokud si ho chcete prohlédnout. Metoda počítající další generaci svět je ImmutableGeneration :: nextGeneration ; vzhledem k sadě živých buněk v zásadě: 1) spočítá všechny sousední buňky a 2) ponechá pouze ty, které budou živé. Pravidla označující, zda bude buňka živá nebo mrtvá, jsou implementována v Rule.java .
EDIT : osobní názor na stručnost versus výřečnost, pokud jde o n chtěl odpovědět na komentář
Nejprve se domnívám, že neexistují správné odpovědi: vše je o kompromisech a osobních preferencích. Pojmenování je těžké a v této oblasti najdete spoustu článků.
V informatice existují pouze dvě tvrdé věci: zneplatnění mezipaměti a pojmenování věcí
– Phil Karlton
Můj názor je, že stručnost je příjemná, ale může vést k dvojznačnostem. A dvojznačnost, zejména skrytá, je hrozbou.První příklad, který mě napadne, je omylem míchání jednotek:
// Everything looks good... double pathLength = distanceFromGoal + distanceToTarget; // ... but adding units makes easy to spot bugs double pathLengthInKilometers = distanceFromGoalInMeters + distanceToTargetInMillimeters;
To jak bylo řečeno, dlouhé názvy znesnadňují čtení kódu. Lze je omezit zohledněním dvou věcí:
- kontext (např. Název obklopující metody / třídy / balíčku),
- rozsah (místní proměnná v 3řádková metoda může být v pořádku s krátkým názvem, zatímco funkce použitá vícekrát v celé kódové základně může potřebovat delší).
To je také to, co doporučuje Konvence pojmenování Google .
Jako poslední poznámku, jak jste navrhli, lze velmi dlouhé názvy považovat za vůně kódu. Většinou se jedná o nedostatek soudržnosti (třída / metoda dělá příliš mnoho různých věcí – opět zde nejsou jasné metriky, je to až pocit vývojáře). Například v kódu, který jsem navrhl, můžeme uvažovat o populateWorldWithAliveCellsFromPreviousGeneration
jako metodě nesoucí odpovědnost: 1) výpočet buněk, které budou naživu v příští generaci a 2) osídlení světa. Mohli bychom ji tedy rozdělit na dvě části: populateWorldWith(aliveCellsFromPreviousGeneration())
.
Stejným způsobem jsme mohli shromáždit atributy, jejichž název končí na “ atNextGeneration “ v nové Generation
třídě:
public class GameOfLife extends CellularAutomaton { private Generation lastGeneration; public String newGeneration() { this.lastGeneration = lastGeneration.nextGeneration(); return this.lastGeneration.toString(); } } public class Generation { public Generation nextGeneration() { return new Generation(aliveAtNextGeneration(this.aliveCells)); } ... }
Ale rozdělení logiky na příliš mnoho tříd také zvýší složitost architektury a znesnadní pochopení toku.
Jako Závěr Doporučuji vám mít na paměti, že jakýkoli kus kódu může být upraven vývojáři, kteří nemají žádné předchozí znalosti o projektu a kteří musí rozumět tomu, co kód dělá, a proč dělá to tak, aby to mohli udržovat nebo opakovaně používat součásti, aniž by zavedli regrese. Neexistuje žádná stříbrná kulička: pouze kompromisy, a na čem záleží, když se rozhodnete, je:
- můžete identifikovat kompromis,
- rozumíte kladům i záporům každou alternativu a vyberte si jednu z nich vědomě.
(ale netlačte na ně příliš velkým tlakem: KISS a nezapomeňte, že kód lze poté znovu upravit)
Komentáře
- Děkuji vám za váš komentář. Jsem tak rád, že jste to komentovali, protože bych nikdy nepřemýšlel pouze o uložení živých buněk (to hodně mění můj kód a podle mého názoru je mnohem lepší). Chtěl jsem se trochu zeptat na váš názor na rovnováhu mezi jasností názvů proměnných a stručností. Jinými slovy, jak můžete zjistit, kdy je program příliš podrobný? Znamená to, že musíte strávit mimořádně dlouhou dobu vytvářením správných názvů proměnných nebo že je něco vadného s vaší logikou a designem kódu? Ještě jednou děkuji.
- Upravil jsem svou odpověď, abych se podělil o svůj názor na ni. ‚ Je spousta textu, který v zásadě říká, že “ neexistují správné odpovědi, ‚ Vše o kompromisech, takže při výběru “ promyslete klady a zápory.
Odpovědět
Mám jen jedno malé doporučení ohledně čitelnosti. Pokud máte metodu nazvanou printBoard
, normálně byste očekávali, že vytiskne tabuli . Lepší název této metody by byl boardToString
.