Triedne vs. Prototypové OOP

Inkvizitor

Re: Triedne vs. Prototypové OOP
« Odpověď #30 kdy: 18. 03. 2011, 00:58:05 »
To není žádná známá pravda, Logiku, to je nesmysl. U konstruktorů obzvlášť.


Inkvizitor

Re: Triedne vs. Prototypové OOP
« Odpověď #31 kdy: 18. 03. 2011, 01:02:14 »
A to, že některé jazyky nemají pojmenované parametry, je jejich mínus. Ty chytřejší z nich (Haskell apod.) mají aspoň record, kterým je možné dosáhnout téhož jinými prostředky.

ondra.novacisko.cz

Callbacky
« Odpověď #32 kdy: 18. 03. 2011, 12:56:00 »
K těm callbackům. Já callbacky nepoužívám, raději používám interfacy. To je lepší, stejně většinou sada nějakých callbacků bývá často spojena v jeden celek, takže není nic snažšího, než místo nastavování callbacků poslat někam referenci na objekt, který implementuje dané rozhraní a je jasno.

Někdy mám pocit, že se programátoři těch různých tříd bojí. Nebojte se. Já nemám problém vytvořit třídu, která zabírá 3 řádky. Někdy tvořím třídy na jednu řádku. Třídy tvořím uvnitř tříd, uvnitř funkcí. Třída nemusí být nic velkého, v projektech mám tisíce pomocných tříd, a pokud mi už vadí, že jich je moc hodně, že znepřehledňují API, strčím je do nějakého namespace, typicky _intr nebo _internal a hned zmizí z API.

Jinak callbacky taky úplně nezavrhuju, ale často používám pointery na metody, často většinou v kombinaci v šabloně, kdy je dán jen prototyp metody a třída je definována jen jako nějaké T. To se hodí u dispatchingu, kdy simuluj dynamické typování, kdy se třeba volá metoda jménem. Chytne to dispatcher, koukne do tabulky, najde ukazatel na metodu objektu, který to implementuje, a zavolá to. V projektu mám i cosi, co nazývám "akce", to jsou malé objekty, které nesou informaci o tom, která metoda kterého objektu se má při akci zavolat. Je to opět založené na pointeru na metodu. Dalším type objektů jsou "zprávy". Ty také nesou metodu, ale
vlastní objekt dodá až volající. Klasicky nějaký hromadný dispatcher, nebo distributor, který informuje prostřednictvím zprávy nějakou kolekci objektů.

Callbacky na metody (pointery na metody) jsou podle mne mnohem užitečnější. Zajímavý je také to, že lze takto ukazovat i na virtuální metody (podiví se možná ten, kdo ví, jak jsou tato volání vlastně implementována v C++). Nicméně pořád platí, že lepší než callback je interface, pakliže to neznamená, že množství kombinací callbacků by vedlo na obrovské množství objektů implementující všechny možné kombinace (a i tam si lze pomoc šablonama)

Logik

  • *****
  • 1 049
    • Zobrazit profil
    • E-mail
