Tiszta kód – 3.rész: függvénystruktúra

Bevezető

Ebben a részben tovább vizsgáljuk, hogy mitől lesz egy függvény “tiszta”. Hány paramétere lehet egy függvénynek? Milyen paraméterei lehetnek egy függvénynek? Hogyan rendezzük a függvényeinket az osztályokon belül? Mi a probléma a switch szerkezetekkel? Milyen problémákat vet fel az értékadás? Milyen nehézséget okozhatnak a brake és a korai return utasítások. Többek között ezekkel a témákkal foglalkozunk ebben a részben.

fractal1

Függvényparaméter

Paraméterek száma

Az általános szabály a függvény paramétereinek számára, hogy minél kevesebb, annál jobb. Egy függvénynek ne legyen háromnál több paramétere! Ennek két praktikus oka is van.

  • Minél több van, annál nehezebb rájönni, hogy mire is valók.
  • Minél több van, annál nehezebb fejben tartani a helyes sorrendjüket.

A sok paraméterű függvények gyakran csoportosan fordulnak elő, és a paramétereik is azonosak. Ilyenkor érdemes megfontolni, hogy egy struktúrába csomagoljuk a paramétereket, hiszen annyira összetartoznak, hogy meg is érdemlik. A másik lehetőség, hogy egy osztály lapul a függvények között, és az átadott paraméterek pedig az osztály adattagjai. Ilyenkor pedig a függvények közvetlenül elérhetik, nincs is szükség paraméterátadásra.

A “maximum három paraméter” szabály érvényes a konstruktorra is. Ha három paraméter nem elég, érdemes elgondolkozni a Builder tervezési minta használatán.

Paraméterek típusa

Ne adjunk át bool paramétert!

A bool paraméter általában azt jelzi, hogy az adott függvény két dolgot csinál. Az egyiket az igaz esetre, a másikat a hamisra. Ezért gyakran az a legjobb megoldás, hogy két függvényt írunk, egyiket az igaz értékre, a másikat a hamisra.
Vegyünk egy példát!

saveEventId(id,true);

A fenti kódnál vajon mit jelent a true? Nehéz kideríteni csak ránézésre. Ha utánamegyünk, megtaláljuk a kódban, hogy igaz ágon az eseményazonosítót nem csak egy listába, hanem például egy statisztikai modulba is elmentjük. Ez a mellékhatás nem derül ki a függvény nevéből sem.

Sokszor segít az olvashatóságban az enumok használata. (Az enumok problémáiról később.) Nézzük a következő példát.

String status = module.getStatus(true);

Vajon mit jelenthet az a true paraméter? A modul a státuszát egy JSON struktúrába ágyazva adja vissza. Igaz ágon ez a JSON formázott, hamis ágon pedig kompakt, behúzások nélküli.
Valamivel jobb így:

String status = module.getStatus(INDENTED);

De a probléma a régi, igazából a függvényen belül ugyanaz az elágazás van, és ugyanúgy két dolgot csinál. Talán így a legjobb:

String status = module.getIndentedStatus(); // getCompactStatus();

Van olyan helyzet, amikor elfogadhatjuk a bool paramétereket, de itt is érdemes figyelni. Az alábbi kódot nehéz szeparálni két függvényre úgy, hogy ne legyen számottevő kódismétlés bennük.

public Booking book (Customer aCustomer, boolean isPremium) {
    lorem().ipsum();
    dolor();
    if(isPremium)
        sitAmet();
    consectetur();
    if(isPremium)
        adipiscing().elit();
    else {
        aenean();
        vitaeTortor().mauris();
    }
}

Ilyen esetben érdemes priváttá tenni ezt a függvényt és két publikus függvényt írni, melyek ezt hívják megfelelő paraméterekkel. Így:

public Booking regularBook(Customer aCustomer) {
    return hiddenBookImpl(aCustomer, false);
}

public Booking premiumBook(Customer aCustomer) {
    return hiddenBookImpl(aCustomer, true);
}
private Booking hiddenBookImpl(Customer aCustomer,  boolean isPremium)
{
    ...
}

bool beállítók

// Általában jobb ez a megoldás
void setOn();
void setOff();

// De ha inkább ilyen kontextusban használnánk:
if (aValue)
    setOn();
else
    setOff();

// Akkor praktikusabb így 
setSwitch(aValue);

Kimeneti paraméterek

