S.O.L.I.D. elvek betartása?

OOP környezetben, kulturált körökben, mennyire szokás betartani a SOLID elveket? (https://en.wikipedia.org/wiki/SOLID_(object-oriented_design))

Jobb ragaszkodni hozzájuk vagy ez csak egy a sok lehetséges útból?
Valamikor évekkel ezelőtt nagy hévvel belevetettem magam a tanulásba, a lelkesedés kitartott pár hónapig, azóta kb. mindent elfelejtettem belőle, viszont egy emlékem megmaradt: rengeteg tutorialban találtam olyan mintákat, amik az én értelmezésem szerint durván felrúgták ezen elvek némelyikét.
Pl. itt van egy: https://docs.python.org/3/library/tkinter.html - 25.1.2.2 A Simple Hello World Program

Fejből nem tudom, hogy konkrétan az öt betűből melyikkel ellenkezik, majd hozzászólásban pótolni fogom, ha szükséges, csak annyira emlékszem, hogy egyik osztályból másikat származtatni csak úgy szabad, hogy a gyerek kompatibilis maradjon a szülőjével, olyan helyen, ahol a szülőt használhatom, a gyerek is használható kell legyen.
Az összes eddig látott példák felrúgják ezt a szabályt, ha jól értem a dolgot, már azzal, hogy a konstruktor (a fenti példában az __init__ metódus) paraméterezése eltér a szülőétől.
Én értelmezek rosszul valamit és a példa mégis korrekt?
Akik ezeket a példákat elkövették, nem törődtek ezekkel az elvekkel?

Csak azért zavar, mert ha már használni próbálok valamit, azt jó lenne korrekt módon... működni működik...

Hozzászólások

A konstruktor eltérése lényegtelen. Hisz ahol példányosítod az adott osztályt, ott pontosan tudod, milyen típusról van szó. A lényeg a viselkedése. Ez egyébként a Liskov principle, az L. Ezt az elvet talán a legnehezebb mindig betartani, vagyis inkább mondom úgy, hogy ez van talán a legtöbbször felrúgva. Ugye egy új exception típus dobásával is könnyen megsérthető.

Igen, illik betartani ezeket az elveket. Ez nem öncélú dolog. Aki minőségi kódot ad ki a kezéből, az az én definícióm szerint eleve betartja ezeket, mert ezek szükségesek ahhoz, hogy könnyen tesztelhető és karbantartható legyen a kód.

Ne felejtsek el visszatérni rá. Most nem tudok kellőképp ellenőrzött forrást, de bennem az maradt meg, hogy a leszármazottat bárhová be kell tudjam helyettesíteni a szülő helyére, olyan megkötés nélkül, hogy úgy is tudom, mit használok. Mert ugye egy leszármazott lehet mondjuk egy API része is, amit én írok, de nem én fogom felhasználni.

Persze lehet, hogy alapjaiban értem félre.
Több helyen olvasom ugyanazt, amit te is írtál, viszont így az itt említett API-s példám esetében ez gázos lehet. Én úgy képzeltem ezt a dolgot, hogy bővíteni lehet rajta úgy, hogy az eredeti működését ne befolyásolja, de szűkíteni már nem illendő.
Ugyanakkor meg sok fórumon találtam olyan véleményt, hogy pont a konstruktorra nem érvényes a LSP...

Kicsit továbbgondolva az egészet: az eredetileg említett példában egy GUI létrehozásakor használok fel kész osztályokat a tkinter package-ből. Végső soron az eredeti működésük nem változik, hiszen saját metódusokat adok hozzá, egyetlen kivétel az a nyomorult konstruktor.
Viszont... ha bármi mást felülírok (pl. metódusokat) ... azzal már nagyon kell vigyázni, hogy ne szűkítsek rajtuk, max. bővítsem a metódusok működését, ráadásul ha paraméterezhetőek, akkor paramétert is csak olyat vehetek fel hozzájuk, aminek van default értéke. Ha jól értem így sokadik olvasatra...

A ketto nem teljesen ugyan az.

Nagyon retardalt pelda, de van a Human osztaly, es a rajta ertelmezett talk() metodus.
Van egy szuper-hacker fejleszto, akinek be kell vezetnie egy uj karaktert, a kutyat. Hat szarmaztassuk le a Human osztalybol, mert hat "ha nagyon messzirol nezzuk, akkor a kutya is beszel". Szepen leimplementalja a Dog.talk() fuggvenyt, mert egyszeru volt a Human-bol szarmaztatni, es visszaadja a "vau" stringet.

Ez mind szep es jo, de nem vettuk figyelembe, mit is szeretne a kliens. Ha a kliens esetleg egy Sims jellegu jatek, akkor innentol kezdve az emberek tudnak a kutyaval beszelgetni, ami megsertheti a LSP-t (ha nem az volt a cel, hogy a kutya is beszeljen :) ).

