OOP a pravidla pro konstruktor

n

Re:OOP a pravidla pro kontruktor
« Odpověď #15 kdy: 03. 06. 2018, 07:07:05 »
...
S tim vsim souhlasim, moc pekne napsano...

...
Chtel bych ale k tomuto poznamenat, ze osobne mam opacny nazor. Samozrejme neni vhodne vsechno dogmaticky odmitat, nebo prijimat a jsou ruzne situace, kdy se hodi i jine reseni(napriklad kvuli rychlosti), nicmene:
(Pozn: Toto je muj nazor, neminim vam ho tady nutit.)
Ve vetsine pripadu, pokud jsi se rozhodl pouzivat v programu vyjimky, je vhodne je vyhazovat hned pri konstrukci, pokud zjistis, ze vstupni data jsou nekonzistentni, nebo proste neodpovidaji pravidlum(pokud neni konstruktor privatni, tak idealne v nem, pripadne v nejake factory metode). Nadrazene funkce maji pak moznost rychle a precizne reagovat na vzniklou chybu. Do budoucna je toto mnohem lepe udrzovatelne(Vsechny API by mely byt co nejprimocarejsi a nejintuitivneji pouzitelne - pokud musis po konstrukci objektu explicitne validovat stav uz zkonstruovaneho objektu, tak to neni intuitivni. Mnohem lepsi je vubec nedovolit zkonstruovat nevalidni objekt.)

Co je ovsem dulezite je, ze validace i vypocty se musi vztahovat opravdu k dane tride. V konstruktoru samozrejme nevolat(nekde to lze, byvaji z toho logicky velke problemy, kdyz potomek jeste neni inicializovany) virtualni metody. Tj. je treba si uvedomit co vlastne konstruujete a validovat/pocitat presne pouze to, co dana trida predstavuje. Validovat/dopocitavat prvky, ktere jsou az kontrakt potomka, je treba delat az v nem a hlavne je treba nepredpokladat, co bude potomek potrebovat validovat/dopocitavat, protoze zasadne vzdy neco opomenete anebo naopak nastavite prilis striktni podminky. Silne dodrzovani tohoto (tak nejak za rucicku) prirozene vede i k lepsimu navrhu -  muze to byt takova pomocna berlicka, ze ktere vypadne lepsi hierarchie trid, vcetne abstraktnich trid, atd...


L.

Re:OOP a pravidla pro kontruktor
« Odpověď #16 kdy: 03. 06. 2018, 08:03:51 »
Konkrétně v Javě je best practice nedávat do konstruktoru co tam není nutné. Konstruktor má vrátit objekt v konzistentním stavu, ale nic víc.

Problém nastává v případě, kdy konstruktor volá virtuální (overridovatelné) metody. Pak se může stát, že se zavolá implementace potomka, kterému ale konstruktor ještě neproběhl a tedy jeho pole nejsou nainicializována.

balki

Re:OOP a pravidla pro kontruktor
« Odpověď #17 kdy: 03. 06. 2018, 09:27:08 »
Došel jsem ze zkušeností k tomu, že konstruktor je třeba výlučně používat pouze k nasetování stavu objektu. Je chyba v konstruktoru provádět jakékoliv výpočty atp., vždycky jsem se tím dostal do problémů a musel jsem to refaktorovat. Máte stejný poznatek? A pokud ano, jak jste na to přišli? Nečetl jsem o tom nikdy v žádné literatuře a přitom mi to přijde jako důležité pravidlo.

Pokial clovek nechce pouzivat lazy inicializaciu, tak je v konstruktore vhodne robit nejake vypocty. Dovodov, preco nepouzivat lazy inicializaciu je viacero, nebudem to tu rozoberat. Plati to najma u objektov, ktory maju dlhsiu zivotnost a su nakladnejsie na vyrobenie. Frameworky maju za tymto ucelom "init" metodu, tak ale je to prast ako uhod, len je o moznost viac, pouzit objekt v nekonzistentnom stave.

anonym

Re:OOP a pravidla pro kontruktor
« Odpověď #18 kdy: 03. 06. 2018, 09:33:58 »
Viz to Unit testování: třída bude mít private metody a já je budu muset mockovat. Jak budeš mockovat, když už při zavolání konstruktoru se ti spustí výpočetní logika?

Jak chceš mockovat privátní metody?
Jak to souvisí s tím, že při konstruktoru se ti spustí výpočtení logika?
Jak to jakkoliv komplikuje jednotkové testování?

Nechápu.

