ugrás a tartalomhoz

Globális változók és osztályok

Hidvégi Gábor · 2014. Már. 8. (Szo), 10.09
A múltkor volt egy kérdés globális változókkal kapcsolatban, és mivel mindenki egyöntetűen azt állítja, hogy kerüljük a használatukat, utánaolvastam, milyen problémákat okozhatnak (mivel nekem sem volt rájuk nagyon szükségem eddig).

A kérdésre adott válaszban le is írtam a lényeget:
A baj az velük, hogy változók, azaz bárki átírhatja őket, így az őket használó függvények által visszaadott érték is változhat.
Tehát egy globális változót használó függvény visszatérési értéke – ugyanazokkal a paraméterekkel meghívva – az idő folyamán változik, ha a változót átírjuk, ezt angolul úgy hívják, hogy nem reentrant.

Továbbgondolva a dolgot, szöget ütött a fejembe, hogy van egy nagyon hasonló szoftvertervezési megoldás – bár jellegéből adódóan kisebb hatáskörű: az osztályok vagy objektumok belső változói. Ha egy metódus ilyen változót használ, akkor a végeredmény pontosan ugyanaz lesz, mint a fenti esetben, azaz az idő előrehaladtával más és más eredményt ad vissza a változó függvényében.

A kérdésem a következő: ezzel kinek mi a tapasztalata? Ha problémát okoz, hogyan lehet elkerülni?
 
1

Globális állapot

vbence · 2014. Már. 8. (Szo), 11.33
Nem feltétlenül az általad leírak miatt nem szeretjük a globális állapotokat (azokat a problémákat inkább a "láthatóság" területére sorolnám - public vs. private).

"Dependency injection" néven szokott futni, amiről a diskurzus mostanában szól (szörnyű név), de a "global state" kulcsszavakra is sok érdekes találat van.

Ezt a videót mindneképpen ajánlanám:
The Clean Code Talks - "Global State and Singletons"

Röviden: A probléma a globális adatokkal (jobban mondva állapottal, legyen az akár egy statikus adattag, vagy egy singletonban található adat) az, hogy lehetetlenné teszik egy-egy rész izolálását (például tesztelés céljából) - vagyis beláthatatlanok az adott kódrészlet függőségei.

Például itt egy függvény, ami az adott user preferenciáinak megfelelően küld egy e-mail kivonatot (mondjuk milyen új cikkek jelentek meg az oldalon a user kedvenc kategóriáiban):
hirlevel_kuldes ()
A függvénynek azonban van egy ki nem mondott függősége, a user azonosítóját a sessionből (globális állapot) olvassa ki. - Vagyis csak ott és akkor lesz működőképes ahova eredetileg tervezték. Más körülmények között obskúrus hibákat eredményez.

Ezzel szemben a következő megoldás...
hirlevel_kuldes (User $user)
egyértelművé teszi a kód olvasója / későbbi fenntartója számára a függőséget (előfeltételeket).

Egy életszagúbb példa, hogy még a második változatnak is van egy húsbavágó függősége, mégpedig a mail() függvénytől (rosszabb esetben egy szerteágazó mail frameworktől). Nyilvánvaló, hogy ebben a formában nem tesztelhető a kód, mert valódi e-mailek repkednének mindenfele. Még egy kicsit fejlesztve az állatorvosi lovon:
hirlevel_kuldes (User $user, Mailer $mailer)
Ebben a változatban a tesztek során átadhatunk egy teszt mailert, amiben akár meg is vizsgálhatjuk a függvény által küldött üzenetek tartalmát.
2

Arra szerettem volna

Hidvégi Gábor · 2014. Már. 8. (Szo), 13.23
Arra szerettem volna rámutatni, hogy számomra úgy tűnik, az osztályok belső változói hasonlóan működnek a globális változókhoz/singletonokhoz.
3

???

