Tiszta kód – 2. rész: függvények

Bevezető

Minden valamirevaló kódsor végső soron egy függvényben köt ki. Valójában ez a kódunk rendezésének első szintje. Ezért nagyon fontos néhány szempontot figyelembe venni.
Mitől lesz tiszta egy függvény? Ez a poszt erről fog szólni.

Első szabály

A függvények legyenek rövidek!

Második szabály

Még annál is rövidebbek!
A nyolcvanas években a kimondatlan szabály az volt, hogy : “Férjen el egy képernyőre.”
Ez manapság már nem helytálló. Akkoriban ugyanis egy képernyő még csak 20 sort tudott megjeleníteni. Manapság ha megdöntjük a képernyőt és kellően kicsire állítjuk a betűméretet, akár 100 sor is elfér a képernyőn.
Mit mondanak a profik?

  • Uncle Bob: “A 4-5 soros függvények még rendben vannak. A 10 soros már nincsen.”
  • Bjarne Stroustrup: „A ’nem fér a képernyőre’ egy elég gyakorlatias megfogalmazása annak, hogy túl hosszú a függvény. Az 1-5 soros függvényeket tekinthetjük normálisnak.”

Sokan gondolhatjátok, hogy ez túlzás, de tény, hogy a nagyon rövid függvényeknek vannak előnyei. Hogy mik ezek?

  • Gyakorlatilag nincs behúzás, ezért a kód olvasásakor kisebb “vermet” kell felépíteni agyunkban, ezért könnyebben átlátjuk a kódot.
  • Egy for vagy while ciklus magjában gyakorlatilag csak 1 sort enged meg a szabály, ezért ha annak az egysoros függvénynek, ami a ciklus magjában van jó nevet adunk, könnyű átlátni, hogy mit is csinál a ciklus.
  • Ha könnyen érthető a logika, akkor a hibák is nehezebben bújnak el a sorok között.
  • Nem kell a kódban görgetni, a navigációban az IDE segít. (Általában Ctrl katt.)

Saját kódjaimban igyekszem tartani az 5-6 soros szabályt, de én nem vagyok annyira radikális, hogy a kapcsos zárójeleket is beleszámítsam.

Miért szeret(jük/tük) a hosszú függvényeket?

A programozók nagy része férfi. Mi férfiak pedig igen csak büszkék vagyunk a tájékozódó képességünkre.
A nagy függvényeket ha megdöntjük, nagyon hasonlítanak egy hegyekkel, völgyekkel szabdalt terepre. Nekünk férfiaknak pedig arra van kitalálva az agyunk, hogy terepen tájékozódjunk. Az alábbi képen az Alpok hegységet és egy kódrészletet láthatunk. (A hasonlóság nem a véletlen műve.)

alpok_kod
Továbbá a kódunkat ismerjük. Tudjuk, ha változtatni kell rajta, azt adott esetben a második legnagyobb hegy utáni kis dombon kell megtenni. Tudjuk, hogy egy adott hibajegy a harmadik nagy while ciklusunk magjának végén kerül kiírásra.
Kényelmesen átlátjuk, hiszen a kód fejlődése és írása közben memorizáljuk a kód szerkezetét is.

Mi a probléma mégis?

A probléma akkor kerül felszínre, amikor nem ismerjük a kód szerkezetét. Talán olyan régen írtuk, hogy már nem emlékszünk, vagy mások levetett hegyvonulatait örököltük meg.
Ilyenkor gyakran hosszas fejvakargatás után az egész függvényt nulláról újraírjuk. A problémák nagyon gyakran tehát nem is fejlesztési időben, hanem a karbantartás során jönnek elő.

A hosszú függvények kórtörténete

Minden függvény rövidnek indul. Általában az a mentalitás duzzasztja, hogy a fejlődése során mindig csak egy – két sort adunk hozzá, azért pedig “nem érdemes új függvényt írni”.
Jellemzően tehát megírjuk azt a pár sort, és ellátjuk egy megjegyzéssel.

Produktivitás problémája

Egy új csapattársnak igen nehéz egy hosszú függvényben tájékozódni. Ha még megjegyzések sincsenek, majdnem lehetetlen a küldetése. Mindenesetre sok időre van szüksége ahhoz, hogy produktívvá tudjon válni. Ellenben ha mindenhol jól elnevezett függvényekkel találkozik, az olyan, mintha jelzőtáblákkal találkozna út közben. Hamar rájön, hogy a kódnak melyik része érdekes a feladata kapcsán, és hamar produktívvá tud válni. Csapatmunkánál tehát sokszorosan felértékelődik a rövid függvények szabálya.

