Dette indlæg er alene udtryk for skribentens egen holdning.

OO dur ikke til unit test

27. maj 2008 kl. 11:2941
Artiklen er ældre end 30 dage

I en artikel i dag, fremgår det, at unit test tilsyneladende ikke er sa populært, som man troede, og slet ikke som al den hype, der har været om metoden giver indtryk af. Jeg tror jeg har en delvis forklaring på dette: Unit test og objektorienteret programmering passer ganske enkelt dårligt sammen.

Groft sagt går unit test ud på at teste enkelte klasser/metoder i isolation, men almindelig OO programmering gør dette vanskeligt:

  1. Klasser har ofte "skjult" tilstand i form af felter, der ikke kan tilgås direkte, men kun indirekte gennem metoder, der ikke direkte sætter eller henter værdien, men måske laver test og beregninger inden. Det betyder dels, at det synlige resultat af et metodekald kan afhænge af usynlig tilstand, og at usynlig tilstand kan blive ændret af metodekald. Det giver en ikke-triviel interaktion mellem metoder og tilstand, der gør det vanskeligt at teste dem i isolation og gør det vanskeligt at afgøre, om den skjulte tilstand er ændret efter hensigten.
  2. På grund af nedarvning kan argumenter til en metode have en anden type end specificeret i metodens erklæring, med deraf følgende ændret opførsel af argumentobjektets metoder. Det betyder, at unit test bør teste en metode med argumenter af alle kombinationer af alle mulige subtyper til de erklærede typer. Det er specielt vanskeligt i situationer, hvor man endnu ikke ved, hvilke typer nedarver fra argumenttypen.

Disse to egenskaber er kendetegnende ved traditionel OO programmering, og både skjult tilstand og nedarvning betragtes som god programmeringsskik i de fleste lærebøger om OO programmering. Men de gør det særdeles vanskeligt at designe unit tests, som både giver en bare nogenlunde dækning, og hvor man rent faktisk kan konkludere nogetsomhelst ud fra testens resultat.

Anderledes forholder det sig med polymorft typede funktionelle sprog. Her er resultatet af et funktionskald kun afhængig af argumenterne, så det er nemt at se, om et testtilfælde giver det ønskede resultat. Samtidig giver parametrisk polymorfi garanti for uafhængighed af instantieringen af de polymorfe typer: Groft sagt kan man sige, at hvis en polymorf funktion virker på en instantiering af de polymorfe typer, så fungerer den på alle instanser. Derfor er det ikke nødvendigt at teste på alle mulige instanser.

Artiklen fortsætter efter annoncen

Nogle funktionelle sprog (f.eks. SML) tillader sideeffekter, der gør funktioners resultater afhængige af mere end argumenterne, men god kodestil i funktionsprogrammering er at bruge sideeffekter så lidt som muligt (i modsætning til OO programmering), så langt de fleste funktioner lader sig nemt unit teste.

Andre funktionsprogrammeringsprog (f.eks. Haskell) udvider parametrisk polymorfi med bounded polymorphism, som ikke har samme polymorfe uafhængighed som omtalt herover. Men selv her kan typen bruges til automatisk generering af testtilfælde.

Så selv om unit test er blevet hyped i OO miljør, så passer det ekstremt dårligt der, og ekstremt godt i funktionsprogrammeringsmiljøer

41 kommentarer.  Hop til debatten
Denne artikel er gratis...

...men det er dyrt at lave god journalistik. Derfor beder vi dig overveje at tegne abonnement på Version2.

Digitaliseringen buldrer derudaf, og it-folkene tegner fremtidens Danmark. Derfor er det vigtigere end nogensinde med et kvalificeret bud på, hvordan it bedst kan være med til at udvikle det danske samfund og erhvervsliv.

Og der har aldrig været mere akut brug for en kritisk vagthund, der råber op, når der tages forkerte it-beslutninger.

Den rolle har Version2 indtaget siden 2006 - og det bliver vi ved med.

Debatten
Log ind eller opret en bruger for at deltage i debatten.
settingsDebatindstillinger
1
27. maj 2008 kl. 13:36

