ugrás a tartalomhoz

LSP és öröklődés

inf3rno · Okt. 6. (P), 09.48
Olvasgatok a contract-ekről, és felvetődött bennem, hogy valami nem teljesen világos az LSP-vel kapcsolatban.

In, Out and Inheritance

If a function in a derived class overrides a function in its super class, then only one of the in contracts of the function and its base functions must be satisfied. Overriding functions then becomes a process of loosening the in contracts.

A function without an in contract means that any values of the function parameters are allowed. This implies that if any function in an inheritance hierarchy has no in contract, then in contracts on functions overriding it have no useful effect.

Conversely, all of the out contracts need to be satisfied, so overriding functions becomes a processes of tightening the out contracts.



https://dlang.org/spec/contracts.html

Liskov substitution principle[6]
“objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.”


https://en.wikipedia.org/wiki/SOLID_(object-oriented_design)

Na most elvileg mindkettő azt mondja, hogy alosztály példánnyal helyettesíthető kell, hogy legyen az aktuális példány. Ahhoz viszont, hogy az alosztály példány is átmenjen minden teszten, amit az eredeti osztállyal szemben támasztottunk (feltéve, ha elég alaposan teszteljük), az kell, hogy meglévő metódust ne írjunk felül, hanem csak új metódusokat adjunk hozzá. Ha felülírunk egy meglévő metódust, pl kiveszünk egy in contract-et, amit javasolnak a contract-es leírásban, akkor az alosztály elhasal minden olyan teszten, ami az adott in contract ellenőrzését végzi.

pl:

class Range {
    change(x,y){
        assert(x<=y);
        this.limits = [x,y];
    }
}

class BetterRange extends Range {
    change(x,y){
        if (x>y)
            this.limits = [y,x];
        else
            this.limits = [x,y];
    }
}

function testRange(Range){
    var range = new Range();
    try {
        range.change(20,10);
        console.log("range validation failed by class: " + Range.name);
    }
    catch (e){
        console.log(".");
    }
}

testRange(Range); // .
testRange(BetterRange); // range validation failed by class: BetterRange
Feloldható ez az ellentmondás valahogy?
 
1

Esetleg az LSP megfogalmazása

inf3rno · Okt. 6. (P), 10.13
Esetleg az LSP megfogalmazása nem elég precíz, és csak az olyan esetekben van szó szabad helyettesíthetőségről, amelyeknél nem a contract-ek, hanem az algoritmus tesztelése a cél?
2

objects in a program should

smokey · Okt. 6. (P), 10.43
objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.


Ez alapján én úgy értem, hogy maga a forráskód jó, értelmezhető, lefordul, lefut, viszont az eredmény helyessége már egy másik téma.

Mint a strategy pattern.
6

Hát akkor többféleképpen is

inf3rno · Okt. 6. (P), 21.14
Hát akkor többféleképpen is lehet érteni. Én inkább arra gondoltam, hogy az eredmény helyessége is ugyanúgy megmarad az új osztállyal. De ha neked van igazad, akkor sokkal egyszerűbb betartani az LSP-t, mint gondoltam.
9

Ha a strategyt vesszük

smokey · Okt. 7. (Szo), 12.58
Ha a strategyt vesszük alapul, akkor én a gyerek osztályokban megvalósuló, ősben definiált abstract metódusokra külön tesztet írnék, mert itt specifikusan teszteled azt a metódust, ami gyerekenként változhat (hiszen azért nem fejtetted ki az ősben, vagy overridoltad a gyerekben, hogy tudj specifikus működést biztosítani).

Az ősosztályban megvalósuló publikus metódus tesztelésére is készítenék egy külön tesztet, ami bemenetként fogadhatja a gyerek osztályok példányát, hiszen nem gyerek specifikus funkcionalitást tesztelsz, hanem ős funkciót. Ez a teszt pedig azt validálná, hogy a gyerek osztály nem teszi tönkre valamilyen módon az ős funkciót.
11

Ja, így a logikus.

inf3rno · Okt. 7. (Szo), 20.49
Ja, így a logikus.
3

In addition to the signature

Poetro · Okt. 6. (P), 10.51
In addition to the signature requirements, the subtype must meet a number of behavioral conditions. These are detailed in a terminology resembling that of design by contract methodology, leading to some restrictions on how contracts can interact with inheritance:
  • Preconditions cannot be strengthened in a subtype.
  • Postconditions cannot be weakened in a subtype.
  • Invariants of the supertype must be preserved in a subtype.
  • History constraint (the "history rule").