Gyakori ellenvetések

Elveszünk a sok apró függvényben.

Ha jó neveket választunk,inkább segítenek a tájékozódásban és nem gátolnak. Beszélni fogunk arról is, hogy a fájlok és osztályok méretét is szabályoznunk kell, ez is segíteni fog.

Illusztrálva a jelenséget, attól tartunk, hogy az első ábrán látható információ rengetegben találjuk magunkat. Egy hosszú függvényben azonban csak azt látjuk, hogy fentről indul és lefelé tart. Létezik azonban arany középút is.

A függvény hívások lassítják a programot.

Igen. A 70-es években ez a plusz idő mikroszekundumokban volt mérhető, manapság nanoszekundumos nagyságrendben van. Beágyazott rendszerek esetén ugyan lehet kritikus, de a legtöbb alkalmazás esetén nem helytálló ez az érv. Ráadásul a mai fordítók olyan jók, hogy lehet, hogy ki is optimalizálják a kódból a problémás részt.
A kódot tehát érdemes inkább olvashatóságra optimalizálni. (Beágyazott rendszereknél pedig a főciklusban lehet in-line-olni.)

Sok időbe telik és nehéz így kódolni

Egy függvény kiemelése kíván némi időt, de a mai fejlesztő eszközökben létezik a “metódus kiemelése (extract method)” refaktorálási lehetőség, így ez az időköltség is minimális. Hosszú távon pedig rengeteg időt spórolunk meg.

funcextract

Az angol “methodize” szó jelentése szó szerint rendszerezést jelent. Ha az Interneten rákeresel erre a szóra, ilyesmi képeket találsz. Pont ebbe a posztba illenek. Te melyikben éreznéd jobban magad?

bathroom-closet-before-after1

Egy hosszú függvényben osztály(ok) lapulhat(nak) meg.

hosszu_fuggvenyMi egy osztály nagyon leegyszerűsítve? Egy kupac függvény, melyek közös változókon keresztül kommunikálnak egymással. Másrészt egy hosszú függvény több funkcionális részből áll, melyeket a főbb indentálások jelölnek, és azért hosszú, mert ezek a funkcionális részek azonos adathalmazt kell manipuláljanak. Ezért tehát a legtöbb hosszú függvény egy, vagy több osztályra bontható.

Egy dolgot csináljon!

Fontos szabály, hogy egy függvény egy dolgot csináljon. Ha több funkcionális részre bontható, akkor több dolgot csinál. Ha több absztrakciós szinten dolgozik, szintén több dolgot csinál.

Honnan tudjuk biztosan hogy több dolgot csinál?

  • Ha új metódust lehet kiemelni, akkor definíció szerint több dolgot csinál.
  • Uncle Bob : “Ha kapcsos zárójelet kell használni a kódban, az már lehetőséget rejt új függvény kiemelésére.”

Mi a teendő?

Addig emeljünk ki új függvényeket, amíg csak értelmesen tudunk. (Extract till you drop!)

Lássunk már kódot!!!

A következő kód egy elképzelt képfeldolgozó funkcióval is rendelkező szoftver részletét mutatja be. Az init függvény célja, hogy példányosítson egy képfeldolgozó objektumot. Néha előfordulhat, hogy valami osztott erőforrás zárolása miatt nem sikerül létrehozni a példányt. Ezt az eshetőséget kezelni kell. Ezen kívül az képfeldolgozást több szálon szeretnénk futtatni, tehát több ilyen objektumot kell példányosítani. Tegyük fel, hogy a szálkészlet tervezési mintát alkalmazzuk. Ehhez készítünk feldolgozó (worker) osztályt.
A kód valós környezetből kerül ide, a problémát általánosítottam, hogy ne legyen felismerhető.

void ImageProcessingWorker::init()
{
    int try_count = 0;
    int max_try_count = 10;
    bool imageProcessingInited = false;
    while (!imageProcessingInited && (max_try_count > try_count) )
    {
        try
        {
            try_count++;
            ip = new ImageProcessor();
            int maxlen = 1024;
            char* errorstring = new char[maxlen];
            memset(errorstring, 0, maxlen);
            int errorcode = 0;
            ip_geterror(&errorcode, errorstring, maxlen-1);
            if ( !ip->IsValid() || (errorcode != ip::IP_NOERR))
            {
                cout << "ImageProcessor module creation FAILED "
                     << ", error code is: " <<  errorcode
                     << ", error string is: " << errorstring << endl;
                delete ip;
                ip = NULL;
            }
            else
            {
                imageProcessingInited = true;
            }
            delete[] errorstring;
            errorstring = NULL;
        }
        catch(IpException &e)
        {
            logIpError("init", e);
            imageProcessingInited = false;
        }
        if (!imageProcessingInited)
        {
            usleep(500000); //sleep 500ms
        }
    }
}