Re: Triedne vs. Prototypové OOP
« Odpověď #33 kdy: 18. 03. 2011, 13:02:07 »
Co není pravda? Že metoda s patnácti argumentama je trochu zvrhlost? Sice konstruktor je jediná výjimka, kdy to může mít opodstatnění, ale ve výsledku je daleko čitelnější jednoduchej konstruktor a následný nastavení vlastností. Srovnej
Kód: [Vybrat]
class A {
   {
   __init__(vlastnosta=1, vlastnostb=2,vlastnostc=3, vlastnostd=4, vlastnoste=5, vlastnostf=6, vlastnostg=7, vlastnosth=8, vlastnosti=9, vlastnostj=10)
             {
             self.vlastnosta=vlastnosta;
             self.vlastnostb=vlastnostb;
             self.vlastnostc=vlastnostc;
             self.vlastnostd=vlastnostd;
             self.vlastnoste=vlastnoste;
             self.vlastnostf=vlastnostf;
             self.vlastnostg=vlastnostg;
             self.vlastnosth=vlastnosth;
             self.vlastnosti=vlastnosti;
             self.vlastnostj=vlastnostj;
             }
  }
a=A(vlastnostc=1, vlastnostd=2);
s
Kód: [Vybrat]
class A:
   def __init__():
             self.vlastnosta=1;
             self.vlastnostb=2;
             self.vlastnostc=3;
             self.vlastnostd=4;
             self.vlastnoste=5;
             self.vlastnostf=6;
             self.vlastnostg=7;
             self.vlastnosth=8;
             self.vlastnosti=9;
             self.vlastnostj=10;
a=A();
a.setVlastnostc(1);
a.setVlastnostd(2);

I když uvažuju jazyk, kde se nemusí definovat proměnné, tak druhá varianta je rozhodně mnohem čitelnější. Navíc je to řešení rychlejší, neboť nemusí na stacku vytvářet všech těch 15 defaultních parametrů konstruktoru (metody setVlastnostc se inlinují, takže tam režie není).

Pokud bych bral jazyk, kde se definují vlastnosti objektu, tak je rozdíl ještě markatnější, neboť zatímco v první variantě by každá vlastnost zabrala dva řádky (jedna definice a jedna v konstruktoru) zatímco varianta dvě by si vystačila s inicializací v definici. A definice vlastností není vůbec "zbytečná věc", neboť pomocí ní lze odchytit typo v přiřazování do vlastností.

Další věc je, že pokud změním požadavky (omezení) na jednu z vlastností, stačí mi upravit setVlastnostx(). V řešení A musím upravit navíc ještě konstruktor. Takže je to i řešení náchylnější k chybám.

No a last but not least: No a klíčová je zkušenost, že zpravidla třída, kterej potřebuje v konstruktoru 15 vlastností má špatnej design: většinou jde o sloučení funkčností více entit do jedné třídy, a mělo by se to rozdělit do víc samostatnejch tříd....
PS: Nesouhlasíš-li, dej příklad objektu s 15 parametry, který je opravu nejlépe implementovat monoliticky.
« Poslední změna: 18. 03. 2011, 13:12:32 od Logik »

Logik

  • *****
  • 1 049
    • Zobrazit profil
    • E-mail
Re: Triedne vs. Prototypové OOP
« Odpověď #34 kdy: 18. 03. 2011, 13:11:24 »
Jo, s tím, že často vhodnější než callback je interface souhlasím.

Ale zaujalo mě, že to říká zrovna člověk píšící v C++, kde se to pomocí interfaců dělá snad nejhůř ze všech jazyků :-) (např. neexistence anonymních tříd, byť to nested classes trochu zachraňují)- ale jak vidět, C++ je mocné a jde v něm všechno snadno a rychle, jen si to holt člověk musí nejdřív sám napsat. :-)




ondra.novacisko.cz

Re: Triedne vs. Prototypové OOP
« Odpověď #35 kdy: 18. 03. 2011, 13:24:27 »
Jo, s tím, že často vhodnější než callback je interface souhlasím.

Ale zaujalo mě, že to říká zrovna člověk píšící v C++, kde se to pomocí interfaců dělá snad nejhůř ze všech jazyků :-) (např. neexistence anonymních tříd, byť to nested classes trochu zachraňují)- ale jak vidět, C++ je mocné a jde v něm všechno snadno a rychle, jen si to holt člověk musí nejdřív sám napsat. :-)

Mě se líbí mýtus anonymních tříd.

Kód: [Vybrat]
void Foo::initListeners(MyObject *obj) {
 
  class AnonymousClass: public IActionListener {
  public:
     Foo &owner;
     AnonymousClass(Foo &owner):owner(owner) {}
     virtual onAction() {owner.onAction();}
  };

  obj->addActionListener(new AnonymousClass(*this));   
}


Pravda, v Javě to je kratší

Kód: [Vybrat]
void initListeners(MyObject obj) {
 
  obj.addActionListener(new ActionListener{
      public onAction() {Foo.onAction();}
  });

}

Ale jinak C++ mechanismus podobný anonymním třídám existuje.

