Přejít k navigační liště

Zdroják » JavaScript » Dart – Ponorme sa hlbšie

Dart – Ponorme sa hlbšie

Články ‐ ‐ JavaScript ‐

Dnes naimplementujeme klasickú hru Pexeso. Názorne si predvedieme, ako písať aplikácie s unittestami, pracovať s knižnicami, triedami a typmi. Nemá zmysel otáľať, hor sa do roboty!

Nálepky:

Viete, že PEXESO je akronym PEKELNĚ SE SOUSTŘEĎ? To aspoň tvrdí prvé číslo 84. ročníku časopisu Naše řeč. V dnešnom dieli si jednoduchú verziu tejto hry naimplementujeme v Darte, výsledok si môžete zahrať tu a kompletné zdrojové kódy nájdete na githube.

Štruktúra

Spustíme si Dart Editor a z menu zvolíme File→New application→Web application. Začneme vytvorením súboru pubspec.yaml a adresárov lib, web a test. Otvoríme pubspec.yaml v editore a medzi dependencies pridáme browser, shuffle a unittest, posledný menovaný ako dev dependency. Podrobnejšie o adresárovej štruktúre pojednáva dokumentácia pub.

Miešanie kariet

Držiac sa vzoru MVC, oddelíme logiku aplikácie od ovládania. Vytvoríme knižnicu pexeso_model, zodpovednú za počítanie skóre hráčov, miešania kariet i kontrolu, kto je na ťahu.

Karty reprezentujeme ako List<int> čísel od 0 po počet párov - 1, každé číslo je použité práve dvakrát. V Darte List najlepšie zodpovedá Array v JavaScripte, v zátvorkách <int> môžeme (nemusíme) uviesť typ objektov, ktoré sa budú v poli nachádzať.

Na začiatku hry treba rozdať karty. Vytvorme funkciu createShuffledCards, ktorá pripraví balíček zamiešaných pexesových kariet. Obsah súboru lib/pexeso_model.dart zatiaľ vyzerá nasledovne:

/**
 * A library that provides all logic for the pexeso game.
 */
library pexeso_model;

import 'package:shuffle/shuffle.dart';

/**
 * Returns randomly shuffled [List] of numbers 0..[numOfPairs]-1 containing each
 * number exactly twice.
 */
List<int> createShuffledCards(int numOfPairs) {

}

Predtým, než sa pustíme do implementácie, napíšeme jednoduchý unittest v súbore test/pexeso_test.dart.

/**
 * Unittests for pexeso game.
 */

import 'package:unittest/unittest.dart';
import 'package:pexeso/pexeso_model.dart';

void main() {

  group("Test of model.", () {
    test("Cards provided by createShuffledCards are enumerated from 0 to"
        " numOfPairs - 1 and each card is contained exactly twice.", () {
      expect(
        createShuffledCards(8),
        unorderedEquals([0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7])
      );

    });
  });
}

Balíček unittest poskytuje sympatický spôsob písania testov. Narozdiel od bežných testovacích frameworkov nepotrebujeme vytvárať žiadne triedy. Súvisiace testy jednoducho spájame pomocou funkcie group, ktorá berie dva argumenty: názov skupiny a funkciu (my sme použili Dartovu syntax pre anonymné funkcie) vo svojom tele obsahujúcu testy. Skupiny testov sa môžu ľubovoľne hierarchicky radiť, takže vnútri group môžeme mať ďalšie groups, prípadne group nemusíme používať vôbec.

Nižšie v hierarchii sa nachádza samotný test, syntax je podobná ako pri group. Telo testovacej funkcie môže obsahovať ľubovoľný kód a okrem toho niekoľko expect direktív. Jednotlivé testy bežia nezávisle, pád jedného testu neovplyvní ostatné, no samotný test sa zastaví na prvom nenaplnenom expect.

Direktíva expect očakáva dva parametre: hodnotu a matcher. V našom príklade sme použili matcher unorderedEquals, ktorý porovnáva List na zhodu prvkov neberúc do úvahy poradie.

Keď teraz test spustíme, v konzole sa nám zjaví nasledovný výstup:

FAIL: Test of model. Cards provided by createShuffledCards are enumerated from 0 to numOfPairs - 1 and each card is contained exactly twice.
  Expected: equals [0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7] unordered
    Actual: <null>   Which: not iterable

  package:unittest/src/expect.dart 78:29  expect
  pexeso_test.dart 20:13                  main.<fn>.<fn>
  dart:async                              _createTimer.<fn>
  timer_impl.dart 96:21                   _Timer._createTimerHandler._handleTimeout
  timer_impl.dart 112:23                  _Timer._createTimerHandler.<fn>
  dart:isolate                            _ReceivePortImpl._handleMessage

0 PASSED, 1 FAILED, 0 ERRORS

Po doplnení implementácie

List<int> createShuffledCards(int numOfPairs) {
  var cards = [];
  for (var i = 0; i < numOfPairs; i++) {
    cards.add(i);
    cards.add(i);
  }
  return shuffle(cards);
}

