Command and Query Separation

Mostanában sokat foglalkozom azzal, hogyan tudnék jobb (értsd: tisztább) kódot írni. Korábbról is ismertem már a Command and Query Separation technikát, de igazából a Clean Code c. könyv hívta fel rá a figyelmemet.

Miről szól?

Az elv több évtizedes, a lényege nagyon egyszerű. Egy függvény (metódus) vagy egy számított értéket adjon vissza – és közben ne változtassa meg a résztvevő objektumok állapotát –, vagy idézzen elő állapotváltozást az objektumokon, ekkor viszont ne legyen visszatérési értéke (azaz void legyen). Azaz egy függvény vagy query (lekérdezés) legyen vagy command (parancs).

A fenti definíció nem pontos, de jól érzékelteti a lényeget: egy nem void visszatérési értékkel rendelkező függvénynek nem szabad mellékhatást okoznia.

Minek?

A kódolvashatóság javítása végett.

public void do(Set<Long> map) {
    // …
    if(map.remove(x)) {
        // do something
    } else {
        // do something else
    }
}

A fenti kód helyett inkább ezt írjuk:

public void doBetter(Set<Long> map) {
    // …
    if(map.contains(x)) {
        map.remove(x);
        // do something
    } else {
        // do something else
    }
}

A példa mutatja, hogy olvashatóbb kódot kapunk, ha először megvizsgáljuk, ténylegesen benne van-e az x elem a map-ben. Ha valaki nem ismeri a remove működését, találgatni fog, vagy meg kell néznie az API leírását. Ha a remove saját kód lenne, akkor valószínűleg bele is kell néznie a kliens kód írójának, hogy mit valósít meg a függvény. (Ilyenkor vagyunk döntési helyzetben, hogy a remove metódusnak ne lehessen visszatérési értéke.)

Ennél rosszabb a helyzet a következő esetben (a példa a Clean Code c. könyvből származik):

public boolean set(String attribute, String value) {
    //…
}
if(set("username", "unclebob")) …

A példában nem lehet tudni, hogy a set egy lekérdezés, amely ellenőrzi egy attribútum értéket, vagy pedig ténylegesen meg is változtatja azt, és a visszatérési érték valójában a művelet sikerességéről tájékoztat (vagy másról). (A set szónak két jelentése is van.)

Hibakezelés

Régi procedurális, alacsonyszintű kódon nevelkedők azt gondolhatják, hogy jó az, ha a metódusok visszatérési értékkel jelzik a művelet sikerességét (error code). (A sikerességet jelezni lehet, pl. egy boolean flag-gel (true = sikeres, false = sikertelen), egy enum-mal, egy speciális Parameter Object-tel, esetleg egy int-tel.)

Ez ellentmond a CQS-nek, hiszen a függvény vissza is ad értékét (sikeres volt-e a parancs), és végre is hajtja a parancsot.

Ugyan van létjogosultsága ennek a programozási módszernek is (alacsonyszintű programozás, többszálú programozás), azonban ennek használata normál alkalmazásfejlesztés esetén káros, ugyanis – az állandóan hibaellenőrzés miatt – bonyolult kódot eredményez a hívó oldalon.

if(doSomething()) {
    if(doSomethingElse()) {
        if(andNowDoThis()) {
        } else {
            // handle the error
        }
    } else {
        // handle the error
    }
} else {
    // handle the error
}

A megoldás – természetesen – a kivételek használata. A command-ot megvalósító metódus void-ot ad vissza, és kivételt dob, ha bármi hiba történik. Ezzel kiküszöbölhető a látszólagos ellentmondás a CQS mintában.

try {
    doSomething();
    doSomethingElse();
    andNowDoThis();
} catch(…) {
    // handle the error
}

Vannak kivételek

A CQS alkalmazása alól is léteznek kivételek. Tipikusan az egy időben több kliens által is használt, megosztott objektumok esetén nem lehet az elvet erőltetni, ugyanis egy command futtatása után egy query eredménye megváltozhat, ha menet közben más is meghívja a command-ot.

Számos példa létezik API-kban is, ahol nem követik az elvet: pl. Stack.pop(), Iterator.next(). Ennek ellenére úgy gondolom, érdemes ezt az elvet magunkévá tenni, és – ahol tudjuk – következetesen alkalmazni.

Share
This entry was posted in Programozás and tagged , , , , . Bookmark the permalink. Follow any comments here with the RSS feed for this post. Trackbacks are closed, but you can post a comment.

