## PTS Projekt - Terra Futura Projekt bol hodnotený podľa nasledujúcej šablóny. V prípade, že si myslíte, že som niekde pri hodnotení vašeho projektu urobil chybu, napíšte a skúste ma nasmerovať k vašemu riešeniu. V prípade, že si chcete vylepšiť hodnotenie, môžete projekt opraviť. Na opravené riešenia však už budem mať vyššie nároky ako teraz. * Tímová časť projektu a formálne záležitosti (16 bodov) - 4b Pripravené PRs - 4b Reviewované PRs - 4b Riešenie je v GITe a má zmysluplnu históriu - 4b Typy a typove anotácie * Trieda Pile (10 bodov) - 8b Poriešenie náhodnosti - 2b Implementácia * Trieda SelectReward (12 bodov) - 4b Vhodné injectnute zavislostí, aby sa dali urobiť solitary testy - 4b Testy sú solitary - 4b Mockovaná trieda Card, overenie, že sa na kartu dostane odmena * Trieda Grid (14 bodov) - 4b Implementácia putCard + canPutCard - 4b Implementácia canBeActivated + setActivated - 3b Testy putCard testujú cca to, čo treba - 3b Testy setActivated testujú cca to, čo treba * Integračný test (10 bodov) - 4b Je integračný test - 6b Kvalita integračného testu (koľko scenárov tam je / ako ťažké je pridať ďalšie scenáre) ### Tímová Čast projektu a formálne záležitosti Túto časť som nehodnotil do hĺbky. Pozrel som sa, či ste sa aktívne zúčastňovali tímovej časti projektu. Pri ľuďoch, korí mali nefunkčný tím som sa pokúsil priradiť body za vykonanú prácu. Body za typy boli v Jave získané automaticky, v Pythone tu bolo možné prísť o niekoľko bodov za nedodržanie obmedzení. ### Trieda Pile Trieda je primárne zaujímavá tým, že sa na ňu v pravidlách viaže náhodné správanie. Aj mimo náhodného aspektu má trieda zaujímavú funkcionalitu, ktorú chceme vedieť otestovať v unit teste aj v integračnom teste. Samozrejme, ak trieda mieša karty bez kontroly, testy nebudeme schopný dobre napísať. Zďaleka najjednoduchším riešením je predpokladať, že karty do konštruktora prídu pomiešané. Problém tohoto riešenia je, že neumožňuje pomiešať vrátené karty keď sa kôpka minie. Nakoľko však tento aspekt pravidiel nebol zohľadnený v dizajne, považoval som toto riešenie za správne. V prípade, že chceme dodržať pravidlá, okrem visibleCards a hiddenCards potrebujeme aj discardedCards. Funkcionalitu je možné implementovať bez toho, aby sa to dotklo ostatných tried. Keďže náhodnosť potrebujeme počas celej hry, nestačí nám dostať zamiešané karty na začiatku, ale musíme miešať aj počas hry. Najsprávnejšie riešenie spočíva vo vyňatí náhodnosti z triedy Pile do nejakej maličkej netestovateľnej triedy. Táto náhodná trieda bude schovaná za interfacom, aby sme ju pri testoch mohli nahradiť vhodným testovacím dvojníkom. Pri návrhu tohoto interfacu treba myslieť na potreby nielen implementácie, ale najmä testovania. Pri čítaní časti o integračnom teste sa môžete zamyslieť, aký interface potrebujete na to, aby sa takýto test dal urobiť. Neintuitívne, ale prekvapivo dobré riešenie je napríklad toto. ``` Interface CardDraw getCard(List[String]) -> Int ``` Alternatívne riešenie, ktoré sa v projektoch vyskytovalo často, spočíva v injectnutí triedy Random, prípadne v injectnutí seedu. Injectnutím triedy Random sa dá za určitých podmienok získať podobné správanie ako extrakciou triedy. Môžeme urobiť podtriedu triedy Random a namockovať metódu shuffle. Táto redukcia interfacu však nie je nikde explicitne spomenutá a v prípadných budúcich zmenách môže niekto ľahko použiť iné metódy, čím postupne strácame kontrolu nad testami. Zároveň, testy sa takto budú písať o Čosi horšie. Dedenie je celkovo nie veľmi vhodný spôsob na výrobu testovacích dvojníkov, keďže nemožno zaručiť, že sa nepoužije produkčný kód. Ak použijeme mockovací framework, tento problém síce odpadá, avšak stále Random možno použiť veľa spôsobmi a bez limitovaného interfacu to nie je príliš stabilné riešenie. Injectovanie seedu je naopak riešenie čisté, naráža však na rôzne problémy. Ak chceme vyskúšať scenár závislý od konkrétneho zamiešania, musíme hľadať správny seed. Preto namiesto toho, aby sa v testoch dialo presne to, čo chceme otestovať, Často skončíme s kompromisom, že takýto seed sa mi podarilo nájsť, tak sa uspokojím s takýmto testom, čo je situácia, ktorej sa chceme vyhnúť. Ďalšou hrozbou je seed-stabilita. Nie vždy je garantované, že rovnaký seed vedie k rovnakému náhodnému správaniu naprieč kompilátormi/implementáciami/verziami. ### Trieda SelectReward Trieda je zaujímavá svojím špecifickým vzťahom ku zvyšku projektu. Interaguje s triedou Card, avšak veľmi špecifickým spôsobom. Jediné, čo trieda potrebuje, je mať možnosť zavolať canPutResources a putResources. Preto chceme triedy Card a SelectReward oddeliť presne takýmto interfacom, čim dosiahneme najväčšie skrývanie informácií a najväčšiu nezávislosť medzi triedami. Taktiež nám to uľahčí testovanie, keďže mockovaná trieda potrebuje mať iba dve metódy. Mať jeden veľký spoločný interface pre triedu Card, je horšie, viď solId. Podobne je to, ak triedu Card do SelectReward neinjectneme, ale vytvoríme testovacieho dvojníka dedením/pomocou mockovacieho frameworku z triedy Card. Toto riešenie je ešte o čosi horšie. Ako som spomínal pri Pile, dedenie má svoje problémy. Použitie mockovacieho frameworku je ok, stále však máme problém s nedostatočným skrývaním informácií. Okrem toho, možno v teste budeme niekedy chcieť iný druh testovacieho dvojníka. To sa dá (mockovacie frameworky sú robustný nástroj, ktorý vie celkom dobre zakryť, že nerobíte, čo treba), ale nemusí to byť práve najpeknejšie. ### Trieda Grid V tejto triede je niekoľko zaujímavých vecí. Prvou je celkovo práca so súradnicami. Mnohí mali v riešení dvojrozmerné polia a bolo potrebné konvertovať súradnicový systém. Ak to už robíme takto, bolo by pekné používať rôzne typy pre rôzne druhy súradníc. Často sa celým kódom tiahlo +-2, čo je nesmierne náchylné na chyby, pričom tieto typové chyby sa môžu ľahko prešmyknúť cez testy. Preferujem použitie mapy z Coordinate na InterfaceCard (mimochodom, tento interface musí myslieť na konverziu na slabší interface pre SelectReward). Toto sa týkalo aj dvoch špecifických problémov, ktoré som si vybral na hodnotenie. Podľa pravidiel sa majú karty vmestiť do 3x3 sieťe, pričom však prvá karta nemusí byť v strede, a pri položení prvej karty ešte nie je jasné kde je stred siete. Značná časť riešení tento zaujímavý aspekt ignorovala. Riešenia by som rozdelil do dvoch kategórií. Jeden typ riešenia po putCard prepočítal nové hranice, kam je karty možné položiť. Toto riešenie síce funguje, ale pridáva invarianty pre triedu, ktoré nie ú potrebné. Takéto invarianty treba striktne vynucovať a je to potenciálny zdroj chýb. Preferované riešenie je priamo v canPutCard počítať či je to ok. Aj toto sa dá urobiť lepšie, či horšie. Samozrejme, ideálne je skryť čo najviac detailov výpočtu. Vhodné riešenie je niečo ako ``` diff: Callable[List[int], int] = lambda x: max(x)-min(x) x_values: List[int] = [c.x for c in cards.keys] + [coordinate.x] y_values: List[int] = [c.y for c in cards.keys] + [coordinate.y] if diff(x_values) > 2 or diff(y_values) > 2: return false ``` Samozrejme, sú aj iné dobré riešenia. V Jave to zasa bude vyzerať úplne inak, ale ak ste vyprodukovali niečo dlhé plné cyklov, tak to nie je optimálne. Btw., veľmi zlé je opakovať kód medzi canPutCard a putCard. Druhým zaujímavým aspektom bola kontrola aktivácií. Tu pravidlá hovoria, že aktivovať sa v danom kole môžu karty, ktoré sú v rovnakom riadku a stĺpci ako novopoložená karta. Plus, treba si dať pozor aby sa novopoložená karta mohla aktivovať iba raz. Tu, opäť, mnohé riešenia nič takéto neobsahovali. ### Integračný test Bolo treba vyrobiť integračné testy, ktorý testuje integráciu celého systému. Podľa zadania, nebolo potrebné tieto testy urobiť poriadne, stačilo urobiť krátky scenár, avšak urobiť ho tak, aby zvolený systém umožňoval napísanie dobrých integračných testov, t.j. písanie ďalších testov nemalo byť príliš pracné a nemalo obsahovať veľa opakujúceho sa kódu. Ideálne je aby sa testy pekne písali a čítali. Testovať treba použitím definovaných interfacov, všetko ostatné sú implementačné detaily, ktoré sa môžu meniť Samotné jadro testu by chcelo vyzerať napríklad takto. ``` deck1_mock.set_refilled_card("City") self.assertTrue(game.takeCard(1, CardSource(Deck.I, 3)), GridPosition(-2, 1)) fake_observer.validate_grid_card(GridPosition(-2, 1), "Factory: rr->Rp") fake_observer.validate_deck(Deck.I, 1), "City", "Factory: rr->Rp", "Factory: gg->Gp", "Green") ``` Na orientáciu v dlhšom teste je nevyhnutné na vhodných miestach špecifikovať medzistavy. Dá sa to robiť komentárom. Všimnite si však ako validácia na štvrtom riadku plní túto úlohu. Takáto validácia nie je potrebná z hľadiska testu, avšak nahrádza potrebný komentár (v dlhom texte by bolo ťažké odsledovať aktuálny stav hry).