Unit test af en klasse passer da perfekt med at man via tests specificerer den kontrakt, som klassen har med omverdenen. Dette gøres ganske naturligt via klassens public metoder, og man sikrer sig på samme måde via disse at klassen opfører sig som forventet.

Mht. polymorfe argumenter, så passer det jo OGSÅ perfekt, idet man blot fremstiller en mock-implementation til at teste med. Hvis man så ellers kan finde ud af at bruge visitor-mønsteret i tilfælde af at forskellige specialiseringer skal behandles forskelligt, så kører det jo derudaf!

Efter min mening så bliver designet helt automatisk bedre og mere OO af at blive skrevet til test.

7
27. maj 2008 kl. 14:47

Mogens skriver om kontrakter med omverdenen. Min pointe er netop, at disse kontrakter bliver overordentlig komplicerede at specificere og specielt at teste, når der er lokal tilstand i en klasse, og hvor man ikke kan sikre, at argumenterne har konsistent opførsel (da man altid kan give et argument, der er en underklasse med en helt anderledes opførsel).

Jeg forstår til gengæld ikke helt pointen om mock-implementeringer. Mener du virkelig, at en test med en enkelt mock-implementering af en argumenttype sikrer, at klassen virker med alle mulige nedarvninger af argumenttyperne?

Jeg er til gengæld helt enig i, at design bliver bedre af at være skrevet til test. Jeg mener så ikke, at det medfører, at det bliver mere OO -- tværtimod. :-)

8
27. maj 2008 kl. 14:58

Mener du virkelig, at en test med en enkelt mock-implementering af en argumenttype sikrer, at klassen virker med alle mulige nedarvninger af argumenttyperne?

Jeg mener at ENTEN er det ikke nødvendigt at bruge arv og specialiseringer, eller OGSÅ er det bare en del af din klasses kontrakt at den kalder noget på din argument-klasse. Og det kan du teste at den gør.

Så er det op til de unit tests du har af hver argument-type at verificere at de overholder deres del af kontrakten.

9
27. maj 2008 kl. 15:08

Mogens skriver:

"Så er det op til de unit tests du har af hver argument-type at verificere at de overholder deres del af kontrakten."

Det vil sige, at den, der skal lave unit tests for en klasse skal kende kontrakterne for alle de metoder, hvor denne klasse kan bruges som argument.

Det lyder ikke længere som test af hver klasse/metode i isolation, som ellers er en væsentlig del af filosofien i unit test.

10
27. maj 2008 kl. 15:23

Det vil sige, at den, der skal lave unit tests for en klasse skal kende kontrakterne for alle de metoder, hvor denne klasse kan bruges som argument.

??? Det forstår jeg ikke. Tag f.eks det her:

1: A, givet en B, lover at kalde foo() derpå.

2: C er en B og har en implementation af foo(), der fungerer.

Hver af disse udsagn kan testes i hver deres unit test.

Hvad de i fællesskab kan udføre når de kobles sammen er et andet spørgsmål. Det kan man teste med integrationstest.

12
27. maj 2008 kl. 15:44

"1: A, givet en B, lover at kalde foo() derpå.

2: C er en B og har en implementation af foo(), der fungerer.

Hver af disse udsagn kan testes i hver deres unit test."

Det forudsætter, at alle klasser (f.eks. A), der bruger B, har samme forventninger til foo(). Det sætter meget strenge begrænsninger på, hvad en nedarvning kan lave om på samt i hvilke kontekster, B kan bruges (da de alle skal forvente præcis det samme af foo()).

Men jeg giver dig ret i, at når dette er tilfældet, så er unit test gennemførligt (forudsat, at hverken A eller B har for meget skjult tilstand).

14
27. maj 2008 kl. 16:04

(forudsat, at hverken A eller B har for meget skjult tilstand)

Mængden af "skjult tilstand" er vel underordnet, hvis bare klassen opfører sig som den lover. På den måde opnår man indkapsling.

Se DET er OO! Og DET kan testes :-)

13
27. maj 2008 kl. 16:01

