Dizajnové čriepky: parametre metód, továreň miesto konštruktora a System.exit(1)

I potreboval som raz istú knižnicu, prehrabal internety a našiel, ale beda: kód pochádzal z roku 2001 a bol kvality nevalnej. Nie je však cieľom búšiť do autora, ktorý už možno dávno opustil Javu a odišiel kamsi inam. Dôležité je, že i zo zlého kódu sa dá poučiti.

Schválne, čo by sa dalo vylepšiť na nasledovnom kóde?

/**
 * Use this constructor when you want all files to have the same probability.
 * @param files An arraylist of file names to be searched.
 * @param flag This is not used. This is present to distinguish between the two constructors.
 */
public FilePref(ArrayList files, boolean flag) {
    this.myPrefs = this.makeEqual(files);
}

Jemná nápoveď: pod týmto konštruktorom sa nachádza ešte jeden:

/**
 * Constructor for FilePref. Use this one when you do not want all the files to have the same probabilities.
 * @param files An arraylist of file names to be searched.
 */
public FilePref(ArrayList files) {
    try {
        this.myPrefs = this.calcPref(files);
    }catch (IOException e) {
        System.err.println("Either the given file does not exist or it does not have a .num file associated to it. If the latter is the case, please make sure you run jstrfile on the file before running jfortune on it.");
        System.exit(1);
    }
}

Trieda zoberie zoznam čohosi (súborov, v skutočnosti Stringov, hoci v tomto kóde to nie je vidieť) a funguje v dvoch režimoch podľa toho, ktorý konštruktor sa zavolá.

Už máte nápad?

V prvom konštruktore udrú do očí minimálne dve veci:

  • druhý parameter, ktorý sa tvári, že tam nie je
  • a prvý parameter, ktorý má konkrétny typ miesto interfejsu

V druhom konštruktore je, ó, hrôzo!

  • System.exit(1) (tri výkričníky)

Parameter-nevyskytujúci-sa-v-tejto-hlavičke

Keď v pythonovie Svätom grále (nie, nejde o jazyk, ale ten film) vymenovávali družiníkov kráľa Artuša, popri Bedeverovi a ďalších spomenuli Rytiera-ktorý-nevystupoval-v-onom-filme.

A presne taký parameter je flag z ukážky: je tam čisto na to, aby umožňoval volanie dvoch preťažených konštruktorov.

public FilePref(ArrayList files)
public FilePref(ArrayList files, boolean flag)

Samozrejme, dva konštruktory sa musia líšiť v dátových typoch parametrov. A ak nevieme vymyslieť žiadnu odlišnosť (“zober zoznam súborov a správaj sa tak, alebo zober zoznam súborov a správaj sa onak”), toto je rýchle-a-špinavé riešenie.

Ale dá sa to určite krajšie. Ale na to treba príbehovú odbočku…

…existuje skvelá kniha. Čo kniha, biblia Javáka. Effective Java alias Java efektivně, od samotného Blocha, jedného z autorov Javy. Buď nájdite v antikvariáte prvé vydanie v češtine, alebo investujte 23 eúr do druhej edície pre Kindle. Sedemdesiatosem tipov: a čo kúsok, to užitočná vec.

Hneď prvý item sa volá:

Consider static factory methods instead of constructors

Namiesto konštruktorov uprednostňujte statické továrenské metódy

Nebudem prepisovať päť strán Blochovho evanjelia, ktoré vysvetľuje okolnosti. Tuto stačí vedieť, že: továrenských metód môže byť koľko len chcete a môžu sa líšiť menom.

public class FilePref {
    private FilePref(ArrayList files) {
        // nechceme vytvárať inštancie inak než továrenskými metódami
    }

    public static FilePref newWithEqualProbabilities(ArrayList files) {
        FilePref filePref = new FilePref();
        ...
        return filePref;
    }

    public static FilePref newWithCalculatedProbabilities(ArrayList files) {
        FilePref filePref = new FilePref();
        ...
        return filePref;
    }
}

Vytváranie inštancie je potom malina:

FilePref filePref = FilePref.newWithCalculatedProbabilities(...)

Omnoho čitateľnejšie a bez obskúrnych parametrov. Dokonca samodokumentujúce sa! Názov metódy je totiž dostatočne popisný i bez JavaDocu.

Prvý parameter, ktorý má konkrétny typ miesto interfejsu

I mal som zoznam súborov:

List files = Arrays.asList(
                new File("en.dat"), 
                new File("es.dat"), 
                new File("sk.dat"));

Lenže beda:

 FilePref filePref = FilePref.newWithCalculatedProbabilities(files);