unittesty krásne zbehnú.

PASS: Test of model. Cards provided by createShuffledCards are enumerated from 0 to numOfPairs - 1 and each card is contained exactly twice.

All 1 tests passed.
unittest-suite-success

 

Reprezentácia hry

Na reprezentáciu hry vytvoríme triedu Game, ktorá si bude držať informácie o počte hráčov, skóre jednotlivých hráčov, hráčovi na ťahu a o zozname pexesových kariet.

/**
 * Class representing one play of the pexeso game.
 */
class Game {
  final int numPlayers;
  final List<int> score;
  final List cards;

  int playerOnTurn;
}

Všimnime si, že niektoré prvky triedy, ako napríklad numPlayers, majú pri svojej deklarácií uvedený modifikátor final. Toto je v Darte spôsob, ako vytvoriť read-only premennú. Premenné označené ako final musia byť inicializované pri deklarácii, alebo v prípade tried skôr, ako sa zavolá telo konštruktora.

Všimnime si taktiež rozdiel medzi score a cards. Kým score je deklarované ako List integerov, cards môže byť List ľubovoľných (aj navzájom typovo rôznych) prvkov. Keby sme chceli, mohli by sme deklarovať nejaký prvok typu dynamic, čo je spôsob, ako v Darte explicitne povedať, že pre premennú sú povolené hodnoty ľubovoľného typu.

class Game {
  final int numPlayers;
  final List<int> score;
  final List cards;

  int playerOnTurn;

  /**
   * Creates new instance of [Game] with number [numPlayers] of players and with
   * pexeso cards [this.cards].
   */
  Game(numPlayers, playerOnTurn, cards) {
    this.numPlayers = numPlayers;
    this.score = new List.filled(numPlayers, 0);
    this.playerOnTurn = playerOnTurn;
    this.cards = cards;
  }

}

Pridali sme do triedy konštruktor. Syntax je priamočiara – konštruktor je funkcia, ktorá sa volá rovnako ako trieda. V Darte je navyše možnosť vytvoriť pre jednu triedu viacero pomenovaných konštruktorov vo forme meno_triedy.meno_konštruktora, v našom prípade napríklad Game.another(param1, param2).

Máme však problém, napísaný kód nie je korektný. V tele konštruktora priraďujeme do final fieldov triedy, no tie musia byť inicializované skôr, ako sa spúšťa telo konštruktora.

Pre tieto prípady môžeme mať konštruktor v Darte initializer list, to je vlastne sada priradení do fieldov triedy oddelená čiarkami. Priradenia v initializer liste majú ešte jedno obmedzenie, v initializer liste sa na pravej strane priradenia nemožno odkazovať na this.

Game(numPlayers, playerOnTurn, cards)
    : this.numPlayers = numPlayers,
      this.score = new List.filled(numPlayers, 0),
      this.playerOnTurn = playerOnTurn,
      this.cards = cards;

Nakoľko sme všetky priradenia vložili do initializer listu, telo konštruktora ostalo prázdne. Dart nám v takých prípadoch umožňuje miesto písania prázdnych zložených zátvoriek  jednoducho ukončiť definíciu konštruktora bodkočiarkou.

Teraz je už kód korektný, no dá sa zjednodušiť. Dart ponúka skratkovú syntax pre priradenia tvaru this.cards = cards. Ak uvedieme v argumentoch konštruktora meno fieldu ako this.field, automaticky sa hodnota argumentu priradí do príslušného fieldu.

Game(numPlayers, this.playerOnTurn, this.cards)
      : this.numPlayers = numPlayers,
        score = new List.filled(numPlayers, 0);

Je príjemné, najmä z hľadiska testovania, že náš základný konštruktor sa nesnaží si závislosti (ako napríklad balíček kariet, alebo začínajúci hráč) vyrobiť sám, ale očakáva ich ako parametre. Často by sme však chceli vytvoriť triedu Game poznajúc počet hráčov a počet párov pexesa. Pre tento účel si správime jednoduchú statickú factory metódu v Game.

/**
 * Creates an instance of [Game] for number [numPlayers] of players and with
 * randomly shuffled [numOfPairs] pairs of cards.
 */
static CreateWithCards(numPlayers, numOfPairs) {
  return new Game(numPlayers, 0, createShuffledCards(numOfPairs));
}

Teraz, keď budeme chcieť vyrobiť hru pre dvoch hráčov s 16 pármi pexesa, stačí zavolať var game = Game.CreateWithCards(2, 16);. Pre zvýšenie prehľadnosti sa vývojový tím Dartu rozhodol oddeliť bežné statické metódy od factory metód zavedením kľúčového slova factory. Jediný rozdiel spočíva v syntaxi.

factory Game.withCards(numPlayers, numOfPairs) {
  return new Game(numPlayers, 0, createShuffledCards(numOfPairs));
}

Pred chvíľou uvedený príklad vytvorenia hry sa zmení na var game = new Game.withCards(2, 16).