Ezzel a kóddal sok probléma van. Egyrészt ez a függvény nagyon hosszú. Másrészt több absztrakciós szinten dolgozik. Van itt minden; számlálástól kezdve hibakezelésen át az alacsony szintű (C-ből örökölt) memset-ig. Az ip_geterror metódus léte tervezési hibára utalhat, hiszen a használt API nem kényszeríti ki ennek a metódusnak a hívását. (Vajon mi történik, ha kétszer hívjuk egymás után???)
Ha vesszük a fáradtságot és végigböngésszük a kódot, a következőket deríthetjük ki.

  • A fő ciklus maximum 10 alkalommal fut le.
  • A célja, hogy sikeresen példányosítson egy FaceRecognizer objektumot.
  • Kezeli a hibákat.

Mennyivel könnyebb ezt a kódot megérteni:

void ImageProcessingWorker::init()
{
    for (int i = 0; i < 10; i++)
    {
        tryToCreateImageProcessor();
        if (imageProcessorCreationOk())
        {
            return;
        }
        imageProcessor.clear();
        usleep(500000);
    }
    throw TimeoutException("Failed to create image processor.");
}

Az eredeti kód kis munkával átalakítható erre. A teljes megoldást a poszt végén találjátok. Röviden összefoglalva a következő észrevételeket tehetjük:

  • A hibakód kezelése saját függvényt kapott
  • A nehézkesen értelmezhető while ciklusunk egy egyszerű for ciklusra cserélhető.
  • A try-catch blokk elegánsan leválasztható.
  • A kód egy kivétellel jelzi, ha tízszeri próbálkozásra sem sikerül az osztály példányosítása. Az eredeti változatban ez hiányzott, a program ilyenkor érvénytelen állapotba került.

A tisztesség kedvéért megjegyzem, hogy a refaktorált kód nem teljesen ekvivalens az eredetivel. Az egyik változtatás, hogy több helyen a Qt okos mutatóit használom. Ez a kódot átláthatóvá és biztonságossá teszi.

A kód fenti refaktorált verziója még közel sem az optimális megoldás, de már megmutatja a függvénykiemelésben rejlő lehetőségeket.

//
// ImageProcessingWorker.cpp
//

void ImageProcessingWorker::init()
{
    for (int i = 0; i < 10; i++)
    {
        tryToCreateImageProcessor();
        if (imageProcessorCreationOk())
        {
            return;
        }
        imageProcessor.clear();
        usleep(500000);
    }
    throw TimeoutException("Failed to create image processor.");
}

void ImageProcessingWorker::tryToCreateImageProcessor()
{
    try
    {
        imageProcessor = 
            QSharedPointer<ImageProcessor> (new ImageProcessor());
    }
    catch(IpException &e)
    {
        logIpException("init error", e);
    }
}
 
bool ImageProcessingWorker::hasIpCreateError()
{
    IpCreationResult result = IpCreationResult.getLatest();
    if(result.isError())
    {
        logIpCreationError(result);
    }
    return result.isError();
}

void ImageProcessingWorker::logIpCreationError(IpCreationResult result)
{
    cerr << "ImageProcessor module creation FAILED "
         << ", error code is: " <<  result.errorCode
         << ", error string is: " << result.errorMessage << endl;
}

bool ImageProcessingWorker::imageProcessorCreationOk()
{
    return (imageProcessor->IsValid() && !hasIpCreateError());
}

//
// IpCreationResult.hpp
//

class IpCreationResult
{
private:
    IpCreationResult(int _errorCode, std::string _errorMessage)
    {
        errorCode = _errorCode;
        errorMessage = _errorMessage;
    }

public:
    static IpCreationResult getLatest()
    {
        const int maxlen = 1024;
        QScopedArrayPointer<char> errorStr(new char[maxlen]);
        memset(errorStr.data(), 0, maxlen);
        int errorCode = 0;
        ip_geterror(&errorcode, errorStr.data(), maxlen-1);
        std::string errorMessage;
        errorMessage.assign(errorStr.data());
        return IpCreationResult(errorCode, errorMessage);
    }

    bool isError()
    {
        return errorCode != ip::ip_NOERR;
    }

    int errorCode;
    std::string errorMessage;
};

Most akkor tényleg 40 sorból csináltunk 80-at?