Amit e helyett lehetett volna tenni, az az, hogy kiemeli egy interfeszbe a kozos metodusokat (ISP+DIP), es megirja a Dog osztalyt say() nelkul.
Ez persze rengeteg munka, de kesobb megeri.
---
Lambda calculus puts the fun into functional programming

Szerintem kicsit félreérted: A lényeg az, hogy ha van egy referencia egy objektumra, akkor úgy tudjuk használni korrekten az a referenciát, hogy ne kelljen tudni az objektum konkrét típusát, mert tudjuk, hogy az összes lehetséges leszármazott ugyanúgy viselkedik. Tehát ebből a szempontból a konstruktor teljesen irreleváns, ugyanis ott tudjuk, hogy mit akarunk épp létrehozni.
Sajnos a python elég lazán bánik a leszármazással és a függvény override-okkal és ezért nehéz megtalálni a helyes utat, de a leszármazott típusban semmilyen módon nem szabad az ős publikus függvényeinek a visszatérési típusán vagy paraméterein változtatni (se hozzáadni se elvenni), legtöbb nyelven erre nincs is lehetőség.
De igazából ha már itt járunk, akkor az Open/Closed principle is idevág, ami szerint úgy kell megtervezni az osztályokat, hogy csak bővíthetőek legyenek, a működésüket ne lehessen módosítani leszármazással.

Amúgy a SOLID elveknek a lényege az, hogy egyrészt jól karbantartható kód szülessen, másrészt, hogy az olvasó számára minél könnyebben érthető legyen, azaz ne tartogasson semmilyen meglepetést a kód. Ha egy adott osztály és a leszármazott függvényének a paramétereinek száma más, az véleményem szerint okozhat meglepetéseket.

Ezt próbálom emészteni.
Sajnos amikor először hallottam OOP témáról, akkor csak odab...ták, hogy nézd milyen csodás újdonság (Turbo Pascal 5.5 asszem) és akkor elég sok hülyeség belémrögzült, utána meg egy pár évvel ezelőtti újabb agymenésig egyáltalán nem érdekelt a téma.

Attól függetlenül, hogy a jelek szerint teljesen félreértettem a LSP-t, az a python kód, a jelenlegi formájában nem rúgja fel az OOP alapelveket?

Az LSP mindig attol fugg, hogy milyen szinten nezzuk a kodot.
Van a "high level policy" (mit csinaljon a program) es a "low level detail" (hogyan csinalja azt amit csinal). Minden modulnak mast es mast fog jelenteni ez a ket szint.

Peldaul a tutorial, amit megadtal annal a "high level policy" az az, hogy bemutassa mukodes kozben az eszkozt. Ehhez neked meg kell hivnod a programot (Ez a neked kiajanlott API). Ennek teljesen megfelel, mert te, mint "kliens", kepes vagy meghivni a scriptet, es felmerni ez alapjan, hogy szeretned-e hasznalni, vagy sem.
Minden mas, az "low level detail". Akar egy jatekot is implementalhattak volna.

OOP szempontjabol teljesen OK-nak tunik nekem a kod, mert van egy osztalyod, amibol szarmaztattal, es felulirtad az egyik kiajanlott fuggvenyt, hogy azt csinalja, amit szeretnel.
Ez lenyegeben az OCP egyik megoldasa. Masik lehetoseg a delegacio, amikor letrehozol egy osztalyt, ami megvalosit egy interfeszt, es atadod parameterkent valaminek.
Hasonlo a mukodes, de mas az elv :)

Amiben lehet finomitani, az a public/private hasznalata az osztalyban (egy alahuzas/ket alahuzas). Amennyire borzaszto a python-ban ezeknek a hasznalata, kb annyira fontos is.
---
Lambda calculus puts the fun into functional programming

