tifyty

pure Java, what else ?

Havi archívumok: december 2012

Miért ne legyen több, mint 9 thread lokál változónk?

Egy korábbi cikkben azt fejtegettem, hogy lassú-e a thread local, és mint vátesz, kijelentettem, hogy egy programban (egy classloader alatt) ne használjunk több, mint 9 ThreadLocal változót ha zavar minket, hogy a Thread objektum threadLocals map-jában egybeesés van, vagy, hogy a JDK átszervezi a map-et (ami relatíve lassú folyamat). Honnan jött ez a 9? Miért 9, miért nem 10, vagy 16?

Na, akkor vágjunk bele.

Először is a 9 egy olyan szám, amelyikre abban a JDK-ban, amelyiket

java -version
java version "1.7.0_07"
Java(TM) SE Runtime Environment (build 1.7.0_07-b10)
Java HotSpot(TM) 64-Bit Server VM (build 23.3-b01, mixed mode)

megnéztem garantált, hogy nem lesz ütközés (egybeesés), sem map átstrukturálás. Ezt azonban nem a szabvány garantálja, és nem a JDK API. Egyszerűen csak látszik a kódból, így előfordulhat, hogy régebbi, újabb, vagy éppen más architektúrára készített JDK-ban ez nem így van.

Azt sem lehet állítani, hogy ha egy programban több, mint kilenc ThreadLocal változópéldány van, akkor lesz ütközés, vagy map átstrukturálás. Előfordulhat, hogy nem lesz. Mint a kertmozi előadás. Ha eső lesz nem lesz. Ha nem lesz, akkor lesz.

A ThreadLocalMap a ThreadLocal osztály egy statikus belső osztálya. Mint minden rendes map, ez is tartalmaz entry-ket. Ez az entry azonban a HashMap entry-jével ellentétben nem tartalmaz next mezőt ellenben saját maga a WeakReference<ThreadLocal> leszármazottja.

Miért érdekes az, hogy nem tartalmaz next mezőt? Azért, mert ütközésnél nem azt csinálja, amit a HashMap, nem fűzi fel egy láncolt listára az elemeket, hanem elkezdi keresni a hash táblában a következő, első üres cellát. Ha minden cella betelt, és körbeért, akkor végtelen ciklusba kerül (haha!). De ez nem következik be, mert amikor 2/3-ig betelt a tábla, akkor megnöveli kétszeresére a táblaméretet. (A HashMap nézi, hogy nem nő-e túl nagyra a tábla mérete, a ThreadLocalMap nem nézi, és nem is nagyon teheti meg, hogy ne növelje magát, amikor kezd tele lenni, mert akkor viszont tele lesz, és akkor végtelen ciklusba kerül. Azért ez érdekes. Szét lehet hajtani a ThreadLocalMap-et?)

A másik, hogy azzal, hogy a WeakReference<ThreadLocal> leszármazottja azt éri el, hogy maga a hash tábla, mint referencia ne gátolja meg a ThreadLocal változó összegyűjtését, ha már semmi más nem hivatkozik rá. Ilyenkor a GC összegyűjti, és a gyenge referencia get() metódusa null-t ad vissza. Más szavakkal, a ThreadLocalMap-t nem használjuk a ThreadLocal változók tárolására. A ThreadLocal változók ebben a map-ben a kulcsok. Ha elveszítjük a kulcsot, nincs rá referenciánk, akkor elveszítjük a hozzá rendelt értéket is, és akkor a GC begyűjti. Ez szuper jó, mert így nem kell külön gondoskodnunk arról, hogy a ThreadLocalMap-ból is töröljük a ThreadLocal változókat. Viszont a map algoritmusa bonyolódik, mert előfordul, hogy olyan entry-t talál, amelyik (mint gyenge referencia) megszűnt objektumra mutat(na), azaz az Entry get() metódusa null-rt-ket ad vissza (stale entry). Amikor ilyennek találkozik, akkor azt törli, viszont ilyenkor minden olyan elemet, amelyik a táblában ez után az elem után jön meg kell vizsgálni, hogy valóban abba a cellába való-e, mint amelyikben van, vagy csak azért került oda, mert az az entry, amelyik már nem létezik és éppen most töröltük kitúrta őt az eredeti helyéről direkt módon, vagy úgy, hogy egy elemet kitúrt és az került arra a helyre, amire ez az elem került volna és így tovább.. Az ilyeneket egészen addig kell keresni, amíg egy üres cellát nem találunk. Ez után már nem lehet olyan érték, ami a most törölt elem miatt lett kitúrva, mert ha lenne, akkor annak itt kellene lennie. Természetesen a tömböt “modulo” kell végigjárni, azaz ha a végére értünk, akkor kezdeni kell az elejéről.