Jeg tror mange forveksler unittest med gammeldags whitebox test, hvor den interne logik i en funktion blev aftestet. Unittest er netop kendetegnet ved, at den ikke tester intern logik, men udelukkende ser på kontekst hvori unit indgår. Det kan derfor bedst sammenlignes med gammel blackbox test, dog på et lavere abstraktionsniveau.

Der testes altså på, hvilket output unit producerer i forhold til det modtagne input. Intern tilstand og polymorph egenskabs påvirkning af output er derfor ikke relevant for en unittest, fsv. dette ikke har indflydelse på output.

Jeg ville mere betragte unittest som en avanceret form for grænseværditest.

15
27. maj 2008 kl. 16:25

Ja, det er netop pointen. Du tester kun udefra, og kan derfor ikke direkte observere den interne tilstand -- hverken før eller efter testen.

Derfor kan en test med et givet argument og resultat, der opfylder kontrakten, ikke garantere, at samme argumenter også næste gang giver et resultat, der opfylder kontrakten. Da unit tests som regel starter i en veldefineret initialtilstand, tester de ikke metodernes validitet i et program, der har kørt et stykke tid og derfor ikke længere har denne veldefinerede initialtilstand. Du har ret i, at intern tilstand er irrelevant, hvis den ikke påvirker det returnerede resultat. Men hvordan tester du det i en ren ekstern test, hvor du ikke kan sætte eller observere den skjulte tilstand? Og hvis den skjulte tilstand ikke påvirker det observerbare resultat, hvorfor så have den i det hele taget?

Det er i modsætning til rene funktioner, hvor samme argumenter altid giver samme resultat, så testens validitet er uafhængig af programmets samlede tilstand.

Din kommentar om polymorfe egenskaber forstår jeg kanske enkelt ikke. Pointen er jo netop, at parametrisk polymorfi sikrer, at instantieringen ikke påvirker funktionens virkemåde, så en test, hvor man instantierer den polymorfe argumenttype til heltal, også sikrer validitet for det tilfælde, hvor man instantierer den polymorfe argumenttype til f.eks. funktioner fra strings til lister af strings. Den slags garantier har du ikke ved nedarvning -- du bliver nødt til eksplicit at formulere, hvilke krav du har til argumenttypen og teste disse krav for hver mulig instantiering (underklasse) af argumenttypen.

16
27. maj 2008 kl. 16:42

Derfor kan en test med et givet argument og resultat, der opfylder kontrakten, ikke garantere, at samme argumenter også næste gang giver et resultat, der opfylder kontrakten.

Som generel betragtning er det her naturligvis korrekt - man kan jo aldrig vide, om klassen "husker" f.eks hvor mange gange getLength() er blevet kaldet, for den 42. gang at returnere noget forkert.

Men som regel vil man jo selv skrive sine unit tests, og dermed vil man formentlig teste koden rimelig fornuftigt - f.eks i noget, som man skønner kunne være vanskelige situationer: grænsetilfælde, uventet input, osv.

Hvis man så en dag finder ud af at klassen opfører sig uventet med en given tilstand, så er det jo bare at genskabe tilstanden og fejlen via en test, og så kode til verden bliver grøn igen.

17
27. maj 2008 kl. 17:18

Hvis unit tests bliver skrevet med kendskab til implementeringen, så er det ikke længere black box.

Ideelt set skal unit tests skrives af en anden end implementatoren, kun med kontrakten som udgangspunkt -- ellers vil testen ofte have de samme underforståede (og potentielt forkerte) antagelser som implementeringen.

19
28. maj 2008 kl. 09:35

"Hvis unit tests bliver skrevet med kendskab til implementeringen, så er det ikke længere black box."

Nej, skal det absolut være det? "Rød-grøn-refactor"-cyklen gør at implementationen udvikler sig i takt med at unit-test-suiten udvikler sig, så jeg synes ikke man kan betragte unit-tests som black-box-tests. Det er med at finde den rigtige balance.

"Ideelt set skal unit tests skrives af en anden end implementatoren"

