Az infokukac blog 2010. márciusában átköltözött. Átirányítás folyamatban...

Az átirányítás automatikus. Ha mégsem sikerülne, látogasd meg az új oldalt a http://infokukac.com címen, és frissítsd a könyvjelzőidet!

2009. július 8., szerda

Tömbök, mint value-objectek

A programozási nyelvek legtöbbje lehetőséget biztosít függvények (metódusok, procedurák) alkalmazására. A függvények hívásakor paramétereket adunk át. Ezen átadások tipikusan kétféle: referencia szerinti (vagy cím szerinti, by reference), ill. érték szerinti átadás (by value). (Vannak más fajták is.)

Az érték szerinti átadás

Ez az egyszerűbben megérhető. Vegyünk egy Java vagy C# metódust, amely paraméterként egy int-et vár:

    void f(int x) {
        // ...
        x = 10;
        // ...
    }

// ...később egy másik metódus belsejében
    int z = 8;
    f(z);
    f(5);

Az f(z) hívás közben a z értéke lemásolódik, és amíg az f függvény végrehajtása tart, az x változóval erre a másolatra (lokálisan) hivatkozunk. Ha az x értékét megváltoztatjuk a függvény belsejében (ami amúgy refaktorálási szempontból nem egy jó szokás), akkor a másolatot módosítjuk, ennek közvetlenül nincs hatása az eredeti z változóban lévő értékre.

Referencia vagy cím szerinti átadás

A következő kód C++-ban írodott:

    void f(int &x) {
        // ...
        x = 10;
        // ...
    }

// ...később egy másik metódus belsejében
    int z = 8;
    f(z);
    f(5);                // fordítási hiba

        

Itt az f(z) híváskor a z változót tároló memóriarekesz (kezdő)címe adódik át. Ennek az a következménye, hogy a függvényen belüli x-re irányuló változtatás közvetlenül megváltoztatja az eredeti változó értékét is. Az f(5) hívás egyenesen fordítási hibát eredményez, mert szemantikailag helytelen. (Hogyan változtatható meg az 5 konstans értéke? Kell változó.)

Na, ez most melyik?

A következő kód Java-ban íródott:

    void buy(Customer customer) {
        customer.addOrder(this);
        // ...
        customer = null;
        // ...
    }

// ...később ugyanebben az osztályban egy másik metódus belsejében
    Customer c = ...;
    buy(c);

Tudjuk, hogy a buy metódus végrehajtása közben ugyanazzal az objektummal dolgozunk, mint ami a híváskor jelen van, de a metódusban a customer változó null-ra állítása nem befolyásolja a c értékét. Úgy tűnik, mintha kettőséggel szembesülnénk: bár ugyanazon az objektumon dolgozunk, és ez referencia szerinti átadást sejtet, viszont ha magát a customer változó értékét változtatom meg, az eredeti c változó nem változik. Ez utóbbi viszont érték szerinti átadást mutat.

Valójában Java-ban minden érték szerint adódik át. A c ill. customer változók referenciák (mintha C-beli mutatók lennének), ezen referenciák értéke másolódik le függvényhíváskor. Így a függvényhívás belsejében a referencia változik meg (már máshová mutat).

Value object-ek