Ale jinak souhlasím, že když si to člověk napíše, tak to jde snadno a rychle. Mám taky už dosti rozsáhlou knihovnu různých takových udělátek. Pak to nejen napíšu rychle, ale ta cena, kterou platím v jiných jazycích za tuhle vymoženost tady prostě není (nebo už je zaplacená). Abych to přirovnal k něčemu. Mohu si pořídit satelit UPC, a bez práce budu platit měsíční paušal. Nebo si koupím, namontuju, zprovozním, zaplatím jednorázově CSLink a pak budu čumět na bednu zdarma. Ano, v tom UPC je v poplatku i HBO, ale ta cena je prostě vysoká (ekvivalent v GC u Javy)

ondra.novacisko.cz

Re: Triedne vs. Prototypové OOP
« Odpověď #36 kdy: 18. 03. 2011, 13:27:06 »
Omlouvám se za syntaxtické chyby v kódu (chybí tam všude void u funkcí), psal jsem to rychle, ale myslím, že to nic nemění na tom, co jsem chtěl prezentovat

ondra.novacisko.cz

Re: Triedne vs. Prototypové OOP
« Odpověď #37 kdy: 18. 03. 2011, 13:38:57 »
Co není pravda? Že metoda s patnácti argumentama je trochu zvrhlost? Sice konstruktor je jediná výjimka, kdy to může mít opodstatnění, ale ve výsledku je daleko čitelnější jednoduchej

Nevýhoda toho druhého případu je ta, že program nejprve musí nastavit proměnné na jednu hodnotu a pak se ta hodnota změní. Pokud to jsou inty, tak bezevšeho, pokud to jsou třeba ikony, tak už je to trošku problém, protože si představ, že program nejprve vytvoří ikonu s nějakým výchozim obsahem a následně je mu ten obsah změněn. Samozřejmě, lze to navrhnout tak, že výchozí stav bude "nic" ale to už zasahuje do obecnosti, vyžaduje to, aby takový stav existoval. Pokud by se jednalo o obecnější objekt, pak tato podmínka je limitující (v C++ typická podmínka, že objekt musí mít defaultní konstruktor, což není vždy vhodné).

Obecně rozděluju objekty na tři základní druhy:
  • stavové
  • jednorázové (readonly)
  • konfigurační

Ad 1) stavové objekty mají svůj vnitřní stav, a ten se po čas jejich života mění
Ad 2) jednorázově se konfigurují se jen při vzniku a dále je není možné měnit. Jediný způsob, jak je změnit je vytvořit nový objekt za pomocí toho původního
Ad 3) nesou typicky konfiguraci, nemají gettery a settery a nemají žádnou výkonnou část.

Stavové objekty jsou asi nejtypičtější. Jednorázové a readonly objekty mají obrovský užitek v MT prostředí, protože přístup do nich není třeba synchronizovat. Naopak stavové objekty se zpravidla v MT nesdílí a každý vlákno má vlastní sadu stavových objektů.

Ale abych se vrátil, právě objekty Ad 2) vyžadují inicializaci pomocí konstruktorů, takže určitě to má smysl a nezavrhoval bych to.

Bone Flute X

Re: Triedne vs. Prototypové OOP
« Odpověď #38 kdy: 18. 03. 2011, 14:16:32 »
Co není pravda? Že metoda s patnácti argumentama je trochu zvrhlost? Sice konstruktor je jediná výjimka, kdy to může mít opodstatnění, ale ve výsledku je daleko čitelnější jednoduchej konstruktor a následný nastavení vlastností.
No, zásadní problém vidím v tom, že jsou to dva objekty s naprosto jiným chováním a použitím. Zatímco v prvním případě vytvářím objekt, který vyžaduje nějaké informace, v druhém případě jsou všechny informace volitelné a tudíž také můžu (a jak známo, co můžu to udělám) jej použít bez nich. Pak záleží, jestli si ty informace kontroluju, následně to vyhazuje výjimky - což ve výsledku IMHO moc čitelné není. A nebo jestli prostě vidím, že musím ten objekt nakrmit při vytvoření.
Teď porovnávej.