Det var da en pudsig opfattelse. Det er sundt at få reviewet sine unit-tests af andre (ligesom man også bør få al anden kode reviewet af andre), men man bør skrive sine unit-tests ud fra "rød-grøn-refactor"-devisen. Så giver de ganske enkelt mest værdi, da man så "up front" undgår at skrive en masse fejl som andre (og en selv) kommer til at lide under sekundet efter du har committet koden til dit kode-repository.

Og mht. argumenterne om skjult tilstand osv.: Hvad værdi giver kode som måske er meget pæn i din editor, men hvor du ikke aner om det virker? Har du for megen skjult tilstand i dine objekter, så refaktorér til noget mere gennemskueligt.

18
27. maj 2008 kl. 21:59

man kan jo selv bestemme hvad man vil bruge sine unit tests til. Jeg er selv meget tilhænger af testdriven development hvor udviklerne laver testene samtidigt med de laver selve koden således at koden bliver testbar.

At objekter kan have en skjult tilstand gør ikke unit tests ubrugelige. Hvis man mener den er relevant for sin unit test bliver man nødt til at åbne op for klassen for at den er testbar.

Nedarvning har ingen relevans. Man bliver nødt til at fokusere på hvad det egentligt er man tester. Man er nødt til at fokusere på det niveau der opereres på og ikke teste for alle mulige nedarvninger af en parametre. Nedarvninger synes jeg man skal teste for sig selv i en anden kontekst.

11
27. maj 2008 kl. 15:41

Hele ideen med unit-test og (s|r)spec, er at teste ydersiden (og kun ydersiden) af et eller flere objekt(er). Hvad der sker indeni er et udelukkende et implementationsspørgsmål. Dette falder rigtig godt i hak med test-first filosofien - først laves objektets specifikation (testen), og så laves implementationen (koden der får testen til at gå i grøn).

Hvis ikke man er i stand til at beskrive hvorledes objekterne skal opføre sig i enhver tænkelig situation (yderside situation), så har man meget større problemer end manglende unit-tests.

3
27. maj 2008 kl. 14:08

Det er vel et fundamentalt valg om man bare vil teste en klasses ydre (public funktioner) eller også fortage test af indre metoder og tilsande (private).

Klart giver den sidste metode mere arbejde, og bryder ind i en klasses flow på en ikke-intuitiv måde. Det er derfor ret besværligt at gøre konsistent...så jeg holder på almindelig test af ydre; der er simplere, og indmaden i klassen kan ændre uden at det i princippet går ud over test-case'ene.

Et andet spørgsmål er så hvad man mener med OO, når nu Tania skriver at Java guidelines fraråder nedarvning...et argument jeg egentlig godt kan følge.

Er det egentlig ikke OO der er problemet og ikke unittest?

6
27. maj 2008 kl. 14:41

Jo, Carsten, problemet er OO og ikke unit test. Prøv lige at læs titlen igen. :-)

20
28. maj 2008 kl. 10:04

Jo, Carsten, problemet er OO og ikke unit test. Prøv lige at læs titlen igen. :-)

He, ja det står jo ligesom også i overskriften :-) Interessant er, at vi når samme konklusion!

mvh .carsten

21
28. maj 2008 kl. 11:38

Jeg tror at en del har svært ved at se hvorfor objekt-orientered sprog ikke er så velegnet til unit-testing som funktionelle sprog, fordi ikke kender til alle fordelene ved testing i et funktionelt miljø. Jeg har fundet en video med en af Haskell-"bagmændende" (Simon Peyton-Jones), der omhandler netop dette:

http://www.foomongers.org.uk/videos/spj-typedriventestinginhaskell.html

Den er primært rettet mod folk der ikke kender til funktionsprogrammering. Det vigtigste at få med er nok hans eksempel med test af en SMS-encoder (der pakker 7-bit tegn ned i 8-bit bytes). Se den :-)

22
28. maj 2008 kl. 21:19

I har ikke fået mig overbevist om at jeg skal droppe OO til fordel for funktion orienteret programmering. OO giver en enorm fleksibilitet bla. vha. nedarvning. Det er f.eks. helt åbentlyst at bruge OO til at kunne dele en opgave ud på flere personer der så hver især kan udvikle dele af applikationen, unit teste den og til sidst sætte den sammen uden at skulle ændre noget i de dele der allerede er unit testet.

