Kod koji obara s nogu

4 min

Pre nekog vremena sam napisao sledeće parče koda. Kako vam se čini?

byte b = 32;

// ...

b++;
if (b < 0) {
    b = 32;
}
char c = (char) b;

Mda, zaslužuje da budem javno osramoćen. Hajde da analiziramo.

Šta je namera i šta se sve ovde dešava? Kod generiše sekvencu karaktera, od ASCII vrednosti 33 do 127, jer su to kako-tako čitljivi karakteri; znak za razmak (ASCII 32) se izostavlja. Pri tome se koristi prečica. Brojač je byte da iskoristi overflow prilikom inkrementiranja: sledeća vrednost posle 127 je -128. Kao da to nije dovoljno, kod ima i grešku: ASCII vrednost 32 je preskočena samo u prvom prolazu, a dozvoljena u ostalim. Postoji i logička greška: ASCII vrednost 127 ne spada u čitljive karaktere.

Nit’ manje koda, nit’ više zbrke! Odlična pitalica za Cicin intervju.

Pošto se ložim na RED, hajde da sitnim koracima unapredimo kod. Promenimo prvo tip:

char c = ' ';
// ...
c++;
if (c == 128) {
  c = ' ';
}

Ispravimo grešku sa upotrebom praznog karaktera:

char c = ' ' + 1;
// ...
if (c == 127) {
  c = ' ';
}
c++;

Ovaj korak je naročito zanimljiv, jer nimalo ne doprinosi čitljivosti. Konačno, da isključimo korišćenje karaktera čija je ASCII vrednosti 127:

char c = 33;
// ...
if (c == 126) {
  c = 32;
}
c++;

Kod sada ispravno radi (sve vreme pišem i testove), ali je podjednako nejasan: previše je magičnih vrednosti u igri. Postoji izvestan kognitivni napor razumeti okvire skupa generisanih karaktera. Da li je 32 uključen ili ne? A 126? Ponudite ovo nekome kao pitalicu; trebaće mu par trenutaka.

Van teme: pored uobičajenih metrika koje proizilaze iz koda (kompleksnost, performanse, složenost…), vredi uključiti i metriku koja dolazi sa suprotne strane - od onoga ko čita kod. Na primer, to može biti broj WTF po desetak linija koda: mera koji je nikada nula:) U korelaciji je sa stepenom razumevanja koda.

Hajde da za trenutak ostavimo razumljivost po strani. Sledeći korak je enkapsulacija. Nema razloga da generator bude razbacan po kodu, već:

public class NextCharGenerator {
  char c = 33;
  public char get() {
    if (c == 126) {
      c = 32;
    }
    c++;
    return c;
  }
}

Iako popravljen, kod se i dalje služi još jednom prečicom. Umeš li da prepoznaš kojom? ASCII tabelom. Nerazumljivost koda upravo dolazi iz predpostavljanja ASCII rasporeda i ugrađenom konverzijom brojeva u karaktere. Drugim rečima, kod je jako uvezan sa ASCII tabelom - ali i sa redosledom karaktera u njoj.

Refaktorišemo:

public class NextCharGenerator {
  final char[] alphabet = "abc...".toCharArray();
  int currentIndex = 0;

  public char get() {
    final char c = alphabet[currentIndex];
    incrementIndex();
    return c;
  }
  private void incrementIndex() {
    currentIndex++;
    if (currentIndex == alphabet.length) {
      currentIndex = 0;
    }
  }
}

Sada je kompletan alfabet eksplicitno definisan u generatoru: više se ne generiše, čime se uklanja veza sa ASCII tabelom i rasporedom. Razumljivost koda je svakako porasla. Postoji još jedna stvar koju možemo da uradimo: da izdvojimo logiku ciklične iteracije u zaseban iterator (na pr. CycleIterator) koji bi se bavio isključivo matematikom indeksa; te kod postaje sličan ovome:

public class NextCharGenerator {
  final CycleIterator charIterator =
    CycleInterator.of("abc...".toCharArray());

  public char get() {
    return charIterator.next();
  }
}

Dometi Jave se ovde završavaju. Da bi videli kako kod može dalje da evoluira, moramo da posegnemo (bar) za Kotlinom.

Kotlin ima pregršt korisnih trikova u rukavu. Jedan je i range - definisanje skupa navođenjem samo početne i krajnje vrednosti. U Java primeru alfabet je definisan stringom koga čine svi karakteri koje želimo da koristimo: to je nekih stotinjak karaktera koji će nepotrebno stajati u fajlu, otvoreni za svaku moguću grešku. U Kotlinu se svodi na:

val alphabet = ('a'..'c') + ('A'..'C')

Dalje, nema potrebe da pravimo zaseban CycleInterator: njega ćemo zameniti generatorom sekvence, na primer:

val seq = generateSequence(
  {0},
  {
    when (it) {
      alphabet.count() - 1 -> 0
      else -> it + 1
    }
  })

Lepša sekvenca, bez when granjanja (nema smisla kada ima samo dva ishoda), može da se odmah mapira u iterator, izgleda nekako ovako:

val alphabet = ('a'..'c') + ('A'..'C')
val it = generateSequence(0) {
    it.inc().takeIf { i -> i < alphabet.count() } ?: 0
  }
  .map { alphabet.elementAt(it) }
  .iterator()

Bang! Dobili smo lep ciklični generator karaktera u formi iteratora, bez preke potrebe za zasebnim klasama.

Počeli smo sa 6 linija i završili sa isto toliko :) Nadam se da je ova kratka vožnja prijala.

🧧
Nisam definisan svojim stavovima. Stavove usvajamo, menjamo, nadograđujemo, ali oni ne čine nas same. Manje je važno da li se slažemo, koliko da se razumemo.