Poetro · 2014. Már. 8. (Szo), 15.16
Miért pont az osztályok belső változói? Miért nem pont a tömbök? Nem értem az összefüggést. A globális változókkal az a probléma, hogy globálisak, és ezért egyetlen állapotot tudnak tárolni. Viszont ahány objektum, annyi állapota lehet. Ennek megfelelően érdemes is kezelni. A singleton-oknak is a legjobb, véges számú állapota van, és ez az állapot egyértelműen lekérdezhető, illetve csak saját maga tudja változtatni az állapotát.
6

setter

zzrek · 2014. Már. 8. (Szo), 22.28
Nem az a különbség, hogy azokat illik setterrel változtatni, így előre definiálható/ellenőrizhető a függőségi probléma? Annyi az egész, hogy az ilyeneket nem függvényparaméterként adjuk át, hanem külön, setterrel. Ez viszont lényeges különbség a globálishoz képest, amit mindenféle korlátozás és látszólagos következmény nélkül meg lehet változtatni, és nincs tipikus helye a változtatás ellenőrzésének, lekövetésének.
4

Ebben a változatban a tesztek

Greg · 2014. Már. 8. (Szo), 17.05
Ebben a változatban a tesztek során átadhatunk egy teszt mailert, amiben akár meg is vizsgálhatjuk a függvény által küldött üzenetek tartalmát.

Ezert szeretem a ruby-t, mert ott a "mail" fuggvenyt ebben az esetben csak a tesztedben felul tudod irni.
Az idezojel oka, hogy rubyban nincs mail fuggveny.
5

runkit powa

complex857 · 2014. Már. 8. (Szo), 21.59
Ha a tesztkörnyezetet saját magunk alakíthatjuk (és miért ne tehetnénk) a runkit extensionnel, php alatt is elérhetővé válik a "menet közben" függvények kicserélése (akár beépítetteket is).
Hogy mennyire szerencsés ötlet ez abban bizonytalan vagyok (ruby alatt ennek sokkal jobb a támogatottsága és nagyobb hagyományai vannak), de legacy kódbázishoz smoke testeket gyártani refactoring előtt viszont hasznos lehet.
7

Azért nem szeretjük őket,

inf · 2014. Már. 9. (V), 00.09
Azért nem szeretjük őket, mert beszennyezik a global namespace-t, és emiatt bármikor esedékes lehet egy name conflict két így megírt program között. Emiatt találták ki a modulokat js-hez.

helyesbítés:
Emiatt találták ki a data hiding-ot, ami megvédi a külső behatástól a változókat, így kontroll alatt tartva, hogy a kód melyik része fér hozzájuk. Ez jelentősen egyszerűsíti a debug-ot, szóval vannak előnyei...

A névterek, modulok, osztályok, package-ek, stb... mind kapcsolódnak valamilyen szinten a kód szervezéshez, illetve ehhez a témakörhöz. Nyilván vannak nyelv függő különbségek ilyen téren...
8

Két nagyon különböző

tgr · 2014. Már. 9. (V), 10.20
Két nagyon különböző problémát keversz össze. Az egyik, hogy a globális változók használata szorosan csatolt kódot eredményez: ha két függvény ugyanazt a globalt használja, akkor hatással vannak egymás viselkedésére, csatolás (coupling) van közöttük. Ez azt jelenti, hogy ha egy globalt használó függvény viselkedését módosítod, és tudni akarod, hogy ennek a módosításnak pontosan milyen hatása lesz a program viselkedésére, akkor az egész kódbázist át kell rágnod elejétől a végéig, hogy használja-e valami más azt a változót (és nyilván használja, különben minek lenne global); ami használja, annak megváltozik a viselkedése, amivel megváltoztathatja más globalok értékét; goto 1. Ha nagyobb kódbázisról van szó, jónéhány álmatlan éjszakát fogsz eltölteni azzal, hogy egy greppel felfegyverkezve próbálod feltérképezni a függvényeid közötti függőségeket (aztán néhány továbbit akkor, amikor élesben beborul a kód, mert valaki valahol nem greppelhető módon használta azt a változót).