Ne használjunk kimeneti paramétereket. A kód olvasója nem számít arra, hogy információ jön kifelé egy függvény paraméterén keresztül. Használjuk inkább a visszatérési értéket erre a célra. Ha több értéket kell visszaadni, használjunk struktúrát.

Null átadása

A természete hasonló a bool átadásához. Két dolgot fog a függvény csinálni. Az egyiket a null esetre, a másikat pedig a nem null esetre. Ehelyett inkább két függvény kéne. A nem nullos verzió, és egy olyan ami nem is használja a paramétert.

Ráadásul defenzív programozásra sarkall. A kódunkban rengeteg trycatch és null ellenőrzésre lesz szükség.

Megjegyzés: Publikus API esetén szükséges a kódot null paraméterekre is felkészíteni.

Osztályon belüli rendezés

Hasznos, ha a kódunk, függvényeink szerkesztésénél hasonlóan járunk el, mint egy újságíró. Egy cikk felépítése általában a következő három részből áll:

  • cím
  • bevezető
  • részletek

A osztályainkat érdemes így strukturálni:

  • név
  • publikus függvények
  • privát függvények, változók

Mind a két esetben az olvasó jár jól. Addig olvassa, amíg érdekli.
Érdemes megfontolni a “lépcső szabályt” is. Ez azt mondja ki, hogy a függvényhívások iránya mindig lefele kell mutasson, vagyis a kódban egy lentebb lévő függvény ne hívjon olyat, ami felette van.

Megjegyzés: Java-ban a konvenció az, hogy az adattagok egy osztály kódjában a legfelső elemek. Ettől a szokástól való eltérés zavaró lehet.

Mi a baj a switch-el?

Ha A modul használja B-t, akkor, ha B-n változtatunk A-t is újra kell fordítani (telepíteni). Ez azért van, mert a forráskód függési iránya megegyezik a vezérlés irányával. A switch-el pont ez a probléma. Gyakorlatilag minden ágától függésben van. Ráadásul ha előfordulnak, nagyon sokszor ugyanaz a blokk többször is szerepelhet, ez pedig kódismétlést jelent.
Ezért a switch nem mondható OO megoldásnak. (Hasonló a helyzet többszörös elágazásokkal is.) Mégis mit lehet tenni?

  • Tegyük olyan helyre, ahol nem zavaró, például egy gyárba (Factory).
  • Használjunk polimorfizmust helyette, például így:
class Bird {
  //...
  double getSpeed() {
    switch (type) {
      case EUROPEAN:
        return getBaseSpeed();
      case AFRICAN:
        return getBaseSpeed() - getLoadFactor() * numberOfCoconuts;
      case NORWEGIAN_BLUE:
        return (isNailed) ? 0 : getBaseSpeed(voltage);
    }
    throw new RuntimeException("Should be unreachable");
  }
}

Polimorfizmust használva így néz ki.

abstract class Bird {
  //...
  abstract double getSpeed();
}

class European extends Bird {
  double getSpeed() {
    return getBaseSpeed();
  }
}
class African extends Bird {
  double getSpeed() {
    return getBaseSpeed() - getLoadFactor() * numberOfCoconuts;
  }
}
class NorwegianBlue extends Bird {
  double getSpeed() {
    return (isNailed) ? 0 : getBaseSpeed(voltage);
  }
}

// Valahol a kliens kódjában...
speed = bird.getSpeed();

Ez a megoldás azért is jó, mert innentől kezdve a különböző “madarakat” külön lehet fordítani, telepíteni és nem utolsó sorban fejleszteni. A kliens kódja sincs függésben, csak az őstől, így őt sem kell újrafordítanunk. Így hozható létre plug-in architektúra.

Értékadás problémája

Nézzük a következő kódot. Mit fog kiírni?

int r1 = a.getResult();
int r2 = a.getResult();
if(r1 == r2)
    System.out.println("1");
else
    System.out.println("2");

Szeretnénk hinni, hogy mindenképpen “1”-et. De nem lehetünk biztosak. Ha például a getResult() függvény változtat valamit a program állapotán, előfordulhat, hogy “2”-t fog kiírni a program. Egy nem túl értelmes, de könnyen érthető implementáció ami más eredményt ad, a következő:

public int getResult()
{
    return ++count; // tagváltozó.
}

A probléma az értékadásból és az abból fakadó mellékhatásokból adódik. (Funkcionális nyelveknél ilyen probléma nincsen.)
Mégis kimondhatjuk, hogy szívesebben dolgoznánk olyan kódbázisban, ahol a két hívás ugyanazzal tér vissza.

