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ó.)
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.
3 megjegyzés:
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 :))
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));
:)
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. :)
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