Inkvizitor

Re: Triedne vs. Prototypové OOP
« Odpověď #39 kdy: 18. 03. 2011, 23:02:52 »
Typickými představiteli objektů, u kterých má smysl narvat mnoho parametrů do konstruktoru, jsou prvky uživatelského rozhraní. Třeba takový Button z Tkinter má volitelných parametrů přes 20. Spoustu parametrů může mít i třída, která popisuje použitý font, případně možná i pero apod. Tam fakt nemá moc smysl to kouskovat, ikdyž by to asi nějak šlo, ale bylo by to dělení za každou cenu.

Jinak souhlasím s tím, co napsal Bone Flute X; hlavní výhodu parametrizace objektu konstruktorem spatřuji v tom, že to případně sletí už při vytváření objektu nebo mi to pohlídá dokonce pylint. Ten sice umí hlídat i použití atributů, ale myslím, že ne tak spolehlivě. Další výhodou je, že většinu logiky vidím na místě, kde se objekt vytváří. Samozřejmě, čím častěji se vytvářejí instance jedné třídy, tím je ta motivace pro předávání parametrů v konstruktoru větší a naopak.

Abych to shrnul, to pravidlo tří parametrů (nikdy jsem o něm nečetl nebo jsem si ho nezapamatoval, ale existovat může) má podle mě hlavní opodstatnění opravdu v tom, že v jazycích typu C a Java parametry nelze při předávání hodnoty pojmenovat a čitelnost klesá. Jiné jazyky si s tím poradí víc než dobře a legitimní počet parametrů může v extrémním případě jít poměrně vysoko. Pokud ale nebudeme trvat na 15 a více a hranici stanovíme někde mezi 5 a 10, příkladů je možné najít mraky; připojení k databázi - např. psycopg2.connect() má maximálně 7 parametrů, subprocess.Popen() má v konstruktoru dokonce max. 13 volitelných parametrů, SMTP_SSL 6 atd. To jsou (vedle GUI) vesměs případy, kde končí OOP a jeho teorie a přichází interakce s reálným světem vně dané aplikace.

Logik

  • *****
  • 1 049
    • Zobrazit profil
    • E-mail
Re: Triedne vs. Prototypové OOP
« Odpověď #40 kdy: 19. 03. 2011, 17:24:07 »
Ad házení výjimek, jakej je rozdíl mezi
Kód: [Vybrat]
try:
  a=b(a=1,b=2,c=3);
catch:
a
Kód: [Vybrat]
try:
  a=b();
  a.seta(1);
  a.setb(1);
  a.setc(1);
catch:
Jeden rozdíl tam přesně vidím - i když to spadne na nějakou obecnou výjimku, v případě 2) vidím, která vlastnost to způsobila, aniž bych musel lízt do definice objektu.

Další výhoda přístupu 2 je při modifikacích kódu - když budu mergovat patche, tak při přístupu 2 vidím hned a daleko lépe, co daný patch změnil. To, že každá řádka má dělat pokud možno jen jednu věc není pravidlo zbytečné (byť samozřejmě jako všechny ostatní velmi často odůvodnitelně porušitelné).

----

Objekty uživatelskýho rozhraní zpravidla maj hromadu parametrů, ale pokud člověk programuje rozumně, tak např. používá themes, takže většina těch parametrů je danejch... Stejnětak font člověk málokdy vytváří "adhoc". Samozřejmě, existují případy, kdy je třeba tydle objekty vytvářet - ale to jsou poměrně řídké výjimky.

----

Co se týče funkcí - tak samozřejmě existují funkce, kde je více argumentů opodstatněných. Ale příliš časté nejsou. Co se týče např. toho popen, tak tam např. místo předání pole deskriptorů (což by umožnilo dát subprocesu víc než jen standardní tři vstupy/výstupy) se dává každej v spešl argumentu. A navíc je to implementace "děravá", viz varování u atributů vzniklého objektu.