Felmerülhet a kérdés, hogy miért jó egy negyven soros kódot nyolcvan sorossá refaktorálni. A kérdésre két rövid válasz van.

  • A refaktorált kódot úgy is sokkal könnyebb megérteni, hogy hosszabb, hiszen nem egy rejtvény.
  • Ha minket nem a teljes működési logika érdekel fejlesztés közben, akkor nem is kell elolvasni mind a 80 sort. A rendszerezetlen kódrészletet, ahhoz, hogy tovább tudjuk fejleszteni el kell olvasni és meg is kell érteni. Igen, az egészet.

Források

  1. Clean Code: Functions
  2. Clean Code – A Handbook of Agile Software Craftsmanship — Martin, Robert C. Prentice Hall, 2009
  3. C++ Core Guidelines
Tiszta kód – 2. rész: függvények

Tiszta kód – 2. rész: függvények” bejegyzéshez ozzászólás

  1. Pal Mezei szerint:

    Sok rendszerrel az atlathatosag szempontjabol pont azert vannak problemak, mert mindenki (probalja) betartani a rovid fuggveny szabalyt. Egyetlen fuggveny megertesehez rengeteg kulso fuggvenyen kell atugralni, sokszor ugy, hogy ezeknek a fuggvenyeknek a nagy resze one shot.

    A programozok felnek a hosszu fuggvenyek esszeru hasznalatatol. Nem azert mert azokkal tenyleg problema van, hanem mert valahol olvastak rola, es esz nelkuk betartjak a szabalyokat, ahelyett, hogy a fejlesztesi elveket a sajat problemajuk szerint formalnak.

    A szintetikus peldanal lehet a kiemeles jobban mukodik, viszont rengeteg ellenpelda van ra.

    Kedvelés

    1. Hogy egy közhellyel éljek: kivétel erősíti a szabályt. Általánosságban mégiscsak a rövid függvények a normálisak. Szerintem is át lehet esni a ló túloldalára, de ez általában még mindig a jobb eset. Az egysoros függvényeknek például olyankor van értelme, amikor egy ciklus vagy elágazás összetett feltételét helyettesítjük egy egyszerűen olvasható kifejezés-függvénnyel.
      A példát, bár erősen torzított verzióban került publikálásra, mégsem nevezném szintetikusnak. Valós probléma lett átdolgozva.
      Viszont nagy örömmel látnék olyan (több) száz soros függvényt, amely nem kiált refaktorálásért, de olyat is, ahol a (helyesen használt, jól elnevezett) rövid függvények okoznak problémát.

      Kedvelés

      1. Pal Mezei szerint:

        A legjobb pelda amit itt irtam korabban, a one-shot fuggvenyeket szerintem teljesen felesleges kiemelni a kornyezetbol az esetek jelentos reszeben.

        Egyszeruen azert, mert atlathatobba teszi az algoritmust, ha latod mi tortenik, es nem kell folyamatosan mas fuggvenyekhez ugralni. Emelle van olyan elonye is, hogy nem szennyezzuk a nevteret olyan fuggvenyekkel, amit csak egyszer hasznalunk fel valahol. Ezzel az egesz api tisztabb lesz, es az ujonnan jovok fele kevesebb lesz a megismerendo fuggveny / metodus.

        Ha olyan fuggvenyrol van szo, ami, valtozatlan formaban, legalabb ketszer hasznalsz, fokent ket egymastol “messze” levo fajltol, akkor mar mas a helyzet. De ilyenkor is gyakran az a szitu, hogy nem ugyanarra a ket fuggvenyre van szukseged, hanem ket eltere variaciora. Szoval vagy fuggveny, ami sok parametert eszik, es felesleges divergenciat ad a rendszerhez, vagy ket specialiazalt, inlinelt valtozat.

        Ezek persze nagyban fuggenek a kiemelendo kod bonyolultsagatol, felepitesetol, vagy az apro divergenciaktol a kulonbozo felhasznalasi teruleteken. Nincsenek okor szabalyok.

        Kedvelés

Vélemény, hozzászólás?

Adatok megadása vagy bejelentkezés valamelyik ikonnal:

WordPress.com Logo

Hozzászólhat a WordPress.com felhasználói fiók használatával. Kilépés / Módosítás )

Twitter kép

Hozzászólhat a Twitter felhasználói fiók használatával. Kilépés / Módosítás )

Facebook kép

Hozzászólhat a Facebook felhasználói fiók használatával. Kilépés / Módosítás )

Google+ kép

Hozzászólhat a Google+ felhasználói fiók használatával. Kilépés / Módosítás )

Kapcsolódás: %s