Liskov substitution principle
7

A másik írásban is ugyanez

inf3rno · Okt. 6. (P), 21.17
A másik írásban is ugyanez volt. Ezek szerint nagy valószínűséggel én nem értem, hogy mit mond ki az LSP.
4

Példa

janoszen · Okt. 6. (P), 10.52
Sokkal egyszerűbb példát mondok: van egy téglalap és egy négyzet objektumod. Ha az LSP-t betartod, a négyzet nem származhat a téglalapból, mivel ez esetben a setA és setB függvénynek mellékhatása lenne. Vagyis ugyan matematikai értelemben leszármazottak, OOP értelemben nem.

A fenti példádnál sajnos nem térsz ki arra, hogy a Range-nek mi a feladata üzleti szempontból, de gyanítható, hogy itt mindkét Range megvalósításnak egy közös, absztrakt őstől vagy interface-től kellene származnia, különben kénytelen vagy a Range bugjait és korlátozásait a leszármaztatott osztályban is lekövetni.

A fenti példában látszólag a Range egy bugját / limitációját javítod a leszármaztatott osztályban, ami azért probléma, mert simán lehet, hogy már egy rakás kód épül erre a bugra. Ha most a Te BetterRange osztályodat adod át az eredeti helyett, további bugokat fogsz előhívni, ami megint berheléshez vezet az eredeti hiba javítása helyett, további bugokat okozva, stb.
5

A range-nek nincs semmi

inf3rno · Okt. 6. (P), 19.10
A range-nek nincs semmi funkciója, csak egyszerű példának hoztam fel arra az esetre, hogyha lazítunk az input contract-eken, akkor törnek az ezzel kapcsolatos tesztek az új osztályra, tehát az alosztály példánya nem helyettesíthető be szabadon bármibe, mert nem teljesen ugyanúgy viselkedik. Olvastam nemrég itt egy olyat, hogy az assert-es contract-ek és a sima if+exception-ös megoldások között van valami különbség, de nem teljesen értem a lényegét. https://stackoverflow.com/a/28635533/607033 Lehet, hogy ennek is köze van a témához, és contract-ekre nem is kéne tesztet írni? Írtak olyat is valahol, hogy a releaselt kódból ki kell szedni ezeket a contract ellenőrzéseket (assert), de ha pl egy keretrendszerről van szó, amit mások is használnak tőled függetlenül, akkor ez kimondottan káros lehet, mert nem tudja a fejlesztő, hogy mit rontott el mondjuk a paraméterezésben. Zavaros ez az egész egy kicsit, még olvasgatok, hátha megvilágosodok.

A négyzetes példád elég érdekes. Lehet egyszerre is változtatni a-t és b-t.
class Rectangle {
    size(a,b){
        assert(a>0 && b>0);
        this.a = a;
        this.b = b;
    }
}
Amit érdemes észrevenni ezzel kapcsolatban, hogyha megtartjuk az interface-t, akkor a négyzetnél be lehet szórni plusz egy input contractet.
class Square extends Rectangle {
    size(a,b){
        assert(a === b);
        super.size(a, b);
    }
}
Így a végeredmény (bár nem a legjobb, mégis) teljesen működőképes, annak ellenére, hogy ellentmond a fentebbi állításnak, mert szigorítottunk, ahelyett, hogy lazítottunk volna rajta:

Overriding functions then becomes a process of loosening the in contracts.


Nyilván ha normálisan csináljuk, akkor a Square nem származhat a Rectangle-ből, mert máshogy méretezzük, hanem talán valami absztrakt közös ősből, ha vannak közös metódusaik.
8

Nem

janoszen · Okt. 7. (Szo), 07.00
Szvsz se nem szigorithatod, se nem lazithatod a contractet, mert gondolj bele, ha Te valamire exceptiont dobsz amire az eredeti osztaly nem dobott exceptiont, az ugyanugy nem fog mukodni, mint ha nem dobsz exceptiont olyasmire, amire az eredeti dobott. A hivo fel vele szvsz ugyanazt a mukodest kell biztositanod mint a parent osztaly. De lehet, hogy en ertettem rosszul valamit.
10

Hát pont ez az, hogy azt