Herná logika

Pridajme do triedy Game metódu turnCards. Metóda dostane ako parametre pozície kariet z balíčka, ktoré hráč otočil a ak sa budú zhodovať, zvýši hráčovi na ťahu skóre. V opačnom prípade hráčov ťah končí a na rad sa dostáva ďalší v poradí.

class Game {
...
  /**
   * Checks whether the [first] and [second] cards match and if so, increases
   * the score of [playerOnTurn] by 1 and return true. Otherwise return false
   * and update [playerOnTurn].
   */
  turnCards(int first, int second) {
  }
...
}

Ako správni TDD nadšenci skôr, ako sa pustíme do implementácie, pridáme do pexeso_test.dart príslušný test.

  group("Test of model.", () {
  ...
    group("Turning cards", () {

      Game game;
      var numPlayers, playerOnTurn, nextPlayer;
      List cards;

      setUp(() {
        numPlayers = 4;
        playerOnTurn = 3;
        nextPlayer = 0;
        cards = [0, 1, 2, 0, 1, 2];
        game = new Game(numPlayers, playerOnTurn, cards);
      });

      test("that are the same increases the score by 1 and do not change the"
          " playerOnTurn. The return value is true.", () {
        expect(game.turnCards(0, 3), isTrue);
        expect(game.playerOnTurn, equals(playerOnTurn));
        expect(game.score[playerOnTurn], equals(1));
      });

      test("that do not match does not affect the score and next player gets"
          " turn. The return value is false.", () {
        expect(game.turnCards(0, 1), isFalse);
        expect(game.playerOnTurn, equals(nextPlayer));
        expect(game.score, orderedEquals(new List.filled(numPlayers, 0)));
      });
    });
  ...
  });

Vnútri každej group môžeme zaregistrovať setUp a tearDown funkcie, ktoré sa spustia pred respektíve po každom teste vnútri group. Ak máme v kóde vnorené group z ktorých každá definuje svoj setUp a tearDown, zavolajú sa najskôr vonkajšie setUp a neskôr vnútorné setUp, naopak najskôr vnútorné tearDown a potom vonkajšie tearDown. Najlepšie si to ukážeme na príklade:

group("outer", () {
  setUp(() => print("outer setUp"));
  tearDown(() => print("outer tearDown"));
  test("outer test", () => print("outer test"));
  group("inner", () {
    setUp(() => print("inner setUp"));
    tearDown(() => print("inner tearDown"));
    test("inner test 1", () => print("inner test 1"));
    test("inner test 2", () => print("inner test 2"));
  });
});

Výstup po spustení tohto kódu by bol:

outer setUp
outer test
outer tearDown
outer setUp
inner setUp
inner test 1
inner tearDown
outer tearDown
outer setUp
inner setUp
inner test 2
inner tearDown
outer tearDown

Ostáva nám doplniť implementáciu turnCards, aby testy zbehli.

/**
 * Checks whether the [first] and [second] cards match and if so, increases
 * the score of [playerOnTurn] by 1 and return true. Otherwise return false
 * and update [playerOnTurn].
 */
turnCards(int first, int second) {
  if (cards[first] == cards[second]) {
    score[playerOnTurn]++;
    return true;
  } else {
    playerOnTurn = (playerOnTurn + 1) % numPlayers;
    return false;
  }
}

Pokračovanie nabudúce…

V tejto chvíli máme naprogramovanú a otestovanú logiku hry. O tom, ako to celé rozhýbať, si povieme nabudúce.

Zahoďte knihovny: dropdowny, tooltipy a přechody stránek jen s HTML a CSS

Rozbalovací menu, tooltip, modální okno a plynulý přechod mezi stránkami. Ještě nedávno čtyři důvody, proč do projektu přidat několik knihoven a desítky kilobajtů JavaScriptu. V roce 2026 už to všechno zvládne prohlížeč sám. Ukážeme si, jak na to s Popover API, CSS anchor positioningem a View Transitions, a také kde mají své limity.

Rok po Shai-Hulud: co se změnilo na npm a co musíte udělat do ledna

Před rokem začal registrem npm procházet samošířící červ Shai-Hulud. Od té doby se útoky na dodavatelský řetězec JavaScriptu staly pravidelnou záležitostí a npm postupně přepisuje pravidla publikování. Projdeme, co se za ten rok stalo, jak dnes bezpečně publikovat balíček, co musíte stihnout do ledna 2027 a jak se bránit jako uživatel balíčků.

Vercel zrychlil směrování v CDN. Překvapivě pomohly menší bloky

Vercel zrychlil směrování v CDN tím, že začal načítat víc dat najednou. U velkých webů mu po každém nasazení scházely v cache údaje o mnoha jednotlivých cestách, takže je musel opakovaně dohledávat. U nových bloků metadat Vercel naměřil o 91 % kratší dobu tohoto hledání v 99. percentilu. Cesta k výsledku ale vedla přes nečekané zjištění: příliš velký blok se při vyřizování požadavků nevyplatil.