Dál jsou tam flagové proměnné - a to jen jiný zápis bitového pole, který pokládám za o něco vhodnější (líp se uchovává stav volání, loguje, kvůli přidání přepínače se nemusí přidávat argument fce...). Pokud nesouhlasíš, tak prostě tydle argumenty pro účely pravidla počítej jako jeden.
No a pak jsou tam dva argumenty, svázané s implementací té funkce na jednom konkrétním OS, což je poměrně úlet (až se python rozšíří na deset OS, tak tam bude těch proměnejch dvacet??).
A je to kontruktor (takže opět by to šlo rozepsat do následných volání).

No a naposled ta třída dělá x věcí (např. hraní si s deskriptorama), pro který by asi bylo výhodný udělat spešl třídu, protože hrát si s deskriptorama je potřeba i jinde a tak je užitečný, aby se to dělo jednotnym způsobem.
Takže design tý třídy za nějak extra dobrej nepovažuju.

Stejně tak psycopg2.connect() má v podstatě jeden argument - buďto řetězec, nebo struktura či asociativní pole parametrů pro spojení. Zbytek je syntax sugar  pythonu, umožněná hlavně konstrukcí **pole (jinak bych za takovoudle syntaxi "vraždil", protože si ty parametry člověk většinou schovává někde v konfiguraci v nějaké struktuře a rozbalovat je ručně je blbina).

Samozřejmě, tři (já ani nevím, jestli v originále pravidla je tři, čtyři nebo kolik, prostě "málo") je velmi striktní číslo a najdou se příklady, kdy je rozumné mít argumentů víc. Jako každé z pravidel to samozřejmě není pravidlo striktní, ale ukazuje na možné chyby v designu aplikace a když si to sám před sebou obhájím, tak ho klidně poruším. To ale nic
nemění na tom, že má velmi často pravdu....

To, jak python zachází s argumentama je úžasný, asi nejlíp z jazyků, co znám (automatické rozbalování polí, pojmenované parametry). Občas se mi ale zdá, že to vede k přílišnému "zplošťování funkcí"

----

Ondra: to je vnořená, ne anonymní třída. To, že něco pojmenuješ anonymní, tak tím ještě anonymní třídu neuděláš :-). Proto jsem psal také, že to jde nejhůř (syntax je rozvleklá, vyžaduje zbytečnej identifikátor a nedá se napsat "inplace", což vše ztěžuje čitelnost programu) a ne že to nejde.
S readonly objejkty máš samozřejmě pravdu, ale ty málokdy bývají příliš komplexní (a nebo jdou rozložit na agregaci menších komplexních objektů).


Bone Flute X

Re: Triedne vs. Prototypové OOP
« Odpověď #41 kdy: 19. 03. 2011, 18:09:12 »
Ad házení výjimek, jakej je rozdíl mezi
Kód: [Vybrat]
try:
  a=b(a=1,b=2,c=3);
catch:
a
Kód: [Vybrat]
try:
  a=b();
  a.seta(1);
  a.setb(1);
  a.setc(1);
catch:
Jeden rozdíl tam přesně vidím - i když to spadne na nějakou obecnou výjimku, v případě 2) vidím, která vlastnost to způsobila, aniž bych musel lízt do definice objektu.
Co dělá objekt b? Vyhazuje výjimky, když zadáš neplatnou hodnotu? V tom případě bych použil druhou implementaci.
Vyžaduje tři povinné hodnoty? Použiju implementaci první. Umožňuje tři volitelná nastavení? Použiju druhou implementaci. To je jasné.

Problém je v tom, že ten příklad je nedostatečný. Rozepíšu ho:

Kód: [Vybrat]
def foo(a):
  return a.process() + 42

def boo(a):
  return str(a.process()) + 'ipsum'

try:
  a=b();
  a.seta(1);
  a.setb(1);
  # a.setc(1); -- zapoměl jsem
catch:
  pass # tady to projde

# nějaký kód

print boo(a) # tady je to ok.
print foo(a) # tady to vyhodí výjimku, že nejde sčítat none a 42