Comments

  • A függvény az függvény – kiszámol valamit.
    Az eljárás az eljárás – csinál valamit.

    A Pascal irányon kifejlődött nyelvekben, külön kulcsszavak vannak arra, hogy megkülönböztessék a “metódusokat” viselkedésük szerint – FUNCTION, PROCEDURE. Ezekben a nyelvekben alapértelmezetten nem lehet a bemenő paraméterek értékét változtatni. Például Ada-ban meg kell mondani, hogy a változó in-out típusú, ekkor lehet csak módosítani a metódus törzsén belül, hogy az maradandó is legyen.

    A C szintaktikára épülő nyelvekben {} a kapcsos zárójelekkel nehéz ezt kifejezni, ezért kell betartani azt, amit fent leírtál.

  • Marhefka István

    Jun 9th, 2010

    Én úgy gondolom, Pascalban is van lehetőség arra, hogy paraméterként egy objektumot adunk át (egy pointert), és így az átadott objektumot meg tudjuk változtatni.

    Nem értettem, mire gondoltál a kapcsos zárójelekkel kapcsolatban.

  • Nem mondják meg nyelvi szinten, hogy milyen jellegű a metódus, amit körbe zárnak. Kapcsos zárójeles környezetben a metódus nevével és a visszatérési értékkel tudsz információt megadni a metódusról.

    Az Ada-ban lehetőség van alprogramként függvények és eljárások létrehozására is. Alprogram hívásakor a formális és aktuális paraméterek összerendelése történhet pozíció, név vagy mindkettő szerint. A név szerinti hozzárendelés formája:
    fomális_paraméter => aktuális_paraméter
    Az Ada háromféle paraméterátadási módot ismer:

    1. Érték szerinti (in): ez a default mód. Függvénynek csak ilyen paramétere lehet. A formális paraméter az alprogram törzsére nézve konstans (csak olvasható). Aktuális paraméter ilyenkor tetszőleges kifejezés lehet.
    2. Eredmény szerinti (out): Ilyenkor a formális paraméter az alprogramon belül csak írható. Az aktuális paraméter csak balérték lehet, értéke a visszatéréskor felveszi a formális paraméter értékét.
    3. Érték – eredmény szerinti (in out): Az aktuális paraméterről a híváskor másolat készül, amelynek értéke az alprogram futása alatt független az aktuális paraméter értékétől. Visszatéréskor azonban az felveszi a formális paraméter értékét. Az alprogramban a formális paraméter írható olvasható. Aktuális paraméter ilkyenkor is csak balérték lehet.

    És ahogy Pascal-ban, Ada-ban is lehet pointereket átadni paraméterként, de ha olvasható kódot akar írni az ember, szerintem kerüli őket. Ha a hatékonyság a szempont, akkor lehet, hogy pointerekkel fog játszani.

  • Marhefka István

    Jun 9th, 2010

    Köszi az ADA továbbképzést :)

    Mellesleg az ADA-ban jellemző out-os paraméterátadást is kerülni érdemes (C#-ban is van ilyen), mert nem áttekinthető tőle a kód. A matematikai függvények jelölési módja miatt minden paramétert az ember intuitíven bemenő paraméternek vár.

    Igen, a Pascal addig szép, amíg nincsenek benne pointerek :) De sajnos ha dinamikus méretű adatszerkezeteket akarsz, akkor nem tudod őket kikerülni :)

  • Ezt még leírom, nehogy bennem maradjon.

    ADA vs C
    Az egyetemen Porkoláb Zoli azt mondta, hogy az egyiket egy összehívott bizottság alkotta meg, a másikat meg két zseni. Ebből ki is jönnek dolgok, pl. hogy C, C++, de akár a Java is, ami egy “leegyszerűsített” C++ (volt) sokkal rugalmasabbak, mint a Pascal, ADA, Delphi.

    Az ADA merev, mint az állat, de cserébe nagyon jók a hibaüzenetei fordítási időben. Sokkal kisebb a valószínűsége a futási idejű hibáknak. Én egyfolytában azt éreztem, mintha életrajzot írnék, amikor ADA-ban kellett kódolni, mert sokkal hosszabbak a kulcsszavak és mindent BEGIN-END-ezni kell. Végén már nem láttam a kulcsszavaktól a kódot.

    Számomra a C, C++, Java kód valahogy lényegre törőbb, mint az Ada, Pascal kód és társaik. :D

Leave a Comment