Ok ještě jinak: máš třídu A a ta má vnitřní závislost na třídu B, která spustí laser, co vypálí díru do zdi. Ta B se nijak nesetuje zvenčí, je vytvářená uvnitř. No a ty sloučíš inicializaci i výpočet do konstruktoru. Jak to budeš potom testovat? Když si v testu zavoláš new A(), jsi v zadeki, protože se ti zavolá B a ty to nijak nemůžeš ovlivnit. Proto už z hlediska testování je na nic, když se v konstruktoru spustí nějaký návazný proces - ten musí být v metodě. A než přemýšlet, co se stane, tak raději budu dávat do konstruktoru pouze inicializace objektu.  Když objekt nejprve inicializuju, pak v něm můžu mockovat potřebné věci. A pak terpve spouštím metodu, kterou chci testovat.

L.

Re:OOP a pravidla pro kontruktor
« Odpověď #19 kdy: 03. 06. 2018, 09:58:37 »
Frameworky maju za tymto ucelom "init" metodu, tak ale je to prast ako uhod, ...

Právě, že není. Alespoň v Javě platí, že konstruktor jako první věc musí zavolat konstruktor rodiče. U init metody žádné takové omezení není.


anonym

Re:OOP a pravidla pro kontruktor
« Odpověď #20 kdy: 03. 06. 2018, 10:23:00 »
best practice je nepsat konstruktory vůbec, nechat je generovat automaticky. Validace a inicializace definovat pro jednotlivé atributy. Potom máte u všech objektů standardizované parametry a chybové hlášky. V Pythonu používáme všude knihovnu attrs.

Ano to je další možnost. V javě se na to používá Lombok.

borekz

  • ****
  • 492
    • Zobrazit profil
    • E-mail
Re:OOP a pravidla pro kontruktor
« Odpověď #21 kdy: 03. 06. 2018, 10:27:58 »
Problém nastává v případě, kdy konstruktor volá virtuální (overridovatelné) metody. Pak se může stát, že se zavolá implementace potomka, kterému ale konstruktor ještě neproběhl a tedy jeho pole nejsou nainicializována.
Potvrzuji, že to je v Javě možné. V C++ to možné není. Pokud se volá virtuální metoda v konstruktoru, volá se verze ze stejné třídy, jako by nebyla virtuální.

wayan

Re:OOP a pravidla pro kontruktor
« Odpověď #22 kdy: 03. 06. 2018, 10:29:42 »
Došel jsem ze zkušeností k tomu, že konstruktor je třeba výlučně používat pouze k nasetování stavu objektu. Je chyba v konstruktoru provádět jakékoliv výpočty atp., vždycky jsem se tím dostal do problémů a musel jsem to refaktorovat. Máte stejný poznatek? A pokud ano, jak jste na to přišli? Nečetl jsem o tom nikdy v žádné literatuře a přitom mi to přijde jako důležité pravidlo.

Dospěl jsem ke stejnému závěru. Podobně jako jsem dospěl k eliminaci dědičnosti ve vlastním kódu a jejímu částečnému "nahrazení" pomocí skládání jednoduchých, jednoúčelových objektů. Přesné vysvětlení, jak jsem k tomu došel, bohužel nemám. Napadá mě jen:

Složitý konstruktor (vlastně jakýkoli neautomatický) konstruktor podle mě porušuje Single Responsibility principle, kdy každá třída by měla mít jen jeden účel, tedy umožnit konkrétní instanci dělat nějakou práci. Konstrukce objektu stojí v tomto pojetí jaksi mimo. Stejně mimo jako jakákoli (statická) metoda třídy, která nepracuje s konkrétním objektem.

Příčinou mnoha zbytečně složitých - a rádoby chytře děděných - konstruktorů, které jsem viděl, je pouze neschopnost najít místo zcela mimo třídu (najít jméno), kam by autor tento kód mohl umístit. Vznikají pak případy, kdy objekt pracuje s nějakou vlastností, ale parametrem konstruktoru je konfigurační soubor a konstruktor třídy vlastnost z konfiguračního souboru "šikovně" vytáhne a tato vytažení se pak "chytře" dědí, respektive sdílí přes nějaké role.

Bill

Re:OOP a pravidla pro kontruktor
« Odpověď #23 kdy: 03. 06. 2018, 11:04:53 »
Když máte složitou logiku konstrukce objektů, vytvořte statickou metodu s příhodným názvem, třeba LoadFromConfigurationFile.

wayan

Re:OOP a pravidla pro kontruktor
« Odpověď #24 kdy: 03. 06. 2018, 14:06:54 »
Když máte složitou logiku konstrukce objektů, vytvořte statickou metodu s příhodným názvem, třeba LoadFromConfigurationFile.

Pokud myslíte statickou metodu ve stejné třídě, tak i s tímto přístupem silně nesouhlasím. V rámci jedné třídy se pak míchají dvě zcela odlišná rozhraní - jednak "obecný" interface objektu a dvak způsob konstrukce objektu, který je zcela poplatný  konkrétní aplikaci ve které se objekt používá a jejím pravidlům - formátu konfigurace, mapování konfigurace na parametry, apod. Nehledě na to, že většinou nechci, aby mi konstruktor každého dílčího objektu načítal znovu a znovu  konfigurační soubor a řešil problémy s tím spojené, konfigurační soubor načtu na začátku aplikace jednou a v případě problémů se do konstrukce objektů, které jsou na něm závislé, vůbec nepouštím.