Det er fair nok at i er glade for funktions sprog, men det gør ikke nødvendigvis OO uegnet til f.eks. at unit teste.

23
29. maj 2008 kl. 10:19

Rune skriver:

"Det er f.eks. helt åbentlyst at bruge OO til at kunne dele en opgave ud på flere personer der så hver især kan udvikle dele af applikationen, unit teste den og til sidst sætte den sammen uden at skulle ændre noget i de dele der allerede er unit testet."

Bevares, det kan man godt slippe af sted med i OO, men netop det du nævner, er en af de ting, hvor funktionsprogrammering er en klar vinder: Programmer er generelt mere kompositionelle, fordi de har renere grænseflader (der klart fremgår af typerne).

24
29. maj 2008 kl. 13:32

Min opfattelse fra ovenstaaende diskusion er desvaerre at flere folk betragter unittest som en metode til at naa korrekte prgrammer. Men det er logiskt at tests ikke viser om hvorvidt kode er korrekt eller ej, med mindre at alle kombinationer er afproevet. En af de foerste artikler http://www.google.dk/search?q=structure+test+software er http://www.ece.cmu.edu/~koopman/des_s99/sw_testing/Har hurtigt skimtet denne og mener at der her er tale om et fornuftigt syn paa situationen. Hvis kontrakt orienteret udvikling/design er saa meget i folks bevidsthed, burde flere vaerdsaette Ada's struktur: *.ads og *.adb (Ada specification og Ada body) og her er der tale om aegte kontrakter - ikke C++'s underlige bastard headere. Ada er frit og helt fra begyndelsen var software engineering en del af sproget. Korrekt kode.

Unit test virker fint i OO. Men opfatter man generelt det at teste kode via white- eller blackbox metoder, som en metode til at bevise korrekthed af programmel paa, er man ude i hampen.

Haaber ikke nogle folk har ladet sig overbevise om at der er endnu er fundet en "silver bullet" til software krisen. "Software crisis" paa Google for mere info om emnet.

25
29. maj 2008 kl. 15:17