Így van. A konstruktoros dologhoz meg: az LSP nem azt mondja, hogy egy gyerekosztály *minden* metódusának meg kell egyeznie a szülőosztály *minden* metódusával, hanem azt, hogy ahol a szülőosztály egy példánya szerepelhet, ott a gyerekosztály egy példánya is szerepelhet anélkül, hogy a program viselkedése megváltozna.

> ahol a szülőosztály egy példánya szerepelhet, ott a gyerekosztály egy példánya is szerepelhet anélkül, hogy a program viselkedése megváltozna

Ezzel nem teljesen ertek egyet, bar ertem a szandekot. A gyerek osztaly akar gyokeresen ellentetes dolgokat is muvelhet, ha a kliens fele a kliens szamara elvart ertekeket adja vissza.

Csak vegyuk szemugyre a kulonbozo mock osztalyokat. Lenyegeben nem csinalnak semmit, es megis behelyettesithetok az adott rendszerbe, mert az eredeti logika alapjan viselkednek.

Megvaltozott a viselkedes? Igen, hiszen ahol eddig tenylegesen vegrehajtottunk valamilyen feladatot, ott most nem csinalunk semmit.
Behelyettesitheto volt az osztaly? Igen, mert a kliens szempontjabol hasonloan viselkedett.

Hasonlo a helyzet a kulonbozo "adatbazisokkal". Mi a feladat? "Mentsd el, olvasd ki, torold le, valtoztasd meg".
Teljesen mindegy, hogy ez memoriaban, fajlban, egy REST API-n keresztul vagy noSQL-ben tortenik, ha a "high level policy"-t megvalositja, akkor mar boldogak vagyunk.
Teljesen ellentetes a program viselkedesenek szempontjabol, es megis helyettesithetok. :)
---
Lambda calculus puts the fun into functional programming

Valoban egy kicsit rosszul fejeztem ki magam.

Amire utaltam, hogy a felhasznalo fele a parancs meghivasa az "interfesz" amit implementalni kell, nem egy osztaly, amit mondjuk polimorf modon be lehet illeszteni egy listaba. Hasonloan egy REST API is lehet "interfesz".

A klienstol fugg, hogy mit kell csinalnia a szolgaltatasnak, amit hasznal. Az LSP azt mondja ki, hogy ha vannak alternativ implementacioid egy feladat megoldasara, akkor azokat ugy tudd beilleszteni a rendszerbe, hogy a kliensen nem valtoztatsz.
---
Lambda calculus puts the fun into functional programming

"Az LSP azt mondja ki, hogy ha vannak alternativ implementacioid egy feladat megoldasara, akkor azokat ugy tudd beilleszteni a rendszerbe, hogy a kliensen nem valtoztatsz."

Ez a sima öröklődés.
Az LSP azt mondja ki, hogy a leszármazottakat bárhol használhatod, ahol az őst és a programod továbbra is helyesen fog működni.

Erre igen jó példa a Rectangle ős, aminek van setWidth és setHeight metódusa. Ha ebből származtatsz egy Square osztályt, akkor az meg fogja sérteni az LSP-t, mivel a setWidth és a setHeight mind a szélességet, mind a magasságot változtatja.
Tehát pl. ebben a kis kódban:


rectangle.setWidth(5);
rectangle.setHeight(10);
assert rectangle.getArea() == 50;

hibát fogunk kapni Square esetén, mivel az 10*10, azaz 100-at fog visszaadni.

Vagyis valtoztatnod kell a kliensen (assert sorok) ahhoz, hogy kepes legyel hasznalni a Square objektumodat.

Nem latom, hogy ezzel a peldaval melyik pontjat akartad megcafolni a mondanivalomnak.

Szerintem ugyan azt mondjuk maskepp megfogalmazva.
---
Lambda calculus puts the fun into functional programming

Mivel az új objektumosztályodról rajtad kívül senki más nem tud, így a konstruktorait nem is tudja más meghívni rajtad kívül. Ergó nem vesztesz semmit a konstruktorok inkompatibilitásával, hiszen aki a régi konstruktorra számít, az eleve sosem fogja meghívni a te új konstruktoraidat -> tehát nem sértetted meg az elvet.

Igazából amúgy az a dolog kulcsa, hogy a konstruktor nem virtuális saját jogán.