inf3rno · Okt. 7. (Szo), 20.47
Hát pont ez az, hogy azt írják mindenhol, hogy a validáláson (precondition) csak lazítani lehet vagy ugyanúgy hagyni, szigorítani nem szabad. A postcondition ellenőrzésen meg szigorítani lehet vagy ugyanúgy hagyni. Viszont erre a precondition lazításra jó lenne valami élő példát látni, hogy mire gondoltak, mert nekem sem áll össze a kép ezzel kapcsolatban.
12

Feltettem a kérdést

inf3rno · Okt. 8. (V), 05.12
Feltettem a kérdést stackexchange-es oldalon is, egyelőre az tűnik a legérdekesebb magyarázatnak, hogy nem értem mit jelent az, hogy "weakening a precondition". A srác totál mást írt, mint amit gondoltam, hogy jelent. Úgy vagyok vele már elég régóta, hogy a kód beszél, mert minden más ontológia, pontos definíciók nélkül nehezen megfogható.
13

Link

janoszen · Okt. 8. (V), 08.10
Link pls!
14

Szégyellős vagyok.

inf3rno · Okt. 8. (V), 16.25
Szégyellős vagyok. https://softwareengineering.stackexchange.com/questions/358757/how-to-losen-input-contracts-by-inheritance Esetleg ha kibogozzátok valamelyik válaszból az igazságot az király lenne. :-) Mindjárt beleolvasok, hogy mit jelent a weakening a precondition. Úgy nézem google alapján, hogy nem csak nekem nem egyértelmű.
15

Szerintem Bart van Ingen

Endyl · Okt. 8. (V), 17.15
Szerintem Bart van Ingen Schenaunak igaza van / viszonylag tisztán leírja, hogy mit kell ezen foglamak alatt érteni.

Ha a kivételdobás az eredmény része, akkor nem definiálhatsz olyan leszármazottat, ami ezen lazítana (továbbá az előfeltételek már a lehető leglazábbak, mivel definiáltad az összes "valid" bemenetet, csak ezek közül némelyikre a "helyes" válasz a kivételdobás).

Ha a kivétel nem az eredmény része, akkor definiálhatsz "értelmes" eredményt az "invalid"-ra, vagy arra, hogy x > y a leszármazottban, csak akkor a tesztjeidet ennek figyelembevételével kell megírnod. Tehát az őst ne teszteld "invalid" vagy x > y bemenetre, mert az nem része a valid előfeltételnek, nem dolga lekezelni/"helyes" működést produkálnia ezekre. A leszármazottat már tesztelheted az ős összes valid inputjára, plusz azokra, amiket esetleg újként bevezetsz.
16

Még nem olvastam, meg most

inf3rno · Okt. 8. (V), 17.47
Még nem olvastam, meg most ezt se tudom elolvasni, mert mennem kell. Később ránézek. Amit igazán hiányolok egy olyan példa, aminél lazítanak a precondition-ön, esetleg szigorítanak a postcondition-ön az alosztályban. Szerintem gyakorlatból sokkal egyszerűbb megtanulni valamit, mint oldalakat rizsázni róla.
17

No én elolvastam ezt: link

inf3rno · Okt. 8. (V), 20.41
No én elolvastam ezt: link egyelőre, ami azt mondja, hogyha A -> B és !(B -> A), akkor A erősebb és B gyengébb feltétel. Hoztam egy egyszerű példát is:

A: x<0
B: x<1


Ha megvizsgáljuk a fenti fogalmakkal A -> B igaz és !(B -> A) is igaz, tehát A erősebb és B gyengébb feltétel. Nézzük meg más nézőpontból is. Legyen xA az olyan x-ek halmaza, amelyekre A igaz és legyen xB az olyan x-ek halmaza, amire B igaz. Ha megnézzük xA részhalmaza xB-nek. Ilyen szempontból tehát jól gondoltam, hogy az előfeltétel lazítása azt jelenti, hogy több minden átmegy a szűrőn.

Az még mindig nem jött át, hogy miért nem jó példa erre az assertion, amit írtam.

A: x !== invalid
B: true


Ez alapján B gyengébb előfeltétel, mint A. No elolvasom mit írtatok.
18

Huhh, nekem még mindig nem