Egy osztály változójáról ezzel szemben pontosan tudod, hogy a kód melyik része fér hozzá; az osztálystruktúra explicitté teszi a függőségeket, a legtöbb módosításról viszonylag könnyű ránézésre megállapítani, hogy helyes.

Ettől teljesen eltérő kérdés, hogy van-e állapota egy függvénynek/objektumnak. Általában igaz, hogy minél kevesebbnek van, annál jobb (könnyebb érvelni a kód helyességéről), OOP-ben ezt tipikusan úgy oldod meg, hogy szétbontod a kódot value objectekre, amikben csak állapot van, logika nincs, és service-ekre, amikben csak logika van, állapot nincs. Ennél extrémebb megközelítés a funkcionális paradigma, ahol egyáltalán nincsenek állapotok, ami nagyon elegáns, de kevésbé illeszkedik a valóságra (ha felhasználói interakciót kell kezelni, vagy I/O-t, akkor ott ugye az állapotosság külső adottság), meg a processzor működésére (aminek a középpontjában a regiszterek állapotának az állítgatása áll).
9

OOP-ben ezt tipikusan úgy

Hidvégi Gábor · 2014. Már. 9. (V), 19.11
OOP-ben ezt tipikusan úgy oldod meg, hogy szétbontod a kódot value objectekre, amikben csak állapot van, logika nincs, és service-ekre, amikben csak logika van, állapot nincs.
Erre tudsz egyszerű (sematikus) példát mutatni?
11

ha jól emlékszem

razielanarki · 2014. Már. 10. (H), 18.21
a symfony2-es sessionstorage/parameterbag-ek valami ilyesmik

(az egyik a sessiönt kezeli db-ben/file-ban/stb, a másik pedig a sessiönben tárolt változók fölé ad egy interfészt)
15

Pl. a modern frameworkok

tgr · 2014. Már. 16. (V), 07.38
Pl. a modern frameworkok repository patternje: van egy modelled, amiben tipikusan csak buta getter/setterek vannak, és minden modell típushoz van egy repository osztály, ami az adott modellhez kötődő lekérdezéseket tartalmazza. A perzisztenciát teljes egészében a repository osztály (illetve az amögött levő további osztályok) kezeli, a modellednek semmit sem kell arról tudnia, hogy ő egy adatbázisból lett betöltve.

(Régebben az ActiveRecord minta volt a divat, amikor a modell maga kezeli a saját perzisztenciáját, ami rengeteg fajta szopást tud eredményezni, amikor ugyanazt a modellt többféleképpen akarod tárolni - a modell megsérti az SRP-t, két különöző felelőssége van (valamilyen entitásnak a reprezentálása és ennek a reprezentációnak a mentése/betöltése), és problémáid lesznek, ha ezt a két feladatot egymástól független módon kell változtatni.)
10

Ettől teljesen eltérő kérdés,

inf · 2014. Már. 10. (H), 02.55
Ettől teljesen eltérő kérdés, hogy van-e állapota egy függvénynek/objektumnak. Általában igaz, hogy minél kevesebbnek van, annál jobb (könnyebb érvelni a kód helyességéről), OOP-ben ezt tipikusan úgy oldod meg, hogy szétbontod a kódot value objectekre, amikben csak állapot van, logika nincs, és service-ekre, amikben csak logika van, állapot nincs.


Ez így szerintem nem igaz. Amit itt leírsz az tipikusan a procedurális megközelítés, használjunk függvényeket és adat struktúrákat... Az oo megközelítés ezzel szemben inkább az adat rejtés és a polimorfizmus mellett van, lásd OCP. Mindkét megközelítésnek megvan a maga helye, és teljesen szituáció függő, hogy éppen melyiket érdemes használni...
13

Business objektumok

vbence · 2014. Már. 11. (K), 10.49
Általában az üzleti logika objektumai amik value-objektumok (Ügyfél, Termék). Ezek kliensoldali párjai létrehozhatók mondjuk WSDL-en (vagy más remoting megoldáson) keresztül, és az interfész a szerveroldali változattal memegyező lesz.