Erre vannak alternatív megoldások, és ott igaz az, amit írsz:
- Javaban van reflection, ami igazából ugye egy orbitális hack, viszont itt igaz szokott lenni, van valami elvárás a konstruktorra, és azt be kell tartani (pl. legyen default konstruktor üres paraméterlistával),
- Factory pattern, itt ugye a Factory ad egy metódust, és annak a paraméterezése kéne fix legyen, aztán hogy a konkrét implementáció milyen konstruktort és hogyan hív meg a háttérben, azt már nem kell látnia az API felhasználójának, és ezzel el is fedted a konstruktor paramétereinek eltérőségét - elég, ha a Factory interfésze fix marad.

A tutorial példák érthetetlenül bonyolultak lennének, ha az összes létező kibaszott patternt meg elvet betartanák.

A tutorial meg az example lényege az _adott_ basz bemutatása.

Példa: man 3 rand
Látod, hogy előbb egy srand(), majd n darab rand() hívás.

Az, hogy te hol és hogyan hívod meg az srand()-ot, az tökmindegy, amíg az első rand() előtt megtörténik.
Te a rand() manualját nézed, nem a mittoménmilyen statikus library inicializálás patternjét.
Érted, na.

"És akkor azt illik korrektül leírni."

Példakódot, tutiralt, leírást, stb. úgy érdemes írni, hogy abból kiderüljön, hogy hogyan kell használni a dolgot, vagy mi a probléma valamivel, mire kell figyelni, stb. Ilyenkor a lényeg az adott komponens, nem a körítés. Ilyenkor a patternek betartása még árthat is, mert nem látod a lényeget. Viszont egy rendes alkalmazásnál már kell hozzá a keret, pattern, hibakezelés, stb., mert attól lesz robosztus vagy karbantartható.

Konkrét példa: ha meg akarod mutatni, hogy hogyan kell csatlakozni egy adatbázishoz és hogyan hajtasz végre egy queryt egy DB libbel, engem nem érdekel, hogy hogyan kell egy komplett MVC frameworkot felsetupolni.

----------------
Lvl86 Troll, "hobbifejlesztő" - Think Wishfully™

Ez általában úgy működik hogy előbb megismered az adott libraryt vagy frameworkot és amikor már eligazodsz benne akkor jön hogy úgy tervezed meg a programod és úgy használod a lib/framework szolgáltatásait hogy az eredmény SOLID legyen.

===============================================================================
// Hocus Pocus, grab the focus
winSetFocus(...)

http://c2.com/cgi/wiki?FunnyThingsSeenInSourceCodeAndDocumentation

Nincs totalis ellentmondas, de valoban van ott egy kis nehezseg, ahol te mondod:

Az a helyettesithetoseg, amirol te beszelsz, az nagyon sok nyelvben a polimorfizmussal kerul megvalositasra. Tehat ugyanannak a metodusnak kulonbozo implementacioi vannak, ami egy adott esetben egy adott referencia eseten a referencia dinamikus tipusaval kerul meghivasra. Ez mondjuk a Java-ban es C++-ban ugye a virtualis metodus (vagy ugy generikusan a dynamic dispatch, amit egy vtable segitsegevel valositanak meg). Ha ezeket a metodusok szignaturajabol parat osszefogsz, akkor van egy interfeszed, amire lehet ilyen szerzodeskent tekinteni, aminek objektumorientalt kornyezetben szoktak orulni.

De. Es itt jon a problema, amit te is eszrevettel: a konstruktor pl. nem lehet virtualis. Ez azert van, mert ha nincs objektum -> nincs vtable -> nincs dynamic dispatch. Igy a konstruktor nem tud resze lenni interfesznek (legalabbis pl. Java-ban, C++-ban, C#-ban stb). Tehat itt jon be, hogy erre is ki kell talalni valamit. C++-ban peldaul ha minden igaz a c++20-as szabvanyban vezetik be a concepteket, amivel mar lehet megkoteseket tenni a konstruktorokra is (hogy ez mennyire fog jol integralodni a polimorfikus fuggvenyeken alapulo implementacios strategiakkal, azt nem tudom). Java-ban nem vagyok annyira jaratos, de ott az egyik workaround erre a temara a mindenfele Factory-k hasznalata. Tehat ahol szukseg lenne polimorfikusan letrehozhato objektumokra ott bepasszolnak egy Factory-t, aminek van egy virtualis Create(), Make() vagy mittudomenmilyen fuggvenye, es az majd gyartja az objektumokat (tobbe-kevesbe) uniform modon.