inf3rno · Okt. 8. (V), 20.51
Huhh, nekem még mindig nem jön át. Ha jól értem, amit írtok a kivétel dobás az egyik esetben az eredmény része, a másik esetben meg nem. Nem igazán értem, hogy mi erre a szabály, hogy mikor az és mikor nem. Nem véletlen kértem példát mindenkitől.
19

Közben agyaltam még egy sort

inf3rno · Okt. 9. (H), 06.47
Közben agyaltam még egy sort az LSP-n. Annyi értelmét már látom a dolognak, hogy ezekkel a szabályokkal (pl hogy csak lazább lehet a precondition az alosztályban) azt akadályozzák meg, hogy véletlenül olyan alosztály példányát helyettesítsük be valahova, ami nem kompatibilis az ősosztálynak szánt kóddal. pl:

class Square extends Rectangle {
    dimensions(w,h){
        assert(w == h);
        super.dimensions(w,h);
    }
}

function drawRedRectangle(Canvas c, Rectangle r){
    r.position(100, 100);
    r.dimensions(10, 20);
    r.fill(Colors.red);
    c.draw(r);
}

drawRedRectangle(canvas, new Rectangle()); // ok
drawRedRectangle(canvas, new Square()); // assertion error
Ebben az esetben a Square lazán átmegy a type check-en, mert a Rectangle alosztálya és váratlan hibát okoz futás közben, szemben a Rectangle-el, aminek nincs gondja a függvénnyel. Az ilyen jellegű problémákat hivatott megakadályozni az LSP és a vele kapcsolatos megkötések.

Ennek ismeretében viszonylag egyszerű olyan kódot írni, ami lazít az előfeltételeken és életképes marad, pl

class A {
	x(a){
		assert(a>0);
		return a+1;
	}
}

class B extends A {
	x(a){
		// teljesen kivettük az előfeltételt, a negatív értékekkel is működik
		return a+2;
	}
}

class C extends A {
	x(a){
		assert(a>-10); // lazítottunk az előfeltételen, néhány negatív érték is átment a szűrőn
		return a+3;
	}
}
Ilyen formán B és C példányai is bárhol használhatóak, ahol A példányait használnánk, feltéve, hogy bármilyen szám megfelel eredménynek. Mivel most nem definiáltunk postcondition-t, ezért valószínűleg igen.

Ezért fontos postcondition explicitté tétele is (ahelyett, hogy csak dokumentációban írnánk le), pl:

class A {
	x(a){
		assert(a>0);
		let r = a+1;
		assert(r>0);
		return r;
	}
}
Mert a postcondition-el tesszük egyértelművé, hogy milyen eredményt kell adnia egy metódusnak, és ha mondjuk alosztályban felülírjuk ezt a metódust, akkor van mihez nyúlni a kimenet ellenőrzésénél is. Tehát az alosztály példányait használó kódnál sem lesz probléma a kimenettel, ha betartjuk az LSP-t.

Ami érdekessége a dolognak, hogy pl:
class B extends A {
	x(a){
		// ...
		if (...)
			// ...
		// ...
		super.x(a);
		// ...
	}
}
Az ilyen jellegű kód általában code smell, legalábbis a precondition szigorítása is előfordulhat ilyen mintánál, amit tilt az LSP, lazítani pedig csak valamilyen transzformációval tudunk így a precondition-ön, aminek a kimenete megfelel az ősosztály metódusánál a precondition-nek. Csak egy példa ilyen transzformációra:

class A {
	x(a){
		assert(a>0);
		let r = a+1;
		assert(r>0);
		return r;
	}
}

class B extends A {
	x(a){
		if (a < 0)
			a = Math.abs(a);
		return super.x(a);
	}
}
Így az alosztály is tökéletesen működik a precondition pedig lazult, a negatív számok is használhatóak és csak a nulla tiltott szám.
20

Visszatérve az eredeti

inf3rno · Okt. 9. (H), 20.31
Visszatérve az eredeti kérdésre, nem jó ötlet a base class contract-jeire tesztelni a subclass-eket, mert ezek a contract-ek megváltozhatnak a subclass-ekben. Pl az in-contract-eknél a precondition-ök lazulhatnak, az out-contract-eknél a postcondition-ök szigorodhatnak, és így tovább. Amire tesztelni kell a subclasseseket az a tényleges viselkedés abban az esetben, ha a base class in-contract-jének megfelelő inputot kapnak. Ebben az esetben át kell menniük a base class tesztjein. A contract-ekre vonatkozó teszteket részben át lehet venni, de egy részük szinte biztosan változnak fog, ha változnak a contract-ek. Ha in-contract-en változtatunk, akkor külön tesztek fognak kelleni az olyan inputokra, amik nem voltak benne a base class contract-jében.

