2011. augusztus 27., szombat

Validálás, ahogy én csinálom

Egy programozó életében rendkívül gyakran előfordul, hogy entitásokat kell validálnia. Lényegében az üzleti logika mindig úgy kezdődik, hogy a bemeneti paramétereket, melyek legtöbbször felhasználói interakció által kerülnek be, validálni kell, hogy a hibás entitások a későbbiekben ne okozzanak hibát a program működésébe. A validálás az a dolog, amit egy kicsit mindenki másként csinál, mindenkinek megvan a saját módszere. Ebben az írásban bemutatnám, én hogyan is szoktam ezt a problémát orvosolni. A bemutatott megoldás nem tökéletes, de remélem gondolat-ébresztőnek megteszi.

A validálás alapproblémája szerintem a következő: az entitások validitását az üzleti réteg végzi el, amelyhez különböző kliensek kapcsolódnak. A különböző kliensek különböző nyelveken, és módon kívánják megjeleníteni a hibaüzeneteket, tehát az üzleti rétegbe írt return "Hibás a név!" és hasonlók megengedhetetlenek (később úgyis kiderül, hogy többnyelvű lesz a megjelenítés). A következő probléma, amivel szembesülünk, hogy egyszerre több hibát is tudni kell kezelni, hiszen egy bonyolult form kitöltésekor nem szerencsés egyesével untatni a felhasználót a hibák közlésével, meg egyébként sem tudjuk klienseink miként működnek. A nyelvi problémát orvosolni viszonylag egyszerű, vezessünk be jól dokumentált hibakódokat, klienseink, meg feldolgozzák saját belátásuk szerint. Az egyidejű több hibajelzés kérdésköre már egy kicsit rafináltabb dolog. A legegyszerűbb megoldás, egy hibakódokkal feltöltött tömb, vagy Collection visszaadása. Ezzel a megoldással a legnagyobb problémám, hogy rengeteg favágó programozással jár. Ha mindenhová, mind kliensben mind az üzleti rétegben kézzel írogatjuk a hibakódokat, akkor a hibaszázalékunk igencsak megnő, mert óhatatlanul elgépeljük valahol a kódot. Ha bevezetünk konstansokat, vagy Enumokat, akkor legalább a hibákat kiküszöböljük, de a ThisIsMyEntity.ERROR_NAME_REQ gépelésébe fogunk megőszülni.

Nekem a következő megoldás vált be: mivel 2db kettő hatványa sosem lesz egy harmadik kettő hatványával egyenlő, a hibakódot adjuk vissza kettő hatványaival. Tehát, ha hiányzik a név adjunk vissza 2-t, amennyiben a lakcím hiányzik adjunk vissza 4-et, ha pedig mindkettő hiányzik adjunk vissza 6-ot. A metódus implementációját személy szerint magába az Entitást megvalósító osztályba szoktam tenni, de ez ízlés dolga. Van aki szerint az Entitásnak csak egy DTO-nak kell lennie, és a validitás eldöntése az üzleti logika feladata. Véleményem szerint egy entitás tudja leginkább magáról eldönteni, hogy megfelelőek-e a benne foglalt adatok.

public int validate() {
            int valid = 0;
            
            if (foo == null || foo.isEmpty()) valid |= 1 << 1;
            if (bar == null) valid |= 1 << 2;
            if (param == null || param.isEmpty()) valid |= 1 << 3;
            
            return valid;
        }
Mivel későbbiekben is bitműveletekkel fogunk operálni, elegánsabbnak tartottam bitműveletekkel megoldani az összeadást, természetesen dolgozhatunk "hagyományos" számokkal is.

Kliens oldalon a hibakód lekezelése pofon egyszerű. Kiválasztunk egy tetszőleges 2 hatványt (érdemes a dokumentációban is említettet választani :)), mondjuk a 4-et, és ellenőrizzük, hogy adott bit, 2^2 az 100, tehát a harmadik, egyezik-e a visszakapott érték harmadik bitjével.

boolean somethingIsValid = (4 & entity.validate()) == 4;
Ez a módszer sem tökéletes, hiszen a hibákat csak bővíteni lehet, egyéb változtatás esetén már meglévő klienseink fognak hibásan működni. A teljes példaprogram az alábbi:
public class ValidatorTest {

