Utility klasa je zlo

Share

Utility klasa (iliti helper klasa), “struktura” koja ima samo statičke metode, koja je kao dizajn veoma popularna u Java svetu (i C#, Ruby, Python svetovima) jer jednostavno omogućava uobičajene funkcionalnosti koje se koriste svuda.

Ako hoćemo da pratimo DRY princip, a većina nas ga smatra jako zdravorazumskim, i ako izbegavamo duplikaciju koda, stavljamo common code blokove u utility klase i koristimo ih ponovo kada je potrebno:

// OVO JE JEZIVO REŠENJE,  don't reuse
public class NumberUtils {
  public static int max(int a, int b) {
    return a > b ? a : b;
  }
}

DRY obično stoji za “Don’t repeat yourself”, i namena principa je da smanji repeticiju informacija.

“Svaka informacija ili podatak moraju imati jednistvenu, nedvosmislenu, autoritativnu reprezentaciju unutar sistema.” Ovo zaista deluje kao veoma zgodna tehnika, zar ne!? I primenjiva na širok spektar stvari uključujući DB šeme, test planove, system build pa čak i dokumentaciju.

Ali, u objektno orijentisanom svetu, utility klase se smatraju veoma lošom (neki bi možda rekli i užasnom) praksom. Postoji dosta diskusija na ovu temu.

I pitanje se namece: ako su utility klase loše, gde da smestim generički kod?

Utility klase jesu loše. I suvoparni zaključak svih rasprava jeste da utility klase nisu pravi objekti, i kao takvi ne pripadaju objektno orijentisanom svetu. Oni su deo nasledja proceduralnog programiranja, najviše zato što smo navikli na paradigmu funkcionalne dekompozicije u nekom ranijem periodu.

Pretpostavljajući da se slažete sa argumentima i želite da prestanete da koristite utility klase, evo primera kako te kreature mogu da se zamene pravilnim objektima:

Recimo, na primer, da hocemo da pročitamo text file, podelimo ga u linije, trimujemo svaku liniju i na kraju zapamtimo rezultate u drugom file-u. Ovo može da se uradi sa FileUtils iz ApacheCommons-a:

void transform(File in, File out) {
  Collection<String> src = FileUtils.readLines(in, "UTF-8");
  Collection<String> dest = new ArrayList<>(src.size());
  for (String line : src) {
    dest.add(line.trim());
  }
  FileUtils.writeLines(out, dest, "UTF-8");
}

Ovaj kod možda izgleda kao clean code, ali ovo je proceduralno programiranje, ne objektno orijentisano. Manipulišemo podacima (bitovima i bajtovima) i eksplicitivno instruišemo kompjuter odakle da ih povuče i gde da ih smesti sa svakom linijom koda. Mi definišemo procedure izvršavanja.

U objektno orijentisanoj paradigmi, treba da instanciramo i sklopimo objekte, i dopustimo im da obradjuju podatke “na način na koji oni to žele”. Umesto da pozivamo suplementarne statičke funkcije, treba da kreiramo objekte koji su sposobni da ispoljavaju ponašanje koje mi od njih zahtevamo:

public class Max implements Number {
  private final int a;
  private final int b;
  public Max(int x, int y) {
    this.a = x;
    this.b = y;
  }
  @Override
  public int intValue() {
    return this.a > this.b ? this.a : this.b;
  }
}

Ovaj proceduralni poziv:

int max = NumberUtils.max(10, 5);

postaće objektno orijentisani poziv:

int max = new Max(10, 5).intValue();

Nije šija nego vrat? Nastavimo dalje…

Kako napraviti istu file-transforming funkcionalnost kao ranije, samo u objektno orijentisanom maniru?

void transform(File in, File out) {
  Collection<String> src = new Trimmed(
    new FileLines(new UnicodeFile(in))
  );
  Collection<String> dest = new FileLines(
    new UnicodeFile(out)
  );
  dest.addAll(src);
}

FileLines implementira Collection<String> i enkapsulira sve file read-write operacije. Instanca FileLines-a se ponaša isto kao i kolekcija stringova i skriva sve InputOutput operacije. Kada iteriramo – file se čita. Kada dodamo addAll() na to – file se upisuje.

Trimmed takodje implementira Collection<String> i enkapsulira kolekciju stringova (Decorator pattern). Svaki put kada se linija povuče, odmah je trimovana.

Sve klase koje učestvuju u ovom snipetu su jako male: Trimmed, FileLines I UnicodeFiles. Svaka od njih je odgovorna za svoj feature, tako da perfektno prate single responsibility princip.

Sa naše strane, kao korisnicima biblioteke, ovo možda nije toliko bitno, ali za njihove developere je imperative. Mnogo je lakše napraviti, održavati i pokriti Unit testovima klasu FileLines nego koristiti readLines() metodu u 80+ metoda I 3000 linija utility klase FileUtils (pogledati source code za referencu).

Objektno orijentisan pristup omogućava lazy egzekuciju. Ulazni fajl se ne čita dok njegovi podatci nisu potrebni. Ako ne uspemo da pristupimo fajlu zbog neke I/O greške, fajl nece biti ni pipnut. Ceo šou počinje tek nakon što pozovemo addAll().

Sve linije drugog snippeta, izuzev poslednje, se instanciraju i spajaju manje objekte u veće. Kompozicija objekata ne troši CPU resurce jer ne izaziva nikakvu transformaciju podataka.

Osim toga, očigledno je da se druga skripta run-uje u O(1) prostora, dok se prva egzekutuje u O(n). To je posledica proceduralnog pristupa podacima u prvoj skripti.

U objektno orijentisanom svetu ne postoje podaci. Postoje samo objekti i njihovo ponašanje!

Share

Prijavi se da prvi dobijaš nove blogove i vesti.

Ostavite odgovor

Strahinja Dević

Senior Developer in Test@Endava
mm

Diplomirao je i završio magistarske studije na Univerzitetu Grenoble u Francuskoj (Joseph Fourier, generacija 2005). Ima više od osam godina iskustva u penetration testingu, razvoju frameworka i sveobuhvatnoj praksi testiranja.

U ovom trenutku radi u Endavi kao Senior Developer in Test, ali ima i iskustva u Enterprise projektima, kao što su Kroll Ontrack, Invensys, Compucon, Midway.
U dosadašnjoj karijeri je radio sa vodećim Pentest timovima, kako bi odgovorio na pretnje protiv sistema u skladu sa PCI standardima, kao i RedTeam testiranjem, automatskim rešenjima, deploymentom na jedan klik itd.

Prijavi se da prvi dobijaš nove blogove i vesti.

Kategorije