Jak vysvětlit, že settery nejsou špatně? Nebo jsou? rubrika: Návrh
Narazil jsem před rokem na dobrý tým. Šéf shání dobré zakázky a protože je programátor, záleží mu i na kvalitě kódu. Děláme code review, máme coding standards. Učíme se, zlepšujeme se. Někdy si to jen myslíme.
Jedním pravidlem totiž je "Setters - do not use them". Přijde mi to docela dogmatické.
Jedna náhrada, je že místo setterů se má použít argument v konstruktoru.
To je jasné pro povinné atributy.
Settery jsou mnohdy používány zbytečně (ale přece né vždy?), protože je prostě generuje IDE/make a private atributy už pak nejsou úplně tak private. Myslím, že na to poukazuje většina článků, které jsou "proti setterům".
Šéf zmiňuje ještě jeden důvod proč je nepoužívat. "Znějí příliš obecně, málo výstižně". Typicky uvádí, že místo setStatus(1/0) se má použít activate()/deactivate(). V tom má samozřejmě také pravdu. Jenže status s nějakým konečným počtem stavů není typický příklad.
Typičtější je asi Article a jeho title, content, date, author a dejme tomu třeba nepovinný mainImage a mainVideo (nebo mi pomožte s jiným příkladem).
Takové případy prostě přicházejí. Samozřejmě si s tím umíme poradit :-). Ale myslím, že si tím zanášíme do kódu bordel, což je škoda, když je to celé ve snaze mít kód čistý.
Dějí se pak většinou tyto dvě věci.
1) převlečené settery
Settery se přejmenují na updatery. Prostě místo set() dáme update(). To nám v review projde. :-)
Jenže to stejně není moc výstižné. Snad možná pro ty povinné atributy, ty už nastavené byly a tak je aktualizuju. Ale ty nepovinné? Prostě to tam teď chci "nastavit" a nejlepší slovo mi přijde "set". Někdy se objeví něco jako "přidat" addMainImage() nebo "přiřadit" assignMainVideo(). Jenže prostě se to stává takové neprůhledné. Člověk si začne říkat co to asi dělá, jestli se jich dá nastavit (add je typicky u ArrayListu).
A co když ho chci odebrat? Jasné by bylo setMainImage(null), to by neprošlo, takže tu máme assignMainVideo(null), což je divné removeMainVideo() zas naznačuje ten ArrayList. A dělat kvůli tomu resetMainImage(), když předtím nebylo možné použít set...
Prostě mám pocit, že se s tím názvoslovím dostáváme, kam nechceme. Nebo aspoň já ne.
2) setujeme, ehm updatujeme hromadně z přepravky
Přepravka, která má public :-) atributy (občas si všimnu, že je to s nějakými anotovanými asserty Symfony\Component\Validator\Constraints as Assert ... ale tady mám mezeru ve vzdělání, nevím jak to funguje, moc tomu nevěřím) se předá v jedné metodě updateFromDeatils(entityDetails), která bez setterů a často bez nějakých kontrol, které by do nich měli patřit (aspoň kdyby byly private).
No takže pokud chápu význam nějakého zapouzdření, kterého se dosahuje pomocí private atributů a kontrolovaného přístupu k nim přes metody, tady se prostě o tu výhodu připravujeme.
Aspoň z mého pohledu. Nejsem žádný profík na OOP, ale tohle mi prostě smrdí.
Jsem sám? Jsou snad settery špatně? Máte nápad, co k tomu dodat? Dík
Settery mají svůj význam. Je špatné je používat špatně, stejně jako je
špatné je nepoužívat protože se to řeklo.
Nahrazením setMainVideo() za assignMainVideo() se vůbec, ale vůbec nic
nezměnilo. assignMainVideo() je setter. Jenom zamaskovanej. Taková
úlitba temným bohům :-)
updateFromDeatils(entityDetails) je celkem použitelná věc, protože měním stav objektu atomicky, a mohu uvnitř zajistit všechny patřicné kontroly. (Pokud to nedělám, jsem blbec.)
Základní pravidlo, které propaguji je, že píšu/programuju to co chci ve skutečnosti dělat. Tedy například:
author = context.getAuthor() author.setAddress(newAddress) context.update(author)
Nastavuji/měním existujícímu uživateli adresu.
Na rozdíl od:
author = new User author.setName("John") author.setSurname("Dee") // author.setBirth() - zapomněl jsem povinnou položku author.setAddress(address) context.insert(author)
V druhém případě, nejen, že jsem zapomněl na povinnou položku, ale ve skutečnosti dělám sadu těchto kroků:
- vytvořím úplně prázdného uživatele (což je nevalidní stav)
- tomu uživateli nastavím pouze jméno (což je nevalidní stav)
- následně tomu uživateli změním pouze příjmení (což je nevalidní stav)
- následně tomu uživateli změním pouze adresu (což je stále nevalidní stav)
Což není to co chci dělat. Nemluvě o tom, že uživatel beze jména je nevalidní.
Což je druhé pravidlo - objekt se nesmí dostat do nevalidního stavu.
Ve své podstatě je jedno, jestli toho dosáhneme pomocí konstruktoru (ideální, protože nejsnazší a většinou nám v tom jazyk pomáhá), nebo pomocí setterů.
Dobré je, když jazyk podporuje vícero konstruktorů. Díky tomu mohu pro každý use-case vytvořit konstruktor, kterým nastavím takovou konstelaci objektu, kterou požaduji. S validací a se sémantikou. Vhodně to řeší volitelné props.
Některé jazyky podporují hromadné nastavení props. To je fajn. Můžeš v tom hlídat validaci. Něco podobného jde více či méně hezky udělat asi v každém jazyce.
Zakázat settery a přitom stále vytvářet nevalidní objekty mi přijde takové nepromyšlené.
A do třetice, je vhodné respektovat zvyklosti.
Ty prefixy obvykle něco znamenají. Obvykle pomáhají se zorientovat. Místo getUpdated() mít isUpdated() je přirozeně lepší, protože to zní normálněji. Ale nahradit setUpdated() za assignUpdated() mi přijde samoúčelné a ve výsledku ke škodě. Já konkrétně assignUpdated() používám. Ale nikoliv jako setter. Používám ho v případě, kdy chci nějakému jinému objektu nastavit nějaké vlastnosti. Ostatně to ta angličtina i trochu říká. A každý vývojář, který to po mě bude číst si řekne "co to je, jaký podivný prefix, co to asi znamená". A to je v mém případě cílem.
Komentáře
- vit.herman : Jen k druhému pravidlu "Objekt se nesmí dostat do nevalidního stavu": V databázově orientovaných aplikacích lze jen obtížně zaručit. Např. nastavíš referenci na neexistující entitu. I kdyby jsi nakrásně referenci databázově kontroloval vždy při přiřazení (magické a tím potenciálně neefektivní), nezaručíš platnost této reference, protože cíl reference může být mezitím smazán. Teoreticky by databáze mohla notifikovat o změnách a objekt na to vnitřně reagovat. Asi je zřejmé, že nedává smysl takto implementovat konzistenci v běžných aplikacích. Objektové modelování mi tedy dává smysl jen pro modelování paměťových struktur daného prog. jazyka a nikoli perzistentních struktur externí databáze. Právě z tohoto důvodu jsem se ze světa tříd a objektů vrátil k datovým strukturám a funkcím pro běžné aplikace. Ale piškvorky bych klidně programoval objektově. — 20.4.2020
- v6ak : IMHO je praktičtější si u objektů ujasnit, jakou míru validity požadujeme. E-mailová adresa lze zkontrolovat nezávisle na okolním světě (aspoň teda syntax), u referencí je praktičtější použít presumpci správnosti a nechat to zvalidovat až databázi. — 20.4.2020
- vit.herman : @v6ak: Ano, to jsem si přesně ujasnil. A to do té míry, že objekty pro účel zapouzdření databázových struktur už dávnou nepoužívám. — 20.4.2020
- Kit : @vit.herman: Objekty a relační databáze moc dohromady nejdou. Je potřeba mít buď mezivrstvu ORM, anebo mít objektově pouze řídicí objekty, přes které data jen protékají ve formě datových struktur. Osobně se přikláním ke druhé variantě, neboť odpadají aplikační cache, které bývají častým zdrojem problémů. Dále při vhodné horizontální dekompozici vypadají řídicí objekty velmi jednoduše a dají se snadno zaměňovat. — 20.4.2020
- Taco : @vit.herman: Já jsem na OOP rezignoval také. Každopádně máš samozřejmě pravdu. A já se absolutně nestydím sem nebo tam nasekat hromadu setterů - když je důvodem ekonomie. Člověk musí vědět proč to dělá. Pak někdy dává smysl pravidla porušovat. — 20.4.2020
- Mlocik97 : No Settery a Gettery v Jave sú vraj len preto, že aby programátori platený od riadkov kódu, mohli program nafúknuť na 2 násobok riadkov. Aspoň to som počul od viacerých a zároveň si myslím totéž. — 20.4.2020
- Kit : @Mlocik97: Kdyby settery byly užitečné, tak by je do Javy přidali podobně, jako byly přidány do C#. Jenže užitečné nejsou, ale vývojáři je stejně chtěli, neboť si mysleli, že tím zapouzdří objekty. — 20.4.2020
- vit.herman : @Taco: Souhlas. Aplikoval bych především tvé první pravidlo "píšu/programuju to co chci ve skutečnosti dělat". Ztotožňuji se s ním. A někdy to může zahrnovat i to psaní getterů, setterů. Určitě neplatí prohlášení, že settery jsou vždy špatné. To by bylo veliké zjednodušení. — 21.4.2020
- vit.herman : @Mlocik97: Buď jsi to blbě pochopil, protože sis to nezařadil do kontextu nebo Ti chybí představivost nebo jsi poslouchal nesmysly. Není těžké najít ukázat vhodné případu užití pro getter/setter. Pokud budeš chtít dejme tomu po nastavení barvy hracího pole překreslit canvas, už potřebuješ setter, který vedle nastavení barvy pole provede překreslení. Nebo pokud budeš chtít okamžité perzistentní ukládání hodnot datové entity, potřebuješ setter. Atd. Dá se vymyslet nekonečno případů. Když někomu radíš, měl bys vždy brát ohled na hranice platnosti toho, co říkáš. Ale to je tvá velká slabina a ukazuje na tvou nezkušenost. Asi je to ode mne arogantní, ale doporučoval bych méně radit a spíš naslouchat... — 21.4.2020
- Taco : @Kit: nechám si vysvětlit, co je na tomto kousku kódu nezapozdřeného: https://gist.github.com/tacoberu/fdb8769de81ec7a5dd042b2bec6444ed — 21.4.2020
- Kit : @Taco: Kdokoli zvenčí může změnit jméno, adresu a věk, jak se mu zamane. Nikde žádné ověřování, zda to smí udělat. — 22.4.2020
- Taco : @Kit: Ano, to je v pořádku. Otázka zněla zda to je zapouzdřené. Máš tam něco ještě? — 22.4.2020
- Kit : @Taco: Z toho, co jsem napsal, vyplývá, že to zapouzdřené není. — 22.4.2020
- Taco : @Kit: Děkuji za odpověď. Nebudu tě dál mučit. — 22.4.2020
- Andreaw Fean : @Kit: To co popisuješ už není zapouzdření, zapouzdření vyžaduje jen neprozrazovat vnitřní implementaci. Ty už si vymýšlíš vlastní pravidla k těm oficiálním. Příklad ze života: Klíč se taky neptá, zda ho smím použít. Pokud se k němu dostanu, tak ho použít můžu. — 22.4.2020
- mr.fatblunt : Se vsim co pise Taco souhlasim a pridal bych jednu asi obecne znamou vec - me na projektech vzdycky nejvic vytrestala mutabilita v multivlaknovem prostredi. Kdyz neco menim za behu (setterem, nebo obecne necim co meni stav) tak je potreba zajistit aby to bylo konzistentni napric vlakny co referenci na mutovany objekt maji na svych zasobnicich. Cim vic je takovych stavu co se daji treba i novackem na projektu kdykoliv za behu zmenit nebo predat mezi vlakny, tim slozitejsi je prijit na to co zpusobilo nejaky uplne nahodny crash systemu v runtime. Dalsi zradou u mutabilnich objektu mohou byt (u pass-by-reference) ruzne RAM-based cache, nikdo nechce menit objekt ktery mezitim jine vlakno dostalo z cache a treba uz jej napul zpracovalo... Proto se snazim mit minimalne datove objekty (jako "User" a podobne) immutable a validace provadet pri vytvareni objektu. Pokud uzivateli chci zmenit adresu, udelam na to specialni metodu v DAO vrstve, pripadne setter vrati novou kopii objektu se zmenenymi udaji (takovy CoW). — 24.4.2020
- v6ak : Tak vícevláknové prostředí by mělo mít nějaký memory model, který nejspíš řekne, že bez nějaké operace jako synchtonizace není možné zapisovat data ani číst data potenciálně zapsaná jiným vláknem. Když to udělám, mohou se dít různé podivné věci vymykající se intuitivnímu chápání významu kódu – právě kvůli různým implementačním detailům (zejména optimalizacím), které se spoléhají, že se budu chovat určitým způsobem. A nejde jen o cache, ale i o různé reorganizace instrukcí kompilátorem i procesorem. Když jedno vlákno podle kódu zapíše hned po sobě hodnoty dvou různých proměnných, mohou ty zápisy na mnoha platformách ostatní vlákna vidět v opačném pořadí, nebo vlivem cache taky třeba vůbec. Nicméně podstatné je, že programátor má hlavně dostatečně rozumět tomu memory modelu. Znalost jevů jako přeskládání instrukcí nebo cache je sice fajn bonus, ale typicky to programátor nepotřebuje nutně znát. — 24.4.2020
Pro zobrazení všech 7 odpovědí se prosím přihlaste:
Nebo se přihlaste jménem a heslem:
Komentáře