Tovább nézve a kódot találunk egy olyan sort, amelyik

private static final int INITIAL_CAPACITY = 16;

azt mondja, hogy a hash tábla kezdő mérete 16 legyen. A következő kódrészlet egy metódus:

        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }

Mivel a threshold értékét sehol máshol nem állítja a kód, ezért azt lehet állítani, hogy az minden esetben a hash tábla méretének kétharmada.

Hogyan történik a hash kód számítása, amit a map indexeléséhez használ a ThreadMap? A map-ek általában az index-nek használt objektumok hash kódját használják, amit a hashCode ad vissza. No a ThreadLocalMap nem ezt teszi. Minden egyes ThreadLocal objektumnak van egy threadLocalHashCode mezője (ennek beállításáról mindjárt), és ezt használja:

int i = key.threadLocalHashCode &amp; (len-1);

A threadLocalHashCode mezőt az új ThreadLocal objektum létrehozásakor a

private final int threadLocalHashCode = nextHashCode();

sor hozza létre, a nextHashCode() pedig igen egyszerű:

    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    private static final int HASH_INCREMENT = 0x61c88647;

    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

(Mekkorát kurjantana a cseksztájl, hogy magic konstans, és milyen igaza lenne! Amúgy nem is kurjantana, mert konstans definícióban van, és nem kód közé rejtve.) Miért olyan érdekes ez a mágikus szám? Nos azért, mert ha elkezdjük összeadogatni modulo 16, vagy modulo 32, vagy modulo 64 és így tovább, akkor végigmegy az összes lehetséges értéken, anélkül, hogy bármelyik értékre egynél többször lépne. (Nem olyan nagy kunszt ilyet találni egyébként, az 1 rögtön ilyen.) Írtam is rögvest egy kis kódot is, ami ezt ellenőrzi:

package com.verhas.hashtest;

import java.util.concurrent.atomic.AtomicInteger;

import org.junit.Test;

import junit.framework.TestCase;

public class TestHashMagicCode extends TestCase {

	private static final int HASH_INCREMENT = 0x61c88647;
	private static AtomicInteger nextHashCode;

	private static int nextHashCode() {
		return nextHashCode.getAndAdd(HASH_INCREMENT);
	}

	@Test
	public void testHashMagicCode() {
		int hashSize = 16;
		nextHashCode = new AtomicInteger();
		while (hashSize != 0) {
			boolean failed = false;
			int[] arr = new int[hashSize];
			for (int i = 0; i &lt; hashSize &amp;&amp; !failed; i++) {
				int thisCode = nextHashCode()  &amp; (hashSize-1);
				if (arr[thisCode] != 0) {
					System.out.println(&quot;Failed for &quot; + hashSize + &quot; at &quot; + i
							+ &quot; from &quot; + arr[thisCode]);
					failed = true;
				} else {
					arr[thisCode] = i;
				}
			}
			if (!failed) {
				System.out.println(&quot;For &quot; + hashSize + &quot; it is collision free&quot;);
			}
			hashSize &lt;&lt;= 1;
		}

	}

}

Hogy mit írt ki?

For 16 it is collision free
For 32 it is collision free
For 64 it is collision free
For 128 it is collision free
For 256 it is collision free
For 512 it is collision free
For 1024 it is collision free
For 2048 it is collision free
For 4096 it is collision free
For 8192 it is collision free
For 16384 it is collision free
For 32768 it is collision free
For 65536 it is collision free
For 131072 it is collision free
For 262144 it is collision free
For 524288 it is collision free
For 1048576 it is collision free
For 2097152 it is collision free
For 4194304 it is collision free
For 8388608 it is collision free
For 16777216 it is collision free
For 33554432 it is collision free
For 67108864 it is collision free
For 134217728 it is collision free

… aztán elfogyott a heap.

Ez eddig szuper, de hogyan jön ebből a 9?

Nézzük meg, hogy hogyan helyez el a ThreadLocalMap egy új elemet a map-ben:

 private void set(ThreadLocal key, Object value) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode &amp; (len-1);

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) &amp;&amp; sz &gt;= threshold)
                rehash();
        }