Így az eredeti példánál maradva így újrahasznosíthatóak a tesztek:

	class Range {  
		change(x,y){  
			assert(x<=y);  
			this.limits = [x,y];  
		}  
	}  
	  
	class BetterRange extends Range {  
		change(x,y){  
			if (x>y)
				super.change(y,x);
			else  
				super.change(x,y);
		}  
	}  
	  
	function testRangeChangeInContract(){  
		var range = new Range();  
		expect(function (){
			range.change(20,10);  
		}).toThrow();
	}
	
	function testRangeChange(Range){
		var range = new Range();
		range.change(10, 20);
		expect(range.limits).toEqual([10,20]);
	}
	
	function testBetterRangeChange(){
		testRangeChange(BetterRange);
		var range = new BetterRange();  
		range.change(20,10);
		expect(range.limits).toEqual([10,20]);
	}
	
	testRangeChangeInContract();
	testRangeChange(Range);
	testBetterRangeChange();
Ha vannak TestCase osztályok, és nem csak ilyen függvényekkel dolgozunk, akkor azokat is örököltethetjük egymásból, hogy megkönnyítsük az újrahasznosítást. Őszintén szólva már nem emlékszem, hogy a mocha vagy más rendszerek mennyire rugalmasak ilyen szempontból, de úgy rémlik, hogy egyáltalán nem. A BDD rendszerek elég rugalmasak, mert elég csak hozzákötni egy Given-hez a példányosítást, a Then és When részeket meg lehet copy-paste-elni. A feature fájlok úgysem jellemző, hogy a későbbiekben megváltoznának, tehát nem probléma a copy-paste és az ezzel járó duplikáció. A step definition-ök, meg szabadon változtathatóak, azoknál teljes újrahasznosítás van, tehát egy base class változásnál is elég lehet egyetlen step definition-t módosítani. Ez amúgy már a sokadik olyan eset, ahol a DRY nem érvényesül, inkább csak ajánlás az, mint örök érvényű szabály.
21

Felreertes

janoszen · Okt. 11. (Sze), 14.04
Sztem itt a legalapvetobb felreertes: ha megvaltoztatod az eredeti osztaly mukodeset, beleertve a bugok javitasat is, akkor bele fogsz futni olyanokba, hogy valaki csomo helyen if (x instanceof BetterRange) checkeket fog implementalni. Ez pedig torekeny kodhoz fog vezetni.

Alapvetoen sehogy nem uszod meg, hogy az eredeti Range hibas/korlatozott mukodesere epito kodhelyeket megvizsgald es egyesevel kijavitsd. Ha csak siman bepattintasz egy "javito" osztalyt, egy csomo helyen hibas mukodest fogsz eloidezni. Nem ilyen egyszeru peldanal, de egy bonyolultabbnal mar igen.

Vagyis nem marad mas mint hogy csinalj egy kozos os osztalyt / interfacet, abba kiemeld az elvart mukodest, es atird az osszes helyet ahol a korlatozasra/bugra epitesz. Es ez fajni fog.

Nem tudom fancy szakszavakba onteni, mindig is fajt megjegyezni az elmeletet, de a gyakorlat ezt mutatja.
22

Az ilyesmi akkor szokott

inf3rno · Okt. 12. (Cs), 12.22
Az ilyesmi akkor szokott előfordulni, ha nem tartják be az LSP-t, és az új osztály nem kompatibilis a régivel. Így muszáj hozzá specifikus kódot írni. Pont ez az egész értelme, hogy a régi kódhoz ne kelljen hozzányúlni. Nyilván én is az interface használat híve vagyok, de ez most csak lazán kapcsolódik a témához.
23

Nem

janoszen · Okt. 12. (Cs), 13.03
Nem mert a fenti kodod ekes peldaja annak hogy nem kompatibilis a mukodes. :)
24

Ami a Range-nél valid, az

inf3rno · Okt. 13. (P), 07.19
Ami a Range-nél valid, az ugyanúgy működik a másiknál is. Nagyjából ez volt a cél. A maradék extra feature. Szóval ha feltételezzük, hogy valid inputot kap csak, akkor kompatibilis.
25