Nesouhlasím s tím přesto, že se uvedený postup asi občas využívá - LoadFromConfigurationFile, NewFromCommandLineOptions, ....


Kit

Re:OOP a pravidla pro kontruktor
« Odpověď #25 kdy: 03. 06. 2018, 14:26:59 »
Já vždycky když jsem sloučil inicializaci a výpočet do konstruktoru, dostal jsem se do slepé uličky - jednak v kódu a jeho struktuře samotné, tak při unit testování. Ani validaci bych nedělal v konstruktoru, jak říká Kit. Když potřebuju validovat nějaká data, udělám z té třídy pojo a validaci té třídy dám bokem do jiné třídy.

Je velmi jednoduché validaci do takového objektu injektovat přes některou z jeho metod.
Kód: [Vybrat]
$object->validate($validator);

Franta <xkucf03/>

Re:OOP a pravidla pro kontruktor
« Odpověď #26 kdy: 03. 06. 2018, 15:28:38 »
Já vždycky když jsem sloučil inicializaci a výpočet do konstruktoru, dostal jsem se do slepé uličky - jednak v kódu a jeho struktuře samotné, tak při unit testování. Ani validaci bych nedělal v konstruktoru, jak říká Kit. Když potřebuju validovat nějaká data, udělám z té třídy pojo a validaci té třídy dám bokem do jiné třídy.

Je velmi jednoduché validaci do takového objektu injektovat přes některou z jeho metod.
Kód: [Vybrat]
$object->validate($validator);

Jaký je důvod, aby validaci řídil objekt, který je validován a ne validátor? Tzn. proč bys dal metodu validate() do objektu a ne do validátoru?

Ze sémantického hlediska mi přijde přirozenější a správnější říct: „validátore, zkontroluj mi tenhle objekt“ a ne „objekte, běž se nechat zkontrolovat k validátorovi a pak mi řekni, jak jsi dopadl“.

Důvodem, proč to dělat obráceně by mohla být viditelnost polí, ale to podle mého není platný argument, protože by to narušovalo zapouzdření – pokud se objekt navenek (tzn. publicprotected pole a metody) chová správně, tak bychom ho měli považoval za validní – co si dělá uvnitř v privátních polích a metodách, to je jeho věc a nás to nemusí zajímat.

BoneFlute

  • *****
  • 1 981
    • Zobrazit profil
Re:OOP a pravidla pro kontruktor
« Odpověď #27 kdy: 03. 06. 2018, 15:36:44 »
Když si v testu zavoláš new A(), jsi v zadeki, protože se ti zavolá B a ty to nijak nemůžeš ovlivnit.
Ano, protože voláš B když ho volat nechceš. S mockováním to nesouvisí s testováním také ne.

Proto už z hlediska testování je na nic, když se v konstruktoru spustí nějaký návazný proces - ten musí být v metodě.
Když bude v metodě, tak z hlediska testování bude úplně stejný problém. Z hlediska mockování taky. Vůbec nic jsi tím nevyřešil.

A než přemýšlet, co se stane, tak raději budu dávat do konstruktoru pouze inicializace objektu.
Je mi líto, ale programování je o přemejšlení.

Když objekt nejprve inicializuju, pak v něm můžu mockovat potřebné věci. A pak terpve spouštím metodu, kterou chci testovat.
Ne, nemůžeš. Tím, že jsi to přestěhoval z konstruktoru do metody jsi vůbec nic nevyřešil. Furt ti to vypálí díru do zdi, furt to nemůžeš mockovat, furt to nejde testovat. Ten problém je totiž jinde.


Jestli ty se jen blbě nevyjadřuješ.

Dávat logiku do konstruktoru je IMHO naprosto v pořádku. Samozřejmě je nesmysl, dát do konstruktoru logiku "pal", když tu logiku chceš vykonat až zavoláním metody inst.pal(). Ale to absolutně nijak nesouvisí s pravidlem, který jsi postuloval na začátku. To považuju za zcela chybné.

BoneFlute

  • *****
  • 1 981
    • Zobrazit profil
Re:OOP a pravidla pro kontruktor
« Odpověď #28 kdy: 03. 06. 2018, 15:39:54 »
Došel jsem ze zkušeností k tomu, že konstruktor je třeba výlučně používat pouze k nasetování stavu objektu. Je chyba v konstruktoru provádět jakékoliv výpočty atp., vždycky jsem se tím dostal do problémů a musel jsem to refaktorovat. Máte stejný poznatek? A pokud ano, jak jste na to přišli? Nečetl jsem o tom nikdy v žádné literatuře a přitom mi to přijde jako důležité pravidlo.