Ha olyan hash értéket kapunk, amelyik még nem volt, akkor nem megy bele a ciklusba, amelyik szabad helyet keres, és ha sz (sic! szize) nem nagyobb vagy egyenlő, mint threshold, akkor nem is próbálja méretezni a táblát, jó az akkorának, amekkora volt. Ha pedig a tábla mérete a kezdetekkor 16, akkor treshold értéke 10, vagyis 9 elemig jók vagyunk.

Egyedül a cleanSomeSlots() hívás lehet zavaró, és időt pazarló. Ez azt csinálja, hogy minden egyes új érték beírásakor takarítja egy kicsit a táblát. Akkor is ha van elég hely. Ez a takarítás log_2size lépést hajt végre maximum. Maximum 9 esetén ez 3. A beállított cella utáni három mezőt megnézi, hogy üresek-e, és ha nem üresek, akkor a hivatkozó ThreadLocal objektum él-e még, vagy a GC már begyűjtötte. Ha már nem él az objektum, akkor kitörli, és ez fájdalmas lehet, mert ilyenkor elindul a következő elemeket megkeresni és átpakolni a saját helyükre. Ennek a kódja csak a teljesség kedvéért:

        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal k = e.get();
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    int h = k.threadLocalHashCode &amp; (len - 1);
                    if (h != i) {
                        tab[i] = null;

                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

Összefoglalás

Ha tehát azt akarjuk, hogy gyors legyen a ThreadLocal, akkor ne használjunk többet, mint 9. Ha nagyon kell, akkor használjunk többet, de hozzuk létre az elején mindet, és amikor elindul egy szál, akkor set-eljük be az összeset, mielőtt belekezdünk abba a részbe, amelyikben nem akarjuk, hogy szöszöljön. Vigyázzunk a ThreadLocal objektumokra, amúgy is általában singleton, static objektumok, ne gyűjtse be őket a GC, mert akkor a ThreadLocalMap is elkezd rendezkedni, amikor stale entry-t talál.

És végül, de nem utolsó sorban: a ThreadLocal-ra, mint az osztálybetöltőre is igaz: ha ezt akarod használni, gondolkodj el azon, hogy nem keretrendszert írsz-e éppen, és ha rájöttél, hogy de igen, akkor ülj le a sarokba, és várd meg amíg elmúlik.

Lassú-e a thread local?

Általában azt szoktuk mondani, hogy a túl korai optimalizálás minden gonosz forrása. Ezen már sok házasság is tönkrement, és különböző orvosok is előállnak mindenféle javaslatokkal, mint a láb hideg vízbe dugása, gondoljunk közben másra stb. Vannak azonban olyan esetek, amikor felül kell bírálni ezt az általános állítást. Az egyik az architektúra és algoritmus tervezés.

Ha az architektúra tervezésnél nem törődöm a teljesítménnyel, akkor utólag már profilozhatom a kódot, mint a krimiben a nyomozó a pszichopata sorozatgyilkost: a való életben nem segít a dolog. A halott nem fog feltámadni. Nyögünk az alulteljesítő rendszerrel, vagy újratervezzük, és írjuk az egészet, vagy rossz esetben a versenytársunk teszi ezt meg.

Széljegyzet: Én azt gondolom, hogy az architektúra tervezése során az optimalizálás nem a “túl korai” kategória. Természetesen csak a maga szintjén. Így ez nem dönti romba a fenti általános érvényű szabályt. Optimalizálni több szinten kell. Architektúrát az architektúra tervezés (nem túl korai) szintjén. Kódot meg később.

Van azonban még egy történet, amelyik persze megítéléstől függően szintén nem rombolja le a fenti állítást, és ez a másik történet már kódoláskor jön elő. Ez pedig az, hogy nem használunk olyan konstrukciókat, algoritmusokat és algoritmus implementációkat, amelyek nem felelnek meg általában a követelményeink jellegének. Ezt nem feltétlenül nevezném optimalizálásnak. Csupán arról van szó, hogy nem írunk szar kódot.

Például ha másodpercenként tízezres nagyságrendű tranzakciót kell végrehajtanunk egy sokprocesszoros gépen, és minden tranzakciónak garantált időn belül le kell futnia, akkor nem használunk HashMap-et, még akkor sem, ha egy szálon belül használjuk csak egy időpillanatban. Miért? Nos azért, mert a HashMap hajlamos arra, hogy néha újraszervezze magát. Megtelik, és úgy érzi, hogy nagyon puffad. Ilyenkor megnöveli a kétszeresére a map méretét (ha tudja), és mivel ilyenkor minden indexelés megváltozik, ezért minden egyes elemet átpakol az új helyére. Ez időbe kerül. Nem kevés időbe, és főleg nem prediktív, nem lehet megmondani, hogy mikor fog bekövetkezni, melyik lesz az a thread, tranzakció, amelyik éppen nem fut le, csak mondjuk 20ms késleltetéssel. Persze az egész Java a GC miatt nem igazi real time, de ennek ellenére olyan esetekben, ahol csak pénz, és nem emberélet függ attól, hogy mennyire real time válaszol az alkalmazás van a használatra bőven példa.

Ez a témakör egyébként a szemétgyűjtés is, amelyik a mai (2012) architektúrákon, és a 3GHz processzor órajelek mellett egy másodperc minden kipucolt gigabájtra. De az egy másik téma, amiről csak olvastam, nem volt eddig vele direkt tapasztalatom, olvasni meg más is tud, akit érdekel nézze meg, mit mond magáról az Azul, hogyan oldja meg, hogy ne csukoljon (össze) a szemétgyűjtés alatt a JVM. (Csodát persze ők sem tudnak tenni, valamit valamiért; de azért jópofa, amit csinálnak. Jópofa? Annál azért kicsit több.)

Minap azt mondta nekem valaki, hogy ilyen rendszerekben nem használunk ThreadLocal változókat, mert az hash alapú map, és mint tudjuk az lassú (lehet).

Valóban az lehet, ha nagyon megtömjük. De meg akarjuk-e tömni? (Már megint ennél a témánál vagyunk, mint a cikk elején?) A konkrét esetben egyetlen szál helyi változóról lett volna szó. Egyetlen egyről, és garantált lett volna, hogy az alkalmazás másik része sem használ másik ThreadLocal változót. Amúgy aztán az egész megoldás, amelyik a ThreadLocal használatát igényelte volna zsákutca volt, de egészen más okok miatt. Ennek ellenére a gondolat befészkelte magát a fejembe, és ha valami fészket rak, akkor az általában tojásokat is, ami ki is fog kelni. (Már megint a szex.) Szóval jöttek újabb gondolatok…

Azt gondoltam, hogy ha lassú a ThreadLocal akkor megnézem, hogy pontosan mi is a gond vele, és hogyan lehetne valami olyant készíteni, ami helyettesítheti és nem lassú. És azt találtam…. És akkor itt lassítsunk egy kicsit.

Mi is az a ThreadLocal ?

private static ThreadLocal<Object> alma = new ThreadLocal<>();

A ThreadLocal általában egy private static változó, amelyiknek a set() metódusával tudunk beállítani egy értéket, amit utána a get() metódussal érünk el. A speciális benne, hogy ha két különböző szál, akár konkurensen meghívja a set() metódust, mind a kettő később a get() metódus meghívásakor a saját objektumát kapja vissza.

Hogyan történik ez? Úgy, hogy magában a Thread objektumban van egy map (a neve threadLocals ha valaki bele akar túrni a JDK kódba), amelyikbe a set() beleteszi az argumentumként megkapott értéket, és mivel ez a map minden egyes szál sajátja, a get() minden szálnak a saját objektumát adja vissza. Van egy kis csavar a dologban, hogy mi a ThreadLocal set() és get() metódusát hívjuk meg, de az elkéri a Thread.currentThread() hívással az aktuális szálat, és onnan kezdve már megvan a szál map-ja is. A map indexeléséhez pedig mi mást használna, mint magát a ThreadLocal objektumot.

Mi lehet ebben lassú? A map maga létrejön amikor az első set() hívás az adott szálon végrehajtódik. Mégpedig ezzel a konstruktorral:

        ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

Ez nem tűnik lassúnak, és ha az is lenne, csak egyszer hívódik meg, amikor a statikus ThreadLocal változónak értéket adunk. INITIAL_CAPACITY értéke 16, így még a modulo számítása is igen gyorsra van faragva egy bit művelettel. Ez nem lassú. A map kezelés pedig eléggé speciális, és minden egyes része arra utal (lehet tanulni a kódból), hogy elég rendesen végigelemezték, hogy ne legyen lassú.

Ott kezdődik, hogy hash számítása le van egyszerűsítve. Minek elbonyolítani? A map-hez a ThreadLocal nem a szokásos hash kódot használja, mert annak a számítása lassú (lehet), hanem a

    private final int threadLocalHashCode = nextHashCode();
    private static AtomicInteger nextHashCode =
        new AtomicInteger();
    private static final int HASH_INCREMENT = 0x61c88647;
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

kódot. Szóval ez nem lesz lassú. Mit csinál a set() és a get() ? Ha nincs táblabetelés, és nincs hash kód egybeesés (mert pl. csak pontosan egy ThreadLocal változónk van) akkor egyszerűen kiveszi az entry tömb hash értékének megfelelő elemet. A JDK forráskódban minden más varázslás csak akkor kezdődik és fut, amikor hash kód egybeesés van, vagy kezd betelni a tábla.

Vagyis a ThreadLocal nem lassú, ha kordában tudjuk tartani az étvágyunkat, és nem használunk belőle sokat. Mondjuk maximum 9-et.

Ki mondja meg, hogy miért pont 9-et? Ha más senki, akkor egy hét múlva én.

StringBuilder és String konkatenáció

Vegyünk egy egyszerű programot:

public class StringAdd {
	public static String stringAdd(String a, String b) {
		return a + b;
	}
	public static void main(String[] args) {
		System.out.println(stringAdd("a", "b"));
	}
}

és egy másikat:

public class StringAddUsingStringBuilder {
	public static String stringAdd(String a, String b) {
		return new StringBuilder().append(a).append(b).toString();
	}
	public static void main(String[] args) {
		System.out.println(stringAdd("a", "b"));
	}
}

Dilemma: melyik a jobb. Melyik generál hatékonyabb kódot? Persze tudom, egy ilyen egyszerű példa esetében ez irreleváns, de adott esetben valami hasonló előfordulhat egy nagyobb kódban is, egy erősen terhelt ciklus közepén. Az biztos így ránézésre is, hogy az első megoldás sokkkkaallll olvashatóbb.

Ha elég sokat gondolkodtunk már azon, hogy vajon melyik a hatékonyabb, akkor nézzük meg a bájtkódot (mostanában rettentően beleszerettem ebbe a bájtkódba, ifjúságomat juttatja eszembe, amikor még számított, hogy egy XOR A egy bájttal rövideb, mint a LD A,0).

Az első programból generált bájtkód releváns része:

  public static java.lang.String stringAdd(java.lang.String, java.lang.String);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=2
         0: new           #2                  // class java/lang/StringBuilder
         3: dup
         4: invokespecial #3                  // Method java/lang/StringBuilder."<init>":()V
         7: aload_0
         8: invokevirtual #4                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        11: aload_1
        12: invokevirtual #4                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        15: invokevirtual #5                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        18: areturn

A második programból generált kód:

  public static java.lang.String stringAdd(java.lang.String, java.lang.String);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=2
         0: new           #2                  // class java/lang/StringBuilder
         3: dup
         4: invokespecial #3                  // Method java/lang/StringBuilder."<init>":()V
         7: aload_0
         8: invokevirtual #4                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        11: aload_1
        12: invokevirtual #4                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        15: invokevirtual #5                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        18: areturn

Akkor most ebből tessék levonni a következtetést.

$ java -version
java version "1.7.0_07"
Java(TM) SE Runtime Environment (build 1.7.0_07-b10)
Java HotSpot(TM) 64-Bit Server VM (build 23.3-b01, mixed mode)

Azért, mielőtt még valaki kidobná a forráskódjából az összes StringBuilder-t: nem mindent konvertál át a javac ilyen okosan! Ha egy ciklusban fűzöm össze egy String tömb elemeit, akkor minden egyes ciklus végén visszaalakítja az eredményt String-é, és minden cikluslefutás előtt új StringBuilder-t épít!

Eddig és ne tovább avagy refaktor és alkoholizmus

Talán még gyerekkoromban láttam egy Columbo (főszerepben Peter Falk) filmet, amiben egy ír alkoholista (egyébként ő volt a gyilkos) rendszeresen karcolgatta a whiskey-s üveget a gyémánt gyűrűjével. Mielőtt este elkezdett inni, megkarcolta valahol, hogy addig fogja leinni az üveget, és nem tovább. Amikor már italos volt nem tudott volna megállni, de ha ott volt a kis bekarcolt vonal, akkor azért megállt.

Eddig és ne tovább

Ez jutott nemrég eszembe a refaktorálás során. Annak ellenére, hogy mindannyian tudjuk, hogy a refaktorálás és a redesign között a különbség a tíz perc, — minden ami ennél rövidebb az refaktor, ami hosszabb az redesign — mégis kísértést érzünk egy hiba kijavítása során, hogy nekiálljunk refaktorálni, és ha nem húzzuk meg előre a határt, hogy “eddig és ne tovább” akkor azt vesszük észre, hogy redesign lesz belőle.

Alapszabály, hogy a refaktoráláshoz mindenképpen szükséges előfeltétel a megfelelő lefedettségű unit teszt. Természetesen ha van elegendő unit teszt, akkor nem lesz annyi hiba. Tehát amikor support projekten vagy, és kapsz egy halom kódot, hogy javítsd a hibákat — mert bizony vannak — akkor jó az esélyed, hogy nem lesz hozzá elegendő (ha ugyan egyáltalán) unit teszt, és arra is jók az esélyeid, hogy az objektum orientáltság három programozási alapelvét (egységbezárás, öröklődés, többalakúság) messze megelőzi a programozás sokkal alapvetőbb és sokkal szélesebb körben ismert és elterjedt paradigmája a copy-pasta. (Érted a szóviccet: pasta, mert hogy spagetti kód.)

Ilyenkor nagyon nehéz megállni, hogy ne akarjál mindenképpen refaktorálni. Talán nem is lehet megállni. Talán nem is kell. Persze unit tesztet írni ilyenkor sem lehet, mert arra nincs idő, marad a manuálisan végzett funkcionális teszt. Szerencsére a dokumentáció olvasás sok időt nem visz el. (rotfl)

Na jó. Szóval belehekkelni a kódba nem szabad, mert akkor a kód minősége egyre rosszabb lesz, és ilyenkor jut el a projekt egy olyan fázisba, hogy az ismert hibák száma ugyan exponenciálisan csökken de a határérték nem a nulla. Ha pedig nem akarunk hackolni, akkor refaktorálni kell, és ilyenkor kell előre megállapítani, hogy mit fogunk refaktorálni, és mi az amihez nem nyúlunk. Mert ha ez nincs, akkor menthetetlenül kiisszuk az egész üveget, vagy ha az túl nagy, akkor addig iszunk, amíg el nem ájulunk, vagy a projekt menedzser ki nem veszi a kezünkből az üveget, és ránk nem szól, hogy most már valami revenue generating dolgot kellene csinálni. A debugging ugyanis nem bevétel generáló tevékenység. Tényleg nem. Ez ugyan egy Bill Gates-nek tulajdonított híres mondás, de van benne igazság: a hibajavítás és még inkább a refaktor direkt módon tényleg nem generál bevételt. A költségeket csökkenti, de azt sem egyszerűen. A jövőbeli költségeket csökkenti a jelenlegi költségek növelése árán. Ha a jelenlegi költségek FV-ja (future value) kisebb, mint a jövőbeni csökkenés és van is rá fedezet, és ezt a budget-et nem is tudjuk másra nagyobb eredménnyel befektetni, akkor hajrá. Ilyen egyszerű a közgazdaságtan elmélete. Aztán mondja meg valaki ezt konkrét esetben. Ez tehát nem járható út annak megállapítására, hogy mennyit refaktoráljunk egy hiba javítása során. Természetesen az elv az ez: nőjön a profit, ami a bevétel mínusz a kiadás, de ez nem alkalmas modell, amikor ott ülök az Eclipse előtt.

No de hát akkor mit lehet mondani? Mennyit refaktoráljak? Azt már tisztáztuk, hogy érdemes előre elhatározni, hogy mennyit fogok refaktorálni, de arról még nem beszéltünk, hogy az a gyémánt karc az üvegben egy centivel a jelenlegi szint alatt legyen, kettővel, vagy akár az üveg fenekén. Őszintén: nem tudom. Ha ilyesminek nekiállok függ attól, hogy mekkora az időnyomás. Mennyire gázos/gazos a kód. Milyen programom van este. Van-e más feladatom, amivel szívesebben foglalkozom. Tovább is enyém lesz-e a kód még hosszú évekig, én felelek-e majd érte, vagy valaki más? Sokáig fogják sokan használni az alkalmazást vagy csak kevesen, és nem a menedzsmentből 🙂

Minden kód amit leírok vállalható kell, hogy legyen. Ha új kódot írok akkor is, de ha hibát javítok, akkor is. Azt az elvet szoktam követni, hogy azt a részt, ami a javított kód képernyőnyi közelében van azt refaktorálom. Persze, ha egy rövid kis metódusról van szó, és mellé van hányva egy képernyőnyi távolságban két teljesen irrelated másik metódus, azért azokhoz nem nyúlok. Átírok egy feltételt az IF utasításban, és olyankor átírom a következő blokkot is, meg az else utáni részt is ha az nem volt vállalható.

Copy-pasta kódnál kiemelem, és készítek egy új metódust, osztályt stb, de csak a hiba javításánál hívom az új kódot, a többi helyen csak kap egy TODO kommentet. Ahogy a Perl mondja: it ain’t broke, don’t mend. Nincs rá idő.

Valahogy úgy érzem magam, mint egy lebombázott városban. Kevesen lakunk ott. Kijavítjuk a közműveket, és eltakarítjuk a romok egy részét ott ahol élünk. A többi marad rom. Nem tölthetjük azzal az időnket, hogy az egészet újra építjük. Élni kell.

és, hogy ezzel nem vagyok egyedül

Ezeket a cikkeket megjelenés előtt át szoktam küldeni néhány kollégának. Nem mindegyiket, de ezt például igen. Egyikük ezt írta vissza:

Én egyedül élek a romok alatt és vannak kitaposott ösvényeim….

Kód revjúzom

code review

Mi járhat annak a Java programozónak a fejében, aki egy konstruktorban beállított final mező gettere elé odaírja, hogy synchronized?

Programozási marhanyelv pácban, és birkapásztorok

A lombok kapcsán merült fel a téma ezen a blogon, meg ma reggel, amikor a fiam, aki első éves villamosmérnök hallgató, kérdezte, hogy az egységbezárás kapcsán azért nem piszkálhatunk bele az objektumok belsejébe, mert nem lehet, vagy mert nem ajánlott, nem jó.

Nyelvfüggő. Két fő irányzat van, meg ezek tetszőleges keverékei és verziói, perverziói.

Az egyik irányzat nemes képviselője a kihalófélben levő nyelv: a Perl. Ő azt mondja, hogy semmi sincs védve, ha nagyon akarod, és belenyúlsz kívülről egy objektum belsejébe, akkor rajta. Te tudod mit csinálsz, kaptál fegyvert, lődd magad tökön, biztos arra van szükséged. Nem azért nem jössz be a hálószobámba mert gépfegyver van a kezemben, hanem azért, mert úriemberek vagyunk.

A másik irányzat azt mondja, hogy nem baj ha úriember vagy de azért én bezárom az ajtót. A Java is ilyen. Na jó, pont ezen a blogon megmutattam, hogy néha a virágcserép alatt az ajtó mellett el van rejtve a kulcs, de alapvetően a Java nem perl.

Melyik a jó irányzat? Van-e jó irányzat, és van-e üdvözítő megoldás, vagy mindegyik másra jó? Ragaszkodjunk-e a Java-hoz, vagy ipari környezetben is izzítsuk a Node.js motorokat, és hajrá JavaScript?

Szerintem van jó irányzat a kettő közül, és ez a Java (szerű megközelítés, nem csak ez a nyelv, mint egyedül üdvözítő). És ugyanakkor megvan a helye a perl-nek is. És ez nem ellentmondás, csak a világ bonyolult.

A kibogozás most is, mint az élet nagyon sok, de nem minden területén: maximalizáljuk a profitot. Közgazdaságtan.

Van egy feladat, meg kell oldani. A feladat megoldása ér valamennyit az ügyfél számára. A feladat elvégzése jár valamekkora költséggel. A kettő különbsége a profit. Ha ez negatív a projekt felejtős. Ha pozitív, vágjunk bele. Lehet haragudni a profitra, de lehet haragudni az esőre is: attól az még van, megszüntetni nem lehet. Ha a kialkudott ár pont az, ami a vevőnek megéri, akkor a szállítónál realizálódik a profit, ha ennél alacsonyabb az ár akkor osztoznak rajta, egészen addig, míg az ár meghaladja a költséget. Ha az ár pont a költséggel egyenlő, akkor a profit a vevőé. Tipikusan open source projekteknél a profit a vevő zsebét gazdagítja. Nem fizet, nem is hívjuk vevőnek, “csak” felhasználónak. A vevő az aki fizet. A költség meg valakit terhel. Szponzort, aki valamit, járulékos eredményeket visszakap, mondjuk marketing értéket, vagy olyan kutatási eredményeket, amelyek kiesnek az open source projektből. Vagy csak a fejlesztő érezte jól magát, amíg megalkotta élete programját, amelyikre büszke. Na másszunk ki a gondolati stack-ből, mielőtt még újabb és újabb frame-et nyitok, és egy kivétel kezelésével tudok csak kijutni belőle. Vissza a programozási nyelvekhez: RETURN.

Alkalmazzunk programozónak úriembereket, akik betartják a szabályokat és pontosan érzik, hogy a szabályokat hol lehet, szabad áthágni, úgy, hogy abból még ne legyen baj, és ne burjánozzon el a gaz a pasta kódban az alatt az idő alatt, amíg a kódot el nem felejtjük és olvashatatlanná válik az utolsó archive lyukkártya is. Vagy alkalmazzunk kóding junkie-kat, akik spagetti kódot gyártanak olyan kuszán, amilyen kuszán csak lehet, és inkább próbáljuk meg ezt a “lehet”-et szűkre venni. Junk programozóból több van, könnyebb találni és olcsóbb. Könnyebb lecserélni is, ami csökkenti a személy rizikóját (elüti a villamos, vagy megnyeri a lottót és elköltözik Bora Borára). A rizikó is költség. És a személy cserélhetősége és elérhetősége egyre fontosabb, ahogy halad az idő.

Ebből az következik, hogy rövid programoknál, amik nem élnek sokáig (prototype például) lehet használni úriembereket. És lehet használni olyan nyelveket, mint a perl vagy a php. Vagy hobbi projektnél, ahol elkövetik azt a közgazdasági nonszenszt az emberek, hogy nem gondolnak a profittal. No sebaj, ilyenek is kellenek, pro bono. De olyan programoknál, amelyek komoly értéket képviselnek, sokan használják, nagy költséggel állítják elő, nem jó ez a megközelítés. Ebben az esetben átlagos programozókat kell használni.

Persze az átlagos programozó nem kód junkie. Annál azért jobb. Aki kód junkie azt nem is szabad programozónak hívni. A kettőt az különbözteti meg, hogy bár mind a kettő vét a programozási paradigmák ellen, de a kód junkie-t ez nem is érdekli, mert nem érti meg, hogy ezt miért nem szabad. Ő az aki nem is akarja bekötni a biztonsági övet. Az átlag programozó csak időről időre elfelejti ezt megtenni. És ilyenkor kellenek a seniorok, akik segítenek, figyelmeztetnek, kontrollt adnak, tanítanak, terelgetnek, mint a nepáli birkapásztorok.

És ilyenkor kellenek azok a nyelvek, amelyek nem engedik meg, hogy bemenj a hálószobába, hanem már fordítási időben a lábadra lépnek, és azt mondják, hogy ezt nem! Ez a mező számodra nem elérhető! Ezt nem teheted meg. Ne kívánd felebarátod feleségét, se privát mezőit, se más egyéb jószágát! És ilyenkor elgondolkodik az átlag programozó, hogy hogyan kell elkészíteni a kódot jól. Úgy, hogy ne csak működjön, de olvasható is legyen.

Szóval a programozási nyelvnek olyannak kell lennie, hogy az átlag programozó se lője rendszeresen tökön vele magát. Amikor egy nyelvet megterveznek, akkor nem csak arra kell gondolni, hogy a nyelven hogyan lehet valamit jól megoldani, hanem arra is, hogy hogyan lehet jól elbaltázni, rosszul megoldani egy feladatot. A Java így lett megtervezve. Tiszta, világos, olvasható. Minden az, aminek látszik. Közben a közösség folyamatosan nyomja, hogy legyen flexibilisebb. Vegyen fel nyelvi elemként olyan dolgokat, amik a nyelv tervezése óta fejlődtek ki, mint programozási pattern-ek. Így került bele a nyelvbe a generikus típus. Ami nagyon jó és klassz, és az eredeti tervezési irányba mutat. És így nem került be a nyelvbe az extension method, amit a közösségből sokan kértek, de a józan többség leszavazta. Aztán lombokkal mégis meg lehet csinálni. De nem kell. Megint mászok lefelé a gondolati stackben.

Nah, mára elég belőlem.

System.exit(0);