Nem

janoszen · Okt. 13. (P), 09.05
Pont ez az: invalid inputra is illik azonosan reagalni. Miota programozunk ugy, hogy csak valid inputra keszulunk fel?
26

LSP szerint nem szükséges.

inf3rno · Okt. 13. (P), 12.47
LSP szerint nem szükséges, sőt végülis nem csak inputról van szó, mert a precondition más is lehet, pl ha egy állapotba csak bizonyos feltételekkel lehet eljutni, az is minősülhet precondition-nek. Egyébként érdekes, tudnál olyan helyzetet mondani, aminél invalid inputra készíted a kódot, és később gondot okoz, ha egy új osztály mégis validként értékeli, amit kapott?
27

Pl

janoszen · Okt. 13. (P), 18.58
Simán a hasamra csapva, tegyük fel hogy tudom hogy egy X osztály dob egy InvalidParameterExceptiont ha a bemenet nem felel meg bizonyos kritériumoknak, szóval az input validálási logikában nem írok külön kódot erre. Természetesen dependency injectionnel kapom meg amit meg akarok kapni, így akár ki is cserélheted amit ki akarsz cserélni, ellenben ha a hibás / nem várt értékekre nem kapok exceptiont a program tovább dolgozik a hibás értékekkel.

Persze, a Range egy végletekig leegyszerűsített példa, de komplexebb, nagyobb alkalmazásban bármilyen apró baromságból lehetnek ordas nagy hibák, szóval számomra a tutibiztos módszer az, hogy interface, az interfaceben leírni a kívánt működés dokumentációját, és minden implementáció ehhez tartsa magát. Ha valamilyen külső szoftvert kell használni, akkor azt egy másik osztállyal elfedem és az adott osztályban lekezelem, egségesítem a külső lib baromságait.

Pont semmibe nem kerül, cserébe kevesebb fejfájás egy év múlva.
28

ha a hibás / nem várt

inf3rno · Okt. 14. (Szo), 01.37
ha a hibás / nem várt értékekre nem kapok exceptiont a program tovább dolgozik a hibás értékekkel


Az új osztály szempontjából ezek nem hibás értékek, tud velük dolgozni. Ha nem így lenne, akkor dobna kivételt ő is. Mint már írtam, a topic azzal foglalkozik, hogy az LSP betartásával hogyan lehet helyesen örököltetni. Az interface-ek használata nem oldja meg ezt a problémát, a kettő legalább részben független egymástól. Egy újabb példa ennek bizonyítására: az ősosztály egyik metódusa csak maximum 10 karakteres string-et fogad el, az alosztály azonos metódusa pedig hosszabb string-eket is. Az interface-nél csak a string típusra ellenőrzünk, tehát mindkét esetben átmegy az interface ellenőrzésén a 15 karakteres szöveg, és csak az ősosztály dob kivételt, mert nem képes kezelni, az alosztály gond nélkül dolgozik tovább vele.
29

Nem értem teljesen mi is itt

MadBence · Okt. 15. (V), 14.12
Nem értem teljesen mi is itt az ellentmondás, LSP-nél arról van szó, hogy a prekondíciók gyengíthetők, azaz a BetterRange teljesen megfelel az LSP követelményeinek (hiszen meg van engedve, hogy fordítva kapja meg az argumentumokat). Ahol Range működik, ott a BetterRange is működni fog, arról viszont sehol sincs szó, hogy ahol a Range nem működik, ott a BetterRange mit fog csinálni.

Ha ez utóbbira is lenne megkötés, akkor tök értelmetlen lenne öröklődésről beszélni, hiszen az pont arra van használva, hogy egy objektum viselkedését módosítsuk (erre pedig nem lenne szükség, ha kikötjük, hogy a két objektum viselkedésének meg kell egyeznie minden esetben). Ilyenkor egy interfész két implementációjáról lehet maximum szó (mindkét implementáció pontosan ugyanúgy viselkedik, így nyilván a kódban felcserélhetőek).
30

Ja, menetközben rájöttem én

inf3rno · Okt. 15. (V), 19.39
Ja, menetközben rájöttem én is, hogy az alosztályt más prekondíciókra kell tesztelni, mint az ős osztályt. Szóval az a része a teszteknek nem (vagy csak részben) újrahasznosítható, ahol invalid értéket adsz át, ellentétben a valid inputos résszel.