Globális változók és osztályok
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: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?
■ 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.
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?
Globális állapot
"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):
Ezzel szemben a következő megoldás...
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:
Arra szerettem volna
???
setter
Ebben a változatban a tesztek
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.
runkit powa
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.
Azért nem szeretjük őket,
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...
Két nagyon különböző
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).
OOP-ben ezt tipikusan úgy
ha jól emlékszem
(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)
Pl. a modern frameworkok
(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.)
Ettől teljesen eltérő kérdés,
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...
Business objektumok
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.Eleve a klienssel való
Nem csak az üzleti logikára
Reentrancy
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 :)