Ha elekezdjük "okosítani" az egyik oldalt, akkor már nem lesz párban a két interfész.

Ha szükséges némi logika (bonyolultabb képlettel számított adatok vagy több mezőt módosító eljárások) azt egy külső segítőosztály is meg tudja valósítani (ÜgyfélUtil, TermékUtil) aminek kódbázisa lehet Haxe vagy Rhino alapú, így ez is egységes API-t eredményez minden platformon.

A betöltést/mentést semmiképpen sem ajánlott magában a business osztályban megvalósítani, Ügyfél.save() -szerű megoldások nagyon sok mindent fűznek össze.
14

Eleve a klienssel való

inf · 2014. Már. 11. (K), 11.01
Eleve a klienssel való kapcsolattartás, a mentés mikéntje, stb... nem szabadna, hogy a business logic része legyen. A BL akkor a legjobb, ha csak nyelvi elemeket tartalmaz, és minden egyébhez meghatározott interface-ű adapterekkel kapcsolódik. Na legalábbis elméletileg. A gyakorlatban most tesztelem ezt a fajta architecture-t.
16

Nem csak az üzleti logikára

tgr · 2014. Már. 16. (V), 07.51
Nem csak az üzleti logikára igaz ez, pl. egy web frameworkben is tipikusan van egy request és egy response objektumod, és a műveleteket már valami más végzi rajtuk, hogy könnyen lehessen cserélni az implementációt anélkül, hogy a controllernek tudnia kéne róla. (Persze szóhasználat kérdése, mondhatod azt, hogy a web framwork írójának a szemszögéből a request kezelése az üzleti logika.) Általában a fontos osztályoknál nem akarod megkötni a kezed azzal, hogy a benne tárolt adatokra és a rajta végzett műveletekre ugyanazt az öröklési fát használod.
12

Reentrancy

BlaZe · 2014. Már. 11. (K), 02.16
Tehát egy globális változót használó függvény visszatérési értéke – ugyanazokkal a paraméterekkel meghívva – az idő folyamán változik, ha a változót átírjuk, ezt angolul úgy hívják, hogy nem reentrant.
Én értem mire gondolsz, de aki esetleg nem ismeri, annak picit félrevezető lehet ez a definíció. Azt jelenti a reentrancy, hogy a függvény menet közben megszakítható, újra meghívható, majd a második hívást követően az első a második hívás futásától függetlenül helyes eredményt ad. Ennek egyik következménye, hogy a reentrant függvénynek nem lehet olyan függősége, amit ő maga módosít, így például nem függhet egy globális változón, amit ír.

Ahogy ez ebből látszik is, a reentrencynek van többszálú aspektusa is, vagyis egy reentrant függvényt több szálból párhuzamosan meghívva, a különböző szálakon futó függvények nem módosítják egymás viselkedését. Ebből következően nem függhetnek olyan adaton, amit ők maguk módosítanak, és több szálból is elérhetőek, vagyis shared adaton. A reentrancy utóbbi definícióját szokták keverni a thread-safetyvel, de a kettő kicsit mást jelent. Egy thread-safe függvény használhat shared adatot, viszont biztosítania kell, hogy a konkurrens hozzáférés ne zavarja össze a működését, pl a hozzáférés szinkronizálásával (lock).

És hogy ez még viccesebb legyen, a reentrancy fogalma létezik a lockok esetében is: a reentrant lock engedélyezi a lockot "fogó" szál számára a lockkal védett kódblokkba az újbóli belépést (pl rekurzió). Pl a Java synchronized metódusai és blokkjai reentrant lockként viselkednek.

Még talán annyi, hogy az nem negatív minősítése egy függvénynek, hogy reentrant, vagy sem, így a globális változókat használó függvények nem emiatt ördögtől valók, hanem mert közös lónak túros a háta :)