Mellékhatások

Gyakran párban találhatók ilyen függvények. Például:

  • getset
  • openclose
  • newdelete

Ezekben az is a veszélyes, hogy helyes sorrendben kell őket meghívni. Ez az ún. időbeli csatolás (temporal coupling). A fenti példák még könnyen érthetőek, de vannak nehezebb esetek is. A problémára megoldás lehet bizonyos esetekben a blokk átadása, vagy a parancs (Command) minta alkalmazása. Ilyenkor azoknak a hívásoknak a sorrendjét rögzíteni tudjuk, ahol ez a sorrend kritikus. Egyszerű példa az alábbi:

public void open(File f, FileCommand c){
    f.open();
    c.process(f);
    f.close();
}

open(myFile, new FileCommand() {
    public process(File f) {
        //... Fájl feldolgozása itt.
    }
});

Parancs és lekérdezés szétválasztása (Command Query Separation (CQS))

Ennek a szabálynak a követése leveheti a terhelés egy részét az agyunkról. Röviden így szól:

  • Azok a függvények, amelyek állapotot változtatnak, ne adjanak vissza semmit.
  • Azok a függvények, amelyek visszatérnek valamivel, ne változtassanak a rendszer állapotán.
void setName(string name); // van mellékhatás
int getResult(); // nincs mellékhatás

Egy általános példa a fenti szabályok megszegésére a következő:

User u = authorizer.login(username, password);

Miért kaptuk vissza a felhasználót?

  • Valamikor nekünk kell logout-ot hívni?
  • Vagy ellenőriznünk kell, hogy sikerült-e a bejelentkezés?

Inkább dobjunk kivételt! A kód így sokkal egyértelműbb:

User u = authorizer.getUser(username);

Utasíts, ne kérdezgess! (Tell don’t ask!)

Objektumainknak parancsot adjunk! Ha olyat látunk, hogy egy objektum állapotára kérdezünk, majd ennek függvényében parancsot adunk neki, az valószínűleg hiba. Az objektum ismeri a saját állapotát és meg tudja hozni a szükséges döntést, köszöni szépen.

Demeter törvénye

Nem jó, ha egy függvény a rendszer egészét vagy legalábbis nagy részét ismeri.

o.getX().getY().getZ().doSomething();

Ezzel csak az egyik probléma az, hogy a programozónak tudnia kell, hogy “o”-nak van “x”-e, “x”-nek van “y”-ja, “y”-nak “z”-je és arra hívható a doSomething() függvény. A másik gond, hogy ha például “y”-on változtatunk, az hatással lehet erre kódrészre is. Azon objektum függvényeit hívhatjuk meg, melyeket:

  • paraméterül kapunk
  • lokálisan hoztuk létre
  • példányváltozók
  • globálisok

Viszont másik függvény visszatérési értékeként kapott objektumokra nem tanácsos függvényt hívni. Persze itt is kivétel erősíti a szabályt

break & korai return

Összetett függvények megértését nagyon nehézkessé tehetik, hiszen sértik a strukturált programozás elvét, miszerint egy blokknak egy be- és egy kimenete lehet. Ilyen esetben felértékelődik a rövid függvények szabálya.
Ugyanakkor meg kell jegyezni, hogy nagyon jó szolgálatot tehetnek, ha nehezen megfejthető beágyazott elágazásokat szeretnénk tisztogatni. Például:

void someFunction()
{
	boolean test1 = ...;
	if (test1) {
		boolean test2 = ...;
		if(test2) {
			boolean test3 = ...;
			if(test3) {
				// csináld!
			} else {
				// valami hiba
			}
		} else {
			// valami más
		}
	} else {
		// megint más
	}
}

void someFunction()
{
	boolean test1 = ...;
	if(!test1) {
		// megint más
		return;
	}
	boolean test2 = ...;
	if(!test2) {
		// valami más
		return;
	}
	boolean test3 = ...;
	if(!test3) {
		// valami hiba
		return;
	}
	// csináld!
}

Források

Flag argumentumok
Switch és polimorfizmus
Beágyazott elágazások refaktorálása
Clean Code — A Handbook of Agile Software Craftsmanship — Martin, Robert C. Prentice Hall, 2009
Clean Code – Function Structure

Tiszta kód – 3.rész: függvénystruktúra

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