    private static final Map<Integer, String> ERRORS = new HashMap<Integer, String>();

    static {
        ERRORS.put(2, "Foo is required!");
        ERRORS.put(4, "Bar is required!");
        ERRORS.put(8, "Param is required!");
    }

    public static void main(String[] args) {
        List<Entity> entities = Arrays.asList(new Entity[] {
            new Entity(null, null, null),
            new Entity("foo", null, null),
            new Entity("", null, "param")
        });

        for(Entity entity : entities) {
            System.out.println(entity);
            int valid = entity.validate();
            
            for(Integer code : ERRORS.keySet()) {
                if ((code & valid) == code) {
                    System.out.println(ERRORS.get(code));
                }
            }
        }
    }

    static class Entity {

        private String foo;
        private Integer bar;
        private String param;
        
        public Entity(String foo, Integer bar, String param) {
            this.foo = foo;
            this.bar = bar;
            this.param = param;
        }

        public int validate() {
            int valid = 0;
            
            if (foo == null || foo.isEmpty()) valid |= 1 << 1;
            if (bar == null) valid |= 1 << 2;
            if (param == null || param.isEmpty()) valid |= 1 << 3;
            
            return valid;
        }
    }
}

Nagyon kíváncsi vagyok a véleményetekre, illetve, hogy nektek milyen módszer vált be ebben a témakörbe.

3 megjegyzés:

  1. Spring Validation (http://static.springsource.org/spring/docs/3.0.x/javadoc-api/org/springframework/validation/package-frame.html). Én jobban szeretem az entity-ktől külön létező validator-okat, mert sokszor belefutottam abba, hogy egy entity-t különböző képernyőkön különbözőképpen kell validálni, valahol létrejön, valahol csak ezt módosítják, valahol csak azt, másképp kell ellenőrizni szerepkörtől függően, stb. Ezért nem tetszik nekem pl. a JSR 303: Bean Validation sem, meg az még annotációkkal is meg van kavarva. Lehet, hogy ez most hitszegés, de nekem a validáció valahogy UI közeli dolog.

    Valamint a szöveges hibakódok tényleg elgépelhetők, viszont debug esetén könnyebben olvashatóak.

    Valamint a FieldError osztály pl. tartalmaz paraméter listát is, így a hibaüzenetet megfelelőképpen paraméterezni lehet a szokásos {0} szintaxissal.

    Egy időben játszottunk a Spring Modules-ban lévő Valang-gal is, mely azt ígérte, hogy majd JavaScript-et is tud majd gyártani, de valahogy visszatértünk arra, hogy az ellenőrzéseket is Java-ban implementáljuk, ne egy speciális kis nyelvben.

    VálaszTörlés
  2. "a validáció valahogy UI közeli dolog" Ez a megjegyzés jó táptalaj egy kis eszmecseréhez :) Szerintem első szinten UI közelien is el kell végezni a validációt, ahol az adott felhasználó a saját jogosultságának, entity állapotának, stb. megfelelően beviszi az adatokat. Ez a validáció semmiképp sem helyettesítheti az üzleti rétegben történő ellenőrzést, hiszen nem bízhatunk a kliensekben. Ha az entitásnak több fázisa, életciklusa van, akkor az ellenőrzést fázisonként külön-külön meg kell valósítani.

    VálaszTörlés
  3. Nyilván, viszont a validációt a webservicek esetében is el kell végezni, söt, a leginkább ott, mert ott van lehetőség arra, hogy valami nagyon gonosz szivárogjon be a csendes kis magányunkba. Az pedig igen kellemetlen dolog.

    Én a hibaüzeneteket általában valamilyen frameworktöl kérem el, ami rögtön lokalizáltan adja vissza. Ez mondjuk akkor kellemetlen, ha a hívó API hívást használt, de lusta volt a HTTP headereket belőni a megfelelő nyelvre - de hát az meg vessen magára árpát. Azért az Accept-Language korrekt kezelése ma már minden keretrendszernél elvárható dolog szerintem.

    VálaszTörlés