Láthatjuk, hogy Java-ban objektumra történő hivatkozást (referenciát adunk át). A primitív típusokon (pl. int, long) kívül mindent referencián keresztül érünk el, azonban bizonyos esetekben célszerű a referenciákon keresztül elért objektumokra ún. "value object"-ként gondolni. Value object-re tipikus példa a dátum, ill. sztring, vagy a komplex számok és a pénzmennyiséget reprezentáló struktúrák. Ezekre általában úgy tekintünk, mint a primitív típusokra, műveleteket is hasonlóképpen végzünk velük. (A C#-ban a string primitív tipus, Java-ban az osztályt ellátták a final módosítószóval, amelynek eredményeképpen az eredeti String objektumon nem tudunk változtatást végezni, egy konkatenáció pl. mindig egy új String-et ad vissza.)

A tömbök, mint value object-ek

Képzeljük el, hogy van egy Customer osztályunk. Ebben az osztályban deklaráltunk egy addresses mezőt, amely az ügyfél címeit reprezentálja.

    public class Customer {
        private Set<Address> addresses;

        public Set<Address> getAddresses() {
            return addresses;
        }
    }

Ezzel a kóddal kapcsolatban a legnagyobb gond az, hogy ha valaki elkéri egy ügyfél címeit, akkor a visszakapott Set<Address> referencián keresztül közvetlenül módosíthatja azokat (magát a halmazt). Ezt OO szempontból "nem szokták szeretni", mivel az objektumnak kell megvalósítania az egységbezárást, az objektumon történő megfelelő metódushívásoknak kell elvégezniük a módosítást (ez így olyan mintha a mezőt public-ká tennénk.) Miért akarná valaki módosítani a visszakapott referencián keresztül a halmazt?

  • véletlenül: előfordult már, 
  • szándékosan: az ilyen emberektől védeni akarjuk a kódot.

Én minden ilyen esetben, amikor egy halmaz, lista stb. -szerű objektumot adok vissza, lemásolom. Tehát a getter-jeim így néznek ki:

        public Set<Address> getAddresses() {
            return new HashSet<Address>(addresses);
        }

Ennek következménye, hogy aki megkapja az új halmazt, vígan módosíthat benne, és nem cseszi el - se szándékosan, se véletlenül - az eredeti halmazt. Ennek a módszernek annyi a hátulütője, hogy lassít az alkalmazáson, és bizonyos típusú szoftvereknél ez tényleg problémát jelenthet. (Meggyőződésem viszont, hogy ORM-környezetben vagy más "enterprise-környezetben" ha egy halmaznak eleve sok eleme van, és Java-kódból kell feldolgozást végezni rajta, az eleve lassú lesz annak lemásolása nélkül is.)

A tömb lemásolása ilyen szempontból úgy viselkedik, mintha egy value object-et adnék át. Önmagában egy kompakt egész, amelyen lekérdezési műveleteket lehet elvégezni. 

Még egy kis finomítás

Van-e annak értelme, hogy az addresses mező null-referencia? Ha nincs egy eleme sem a halmaznak, akkor az egy üres halmaz. A Set-nek ugyanúgy léteznie kell, legfejlebb üres lesz.

Valamiért utálok egy mezőnek kezdő értéket adni, inkább a konstruktorokban teszem ezt meg. Ennek ellenére a Set-hez hasonló osztályokat mindig deklarációkor inicializálom:

    public class Customer {
        private Set<Address> addresses = new HashSet<Address>();

        public Set<Address> getAddresses() {
            return new HashSet<Address>(addresses);
        }
    }

A primitív típusok esetén is van default érték, tipikusan 0. Itt halmaz esetén az üres halmaz lesz az (ha kell elemet bele pakolni, akkor a konstruktorban még mindig megtehetem). Még tovább: mi történik, ha egyszerre akarom "felülcsapni" az ügyfél összes címét?

    public class Customer {
        private Set<Address> addresses = new HashSet<Address>();

        public void updateAddresses(Set<Address> newAddresses) {
            addresses = new HashSet<Address>(newAddresses);
        }

        public Set<Address> getAddresses() {
            return new HashSet<Address>(addresses);
        }
    }

Az előző gondolatmenet alapján ennek így kell lennie. Ha addresses = newAddresses-t írnék, akkor ugyanaz a probléma állna fenn, mint a getter-eknél. (Ha valaki kívül módosítja utólag a halmazt, az módosul a Customer-ön belül is.)

Collections.unmodfiableSet - még egy kis adalék

Pár hónapja leltem rá a Java SDK-ban a Collections osztály statikus unmodifiableSet (...List stb.) metódusára. A metódus paramétere egy halmaz, visszatérésként egy másik halmazt kapunk vissza, amely nem módosítható. (Ha módosítanánk rajta, akkor UnsupportedOperationException-t kapunk.) Ez nagyon hasonló a mi "kézi" halmazlemásolásunkhoz, de valójában csak egy view-t kapunk vissza. Ha valaki az eredeti halmazt módosítja, az a view-ban is megváltozik.






Címkék:

3 megjegyzés:

Blogger Kristof Jozsa írta...

Collections.unmodifiableSet() a szép megoldás. az inline inicializálással meg semmi gond nincs (azt legalább nem lehet megkerülni ha valaki fél perc alatt rittyent az osztályra egy alternatív konstruktort :))

2009. július 9. 8:46  
Blogger Marhefka, István írta...

A Collections.unmodifiableSet() tök jó, mert a kliens azonnal elszáll, ha megkísérli változtatni a halmazt. Viszont ha az unmodifiableSetet visszaadó kód utólag megváltoztatja a halmaz tartalmát, akkor a kliensnél az unmodifiableSet is "megváltozik" (mivel belül az eredeti halmazra hivatkozik). Mit szólsz egy ilyenhez?:

return Util.unmodifiableSet(new HashSet<Address>(addresses));

:)

2009. július 9. 11:03  
Blogger István írta...

Szia! Nagyon tetszik a blogod, ritka jó, most találtam, és azonnal el is olvastam az összes bejegyzést!

Ehhez a bejegyzéshez szeretnék annyit írni, hogy a címben is tömb szerepel, de valójában a Collection API osztályait használod, persze ezeken belül tényleg tömbök vannak.

Valamint írod, hogy "(A C#-ban a string primitív típus, Java-ban az osztályt ellátták a final módosítószóval, amelynek eredményeképpen az eredeti String objektumon nem tudunk változtatást végezni, egy konkatenáció pl. mindig egy új String-et ad vissza.)"

Ez nem teljesen így van, vagy rosszul értelmezem, amit írsz. A String tényleg final, azért hogy ne lehessen az osztályból leszármaztatni, azaz egy MyString osztályt létrehozva, ami felülírja a konkatenációt, és a polimorfizmus miatt bárhol használhatnánk String helyett. De amit te írsz, hogy a String objektum értéke nem változtatható meg (tudományosan immutable), az nem abból adódik, hogy a String osztály final, hanem abból, hogy a metódusok nem változtatják a String értékét tároló char value[] tömböt, és nem is tudnák, mert az is final. :)

A többi post tökéletes. :)

2009. december 8. 18:54  

Megjegyzés küldése

Megjegyzés: Megjegyzéseket csak a blog tagjai írhatnak a blogba.

Feliratkozás Megjegyzések küldése [Atom]

<< Főoldal