Dospěl jsem ke stejnému závěru. Podobně jako jsem dospěl k eliminaci dědičnosti ve vlastním kódu a jejímu částečnému "nahrazení" pomocí skládání jednoduchých, jednoúčelových objektů. Přesné vysvětlení, jak jsem k tomu došel, bohužel nemám. Napadá mě jen:

Složitý konstruktor (vlastně jakýkoli neautomatický) konstruktor podle mě porušuje Single Responsibility principle, kdy každá třída by měla mít jen jeden účel, tedy umožnit konkrétní instanci dělat nějakou práci. Konstrukce objektu stojí v tomto pojetí jaksi mimo. Stejně mimo jako jakákoli (statická) metoda třídy, která nepracuje s konkrétním objektem.

Příčinou mnoha zbytečně složitých - a rádoby chytře děděných - konstruktorů, které jsem viděl, je pouze neschopnost najít místo zcela mimo třídu (najít jméno), kam by autor tento kód mohl umístit. Vznikají pak případy, kdy objekt pracuje s nějakou vlastností, ale parametrem konstruktoru je konfigurační soubor a konstruktor třídy vlastnost z konfiguračního souboru "šikovně" vytáhne a tato vytažení se pak "chytře" dědí, respektive sdílí přes nějaké role.

Na jedné staně extrém, kdy v konstruktoru jen nasetujeme atributy bez jakékoliv logiky.
Na druhé straně, logiku, kterou potřebujeme nemusíme nutně implementovat v konstruktoru. Můžeme ji mět v nějakých pomocných třídách, statických metodách - každopádně se ale provede v konstruktoru. Protože kdykoliv později je pozdě.

anonym

Re:OOP a pravidla pro kontruktor
« Odpověď #29 kdy: 03. 06. 2018, 16:44:16 »
Došel jsem ze zkušeností k tomu, že konstruktor je třeba výlučně používat pouze k nasetování stavu objektu. Je chyba v konstruktoru provádět jakékoliv výpočty atp., vždycky jsem se tím dostal do problémů a musel jsem to refaktorovat. Máte stejný poznatek? A pokud ano, jak jste na to přišli? Nečetl jsem o tom nikdy v žádné literatuře a přitom mi to přijde jako důležité pravidlo.

Dospěl jsem ke stejnému závěru. Podobně jako jsem dospěl k eliminaci dědičnosti ve vlastním kódu a jejímu částečnému "nahrazení" pomocí skládání jednoduchých, jednoúčelových objektů. Přesné vysvětlení, jak jsem k tomu došel, bohužel nemám. Napadá mě jen:

Složitý konstruktor (vlastně jakýkoli neautomatický) konstruktor podle mě porušuje Single Responsibility principle, kdy každá třída by měla mít jen jeden účel, tedy umožnit konkrétní instanci dělat nějakou práci. Konstrukce objektu stojí v tomto pojetí jaksi mimo. Stejně mimo jako jakákoli (statická) metoda třídy, která nepracuje s konkrétním objektem.

Příčinou mnoha zbytečně složitých - a rádoby chytře děděných - konstruktorů, které jsem viděl, je pouze neschopnost najít místo zcela mimo třídu (najít jméno), kam by autor tento kód mohl umístit. Vznikají pak případy, kdy objekt pracuje s nějakou vlastností, ale parametrem konstruktoru je konfigurační soubor a konstruktor třídy vlastnost z konfiguračního souboru "šikovně" vytáhne a tato vytažení se pak "chytře" dědí, respektive sdílí přes nějaké role.

Já si "dědičnost" zreformuloval na "rozšiřování třídy". Slovo dědičnost v OOP tak, jak je popisováno v literatuře (ala  zvířátka v ZOO), mi přijde silně zavádějící a aplikace tohoto typu dědičnosti vede k problémům v kódu. Proto když přemýšlím nad dědičností, používám vždycky vztah Rozšiřovat/Specializace, a nikoliv Dědit/Potomek. Protože Dědit/Potomek vede intuitivně ke vztahům typu Zvíře<-Chlupaté<-Tygr, nebo Otec<-Syn<-Vnuk, Obdelník<-Čtverec, což jsou na OOP nesmyslné/neaplikovatelné vazby z reálného a velice složitého světa. Oproti tomu Rozšiřovat/Specializace je mnohem konkrétnější a vztahuje se více k praktické podobě programového kódu.

Osobně si (zatím) myslím, že ty snahy úplně dát pryč Rozšiřívání tříd jsou druhý extrém. Je to podobné, jako když vznikají jazyky typu Go, že jako OOP je moc složité a nepotřebné a proto budoucnost patříd jazykům, kde se dají dělat pouze funkce.