Man kan ganske rigtigt ikke bevise et (ikke-trivielt) program fejlfrit med test (det er Dijkstra's lov om test).

Men hvis tests ikke kommer bare nogenlunde rundt i rummet af mulige brugsmønstre, så er de intet værd. Så kan du være nok så glad over at have lavet unit tests på alle metoder, men du kan ikke bruge resultatet til noget. Kort sagt har du spildt din tid.

Lad os sige, at du har en klasse, der implementerer sekvenser som enkelthægtede lister, noget i stil med

class List implements Sequence { private Object head; private List next; public Object elem() {return this.head;} public List advance() {if (this.next==NULL) error(); else return(this.next);} public List last() {if (this.next==NULL) return this; else return this.next.last();} }

Nu vil du gerne udvide med en konkateneringsoperator, så du tilføjer en metode:

public List concat(List y) {this.last().next = y;}

Dine unit tests prøver concat() på hundrede par af tilfældigt konstruerede lister, og den finder ingen fejl. Men en dag er der en, der laver kaldene

x.concat(x); ... y = x.last();

Testen afslørede ikke problemet, fordi den aldrig prøvede at konkatenere en liste med sig selv. Problemet er, at det er ret svært at forudsige de mulige brugsmønstre bare nogenlunde præcist, hvis de kan afhænge af tilstand og aliasing.

27
29. maj 2008 kl. 16:20

Problemet er, at det er ret svært at forudsige de mulige brugsmønstre bare nogenlunde præcist

Er det nu også formålet med test? I min optik består test af to trin:

  1. Identificer den lovlige delmængde af inputværdier fra populationen.
  2. Skriv test for afprøvning af denne delmængde.

Overført på dit eksempel mangler der en definition af den lovlige mængde af input. I f.eks. C++ ville en assignment metode i en klasse altid teste for "self assignment":

Foo& Foo::operator= (const Foo& f) { if (this == &f) return *this; // Håndter self assignment

// Normal håndtering af assignment

return *this; }

Det der går galt i dit eksempel, er den manglende håndtering af self assignment, hvilket burde have været en del af designet.

28
29. maj 2008 kl. 17:46

"1) Identificer den lovlige delmængde af inputværdier fra populationen. 2) Skriv test for afprøvning af denne delmængde."

Det virker ku, hvis du er sikker på, at dine metoder aldrig bliver kaldt med værdier udenfro denne mængde, så hvis ikke typen i sig selv sikrer dette, så skal din kode test for inputtets validitet, og din unit test skal teste, at denne test sker korrekt.

"Det der går galt i dit eksempel, er den manglende håndtering af self assignment, hvilket burde have været en del af designet."

Og hvis det burde have været en del af designet, burde et også have været en del af testen.

Problemet i Java og de fleste andre OO sprog er, at typerne sjældent er stærke nok til at sikre mod invalide argumenter. Specielt kan typerne ikke udtrykke aliasing, og netop aliasing er en hyppig kilde til invalide argumenter. Mange programmører tænker ikek engang over, at aliasing kan være et problem for deres kode.

Gode designere vil teste for det på køretid, men dels er det dyrt og dels bliver den slags test ofte sprunget over -- generelt laves der alt for lidt argumentvalidering, se bare på SQL-injection problematikken.

29
29. maj 2008 kl. 18:50

Det virker ku, hvis du er sikker på, at dine metoder aldrig bliver kaldt med værdier udenfro denne mængde, så hvis ikke typen i sig selv sikrer dette, så skal din kode test for inputtets validitet, og din unit test skal teste, at denne test sker korrekt

Det var derfor jeg tidligere anlagde betragtningen, at unit test er en avanceret form for grænseværdi test. Punkt 1 i min to-trins model indeholder den implicitte antagelse, at der foretages inputvalidering; Består input ikke denne test, skal der returneres en exception (om denne så bør være checked eller ej, er en helt anden diskussion).

Er din definition af aliasing en interface type? I så fald tilbyder Java instanceof til at teste objekter på kørselstidpunkt. For en dybere validering tilbyder Java inspektion (reflection).

Da min opdragelse indenfor programmering tager afsæt i Pascal og C, er jeg opflasket med inputvalidering - Pascals var indbygget i sproget, og i C blev man vendet til at lave kontrol af returværdien fra funktioner ( if ((res = foo()) == 0) {....}), hvorfor jeg finder det naturligt, at lave inputvalidering af alle metoder i public scope.

Det er egentligt forbavsende så få Java programmører, der anvender sproget indbyggede hjælp til inputvalidering; jeg tænker her på assert. Jeg vil tro, at over halvdelen af alle køretidsfejl i Java skyldes NullPointerException, hvilket enkelt kunne have været løst med en kombination af inputvalidering og assert.

Et andet typisk eksempel er følgende - java er brugt som eksempel:

public class Foo { Foo instance;

private Foo() {}

public static Foo getInstance() {
    if (instance == null)
        instance = new Foo();
    return instance;
}

}

Kan du gætte fejlen i ovenstående, som jeg er 99% sikker på, de fleste ikke tester for med unit test?

31
29. maj 2008 kl. 20:56

Jeg er ikke ekspert i alle detaljer omkring Java, så jeg ved ikke, om det betyder noget, at konstruktøren er erklæret private. Hvis ikke, så ser jeg ingen grund til at erklære den eksplicit, for defaultkonstruktøren gør det samme (initialiserer felter til nul). Hvis "private" betyder, at man ikke kan konstruere instanser af objektet, er det vel fejlen.

Men hvad angår nullPointerExeptions, så foretrækker jeg Spec#s ide, hvor default pointertypen ikke kan være null, og man eksplicit skal erklære pointere til at kunne være det. Dermed kan det statisk valideres, at et argument ikke er null.

Apropos inputvalidering, hvad ville du bruge til concat-metoden i mit eksempel?

39
30. maj 2008 kl. 10:32

[quote]Apropos inputvalidering, hvad ville du bruge til concat-metoden i mit eksempel?

Et forslag kunne være at implementere interfacet Comparable.[/quote]

Jeg kan ikke lige se, hvad det skulle hjælpe. Kan du uddybe?

Og forklar så, hvordan det vil fange problemet i følgende kode:

[code=java] y = x.advance(); x.concat(y); z = x.last(); [/code]

40
31. maj 2008 kl. 02:19

[code=java] class List implements Sequence, Comparable<List> {

private E head;
private List<E> next;

public E elem() {
    return head;
}

public List<E> advance() {
    if (next == null)
        throw new IndexOutOfBoundsException();
    return next;
}

public List<E> last() {
    if (next == null)
        return this;
    else
        return next.last();
}

public void concat(Sequence<E> y) {
    if (this.compareTo((List<E>)y) != 0)
        last().next = (List<E>) y;
    else
        throw new RuntimeException("cannot concatenate to self");
}

public int compareTo(List<E> o) {
    List<E> y = this;
    List<E> x;
    try {
        while (y.next != null) {
            x = o;
            while (x.next != null) {
                if (y.equals(o))
                    return 0;
                x = x.advance();
            }
            y = y.advance();
        }
    }
    catch (IndexOutOfBoundsException e) {}
    return 1;
}

} [/code] Bemærk dog det er en dyr kontrol, da kompleksiteten af metoden er O(n^2). Så der bør nok findes en anden løsning for store lister.

41
31. maj 2008 kl. 02:38

Glemte: Forudsætningen er, at klassen i sin nuværende udformning vil loope uendeligt i last metoden, hvis concat tillader input fra lister, hvor fællesmængden af elementer mellem de to lister ikke er tom. compareTo metoden laver denne kontrol, og såfremt den finder blot et identisk element mellem de to lister, afviser concat med en RuntimeException. Ens i denne sammenhæng vil sige, at elementernes reference er ens, og altså ikke indholdet af elementerne. Hvis et element fra to forskellige lister har samme reference, vil de også have fælles reference til næste element.

42
2. juni 2008 kl. 11:42

Din ide er god nok, men som du siger er den O(n^2). En bedre metode, der bruger samme grundlæggende ide er at sammenligne this.last() med y.last().

Det giver dog stadig et væsentligt overhead, for uden testen er tidsforbruget af x.concat(y) O(|x|), mens det med testen er O(|x|+|y|), hvor |x| er længden af x.

32
29. maj 2008 kl. 21:09

Fejlen er at dit singleton pattern kun returnerer instansen første gang den bliver kald.

Jeg vil konkludere til mig selv at unit test kun kan bruges til at teste de situationer man kender der vil give problemer. Det er altså ikke hvilken teknologi der er afgørende for om en unit test er effektiv, men udviklerens overblik. Funktionelle sprog har altså i min bog ingen fordel frem for OO til unit test.

Til Torben: Hvad er dine erfaringer med OO siden du er så skræmt af det?

34
30. maj 2008 kl. 09:34

Rene skriver:

Fejlen er at dit singleton pattern kun returnerer instansen første gang den bliver kald.

Que? return-sætningen er uden for if-sætningens virkefelt, så indholdet af instance bliver returneret hver gang. Eller også har jeg slemt misforstået Java's syntaks og semantik.

35
30. maj 2008 kl. 09:37

Fejlen er at den ikke er trådsikker.

37
30. maj 2008 kl. 10:14

[qoute]Fejlen er at den ikke er trådsikker.[/quote]Ja, det var den åbenlyse fejl. Fejlen kan udbedres på følgende måde:

[code=java] public class Foo { Foo instance; private Foo() {}

public static synchronized Foo getInstance() {
    if (instance == null)
        instance = new Foo();
    return instance;
}

} [/code] Men der er en anden, og endnu værre, fejl, der relaterer sig til Java's objektmodel. Fejlen kunne Torben have brugt som argumentation for at unit test i OO er væsentligt vanskeligere end i funktionsprogrammering.

Årsagen til fejlen skal findes i Java's indbyggede regel om, at alle objekter implicit arver fra Object. En af metoder i object har følgende forskrift:

[code=java] protected Object clone(); [/code] Umiddelbart har det derfor ingen konsekvenser for vores klasse, men skulle fremtiden betyde nedenstående, kan vi risikere at stå med en logisk bombe: [code=java] class Bar implements Clonable {

public Object clone {
    Object o = null;
    try {
        o = super.clone();
    }
    catch (CloneNotSupportedException e) {
        System.err.println("MyObject can't clone");
    }
    return o;
}

}

class Foo extends Bar { Foo instance;

private Foo() {}

public static synchronized Foo getInstance() {
    if (instance == null)
        instance = new Foo();
    return instance;
}

}

Foo foo = Foo.getInstance(); Object o = foo.clone() /* Da super implementerer clone er det lovligt. Det var vist ikke meningen med en singleton! */ [/code] For at udelukke denne situation, bør vores singleton derfor have følgende udvidelse: [code=java] class Foo Bar { Foo instance;

private Foo() {}

public static synchronized Foo getInstance() {
    if (instance == null)
        instance = new Foo();
    return instance;
}

public Object clone()
        throws CloneNotSupportedException {
    throw new CloneNotSupportedException(); 
    /* Ny kan man ikke lave en kopi af singleton
     * grundet nedarvning fra super.
 */
}

} [/code]

36
30. maj 2008 kl. 09:47

En anden pointe mht. unit test og OO, er, at unit test er en måde (hvis ikke det er mådEN) at fastholde de krav man har til sit system.

Noget af det gode ved OO er jo netop indkapsling af implementationsdetaljer, som er omverdenen uvedkommende - men hvordan sikrer man at koden stadigvæk er "korrekt" (altså ikke i algoritmisk forstand, mere i en bred "det blev vi enige med kunden om"-agtig forstand) efter en stor refaktorering?

Det gør man ved at have unit tests! Og der er - så vidt jeg kan se - ingen anden (praktisk lade-sig-gørlig) måde at gøre dette på.

Hvis man vil bevise at koden er matematisk korrekt, skulle man tage at kigge på nogle af de værktøjer der findes til kontraktbaseret programmering i stedet.

33
30. maj 2008 kl. 09:31

Rene skriver

Til Torben: Hvad er dine erfaringer med OO siden du er så skræmt af det?

Jeg er ikke skræmt -- jeg har nok programmeret i mange flere forskellige sprog meg mangle flere forskellige paradigmer end dig, så der skal meget til at skræmme mig. Men jeg har stirret OO-programmering i øjnene, og jeg brød mig ikke om, hvad jeg så. Jeg vil tro, at det er det perspektiv, jeg har fra at have prøvet så mange forskellige sprog, der lader mig se det, hvor folk, der kun har programmeret imperative og OO sprog, ikke kan se noget galt.

Jeg kan jo så til gengæld spørge: Hvad er dine erfaringer med funktionsprogrammering, siden du er så skræmt af det?

30
29. maj 2008 kl. 18:57

Foo instance;

Skulle selvfølgelig have været erklaret: private static Foo instance; Det er altså ikke det, der var fejlen:-)

26
29. maj 2008 kl. 15:50

Men giver det overhovedet mening at teste for et brugsmønster som ikke giver mening? Hvis du skal teste funktioner for alle brugsmønstre skal du jo også kunne teste den mad alle mulige input. Det er ikke realistisk.

4
Indsendt af David Askirk Fotel (ikke efterprøvet) den tir, 05/27/2008 - 14:25

Smalltalk har da unit testing med. Og det må siges at være OO :-)

5
27. maj 2008 kl. 14:40

Hvis man ikke bruger nedarvning og lokal tilstand i klasser, hvad er der så tilbage af OO? Så programmerer man jo funktionelt.

Jeg forstår såmænd godt dem, der fraråder brug af nedarvning og tilstand i OO-programmeringssprog, men det svarer lidt til at fraråde brugen af pointere og heltal i C -- det er en god ide, men det er ikke det, sproget er designet til.