Snad je z toho příkladu jasné v čem je problém.
1. Chyba vznikne až někde hluboko v kódu.
2. Vznikne někdy.
3. Hláška chyby není vůbec k věci. Problém je v tom, že objekt není dostatečně inicializován, ne v tom, že se nedají sčítat none a číslo.

Řešení jsou dvě, obě jsem zmínil. Buď se v metodě process bude kontrolovat, zda jsou naplněné všechny tři proměnné, nebo se ta kontrola bude provádět v konstruktoru, a zajistí ji interpret/překladač.

Já jsem pro druhou možnost, protože je to jednodužší a kratší. Taky bych mohl prohlásit že konstruktor je od toho konstruktorem, aby vytvořil regulérní objekt.

Je tu ještě jeden scénář:
V konstruktoru přiřadím atribut objektu. Dále někde v kódu si to rozmyslím, a budu tu hodnotu chtít změnit. Vytvořím si na to setter (nějak), a ten mi bude kontrolovat, zda přiřazuji platnou hodnotu. Zpětně pak tento setter použiju v konstruktoru. zisk je v tom, že:
1. Vždy mám validní hodnotu.
2. Validaci mám na jednom místě.
3. Nemůže se mi stát, že hodnota není nastavená.
4. Nemusím tyto hodnoty dále kontrolovat.
5. Hodnoty mohu svobodně měnit i později aniž bych si to rozbil.

V případě řešení bez parametrů v konstruktoru, nesplňuji body 2,3,4. A to jen kvůli tomu, aby to nebylo moc velké.

Na závěr bych možná ještě zmínil, že imho pojmenované parametry nic neřeší. Protože já zde argumentuji, že povinné hodnoty objektu patří do konstruktoru, zatímco volitelné hodnoty objektu patří do samostatných metod/property.

Bone Flute X

Re: Triedne vs. Prototypové OOP
« Odpověď #42 kdy: 19. 03. 2011, 18:19:12 »
Eh, ještě doplním, že setkal jsem se situací, kdy uvedu parametry konstruktoru jako volitelné, protože je to jednodužší na vytvoření objektu, než ho vytvořit nějak, pak zahodit a vytvořit znova. Případ, kdy by bylo vhodné dávat povinné parametry do samostatné metody si nevzpomínám.

Logik

  • *****
  • 1 049
    • Zobrazit profil
    • E-mail
Re: Triedne vs. Prototypové OOP
« Odpověď #43 kdy: 19. 03. 2011, 18:37:57 »
Já netvrdím, že do konstruktoru nepatří nic. Samozřejmě, pokud stav objektu bez někajech hodnot nemá smysl a nejsou ani k dispozici rozumný "defaultní" hodnoty, je asi nejlepší ho inicializovat rovnou v konstruktoru. Takových hodnot ale zase zpravidla nebývá 15.....

Pokud je hodně "povinných" parametrů nějaké funkce či konstruktoru, bývá to většinou proto, že daný objekt(funkce) dělá dvě věci najedou. V tu chvíli je rozumné ten objekt rozdělit na dva (a třeba ten druhý objekt poslat do konstruktoru toho prvního).

omdra.novacisko.cz

Re: Triedne vs. Prototypové OOP
« Odpověď #44 kdy: 19. 03. 2011, 18:58:02 »
Ondra: to je vnořená, ne anonymní třída. To, že něco pojmenuješ anonymní, tak tím ještě anonymní třídu neuděláš :-). Proto jsem psal také, že to jde nejhůř (syntax je rozvleklá, vyžaduje zbytečnej identifikátor a nedá se napsat "inplace", což vše ztěžuje čitelnost programu) a ne že to nejde.

Ona není vnořená, nebo tedy ne v tom typickém smyslu (vnořená třída do třídy), protože je vnořená do funkce a z vnějšího světa prakticky nereferencovatelná. Identifikátor je tam povinný, ale v zásadě nemá širší platnost než uvnitř funkce. Jo, je to větší okecávačka, ale není to limitace. Programátorské postupy z Javy lze tedy i tady uplatnit.

Mimochodem, ani v Javě anonymní třídy nejsou úplně anonymní. Jen mají nějaký náhodný identifikátor. Viz soubory class