Toto je kompilačná chyba: files je typu java.util.List, ale metóda čaká konkrétnu implementáciu ArrayList. A či ja viem, čo je files za typ? Možno poľový ArrayList, možno LinkedList, ktovie. Možno dokonca Vector, jeden nikdy nevie. (Ve zzkutečnosti ArrayList, teda aspoň v Oracle Jave, ale to nás nemá čo zaujímať.)

Neostáva mi nič iné, než vytvoriť nový ArrayList a strčiť doňho prvky môjho zoznamu. Ešteže to ide ľahko:

FilePref filePref = FilePref.newWithCalculatedProbabilities(new ArrayList(files));

Dizajnér API spravil chybu:

Programujte voči interfejsom, nie voči implementáciám.

Načo je potrebné vedieť, že metóda berie ArrayList, keď jej stačí všeobecný zoznam súborov List?

Plus generiká

Áno, už osem rokov máme Javu 5 s generikami. Nie je absolútny dôvod nepoužívať ich.

Opravený kód

Opravený kód je zároveň prehľadnejší: pozriem na List<File> a vidím, že potrebujem zoznam súborov; môžem do toho napchať spoják, poľák (poliak?), vektor… všetko sú to triedy implementujúce List. Navyše hneď vidím dátový typ.

Výsledok:

public static FilePref newWithEqualProbabilities(List<File> files) {
    FilePref filePref = new FilePref();
    ...
    return filePref;
}

public static FilePref newWithCalculatedProbabilities(List<File> files) {
    FilePref filePref = new FilePref();
    ...
    return filePref;
}

Strieľať za System.exit(1)

Elton John sa v roku 2004 preslávil:

Všetci, čo na koncertoch so 75 librovým vstupným spievajú na plejbek, by mali byť zastrelení.

Madona (lebo to bolo na ňu) sa nepotešila.

To isté platí pre System.exit(1):

public FilePref(ArrayList files) {
    try {
        this.myPrefs = this.calcPref(files);
    }catch (IOException e) {
        System.err.println("Either the given file does not exist or it does not have a .num file associated to it. If the latter is the case, please make sure you run jstrfile on the file before running jfortune on it.");
        System.exit(1);
    }
}

Ste používateľka megasoftvéru vo verzii 2013, ktorý stavia na tejto triede. Naklikáte zoznam súborov, dáte OK, a vaša aplikácia zhučí. Spolu s rozrobenou prácou. Lebo možno jeden súbor sa nedal prečítať. Alebo mal zlú štruktúru. Ktovie.

Zhučí. S chybovým kódom 1, ktorý sa nedozviete, lebo konzolu nevidíte, a ani sa ho nechcete dozvedieť, lebo vás nemá čo zaujímať. (Samozrejme, tam niekde vonku v šírej záhrade softvéru existuje bashový skript, ktorý volá Java program a po jeho dobehnutí overuje exit code, ale.)

System.exit(1) sa môže vyskytnúť leda tak v main()e, aj to s výhradami. Výnimky neboli vymyslené len tak (a Bloch im nevenoval len tak 10 položiek vo svojej knihe).

Nech metóda vyhodí IOException. Alebo inú výnimku. Alebo nech ju aspoň zaloguje a zhltne. Ale nie System.exit(1).

Nie. Fontom veľkosti 144.

2 thoughts on “Dizajnové čriepky: parametre metód, továreň miesto konštruktora a System.exit(1)

  1. Nad tímhle kouskem kódu jsem se pozastavoval už před pár dny na hovnokod.cz, ale jak vidím, tak ta nejlepší část (System.exit) v onom příspěvku vlastně ještě chyběla. :)

    Ještě mě tak napadá, že kdyby člověk byl úplný pedant, tak by ony factory metody šly ještě trochu vylepšit pomocí wildcards: public static FilePref newWithEqualProbabilities(List<? extends File> files)

    Díky tomuhle by klient mohl nejenom předat jakoukoliv implementaci Listu, ale zároveň i list jakékoliv subclassy File (pokud by teoreticky s nějakou takovou pracoval). Ale to už je trochu vyšší dívčí a je otázka, jestli by to bylo někdy vůbec potřeba.

  2. Línia úvahy išla: hľadal som parser pre unixovské fortune cookies, našiel onú knižnicu, vypadli mi oči, napísal som na hovnokód a potom tento článok.

    Fortune cookies som nakoniec vyriešil inak, bez knižnice.

    Inak rozmýšľam, či som v nejakom projekte videl subclassnutý java.io.File, resp. čo by taká trieda robila (nejaké dodatočné helper metódy?): Používali ste to niekde?

Pridaj komentár

Vaša e-mailová adresa nebude zverejnená. Vyžadované polia sú označené *