LinkedList Dojo mit Visual T# – Teil 5

Karate

Image courtesy of „luigi diamanti“ / FreeDigitalPhotos.net


Von den am Anfang des LinkedList Dojo Teil 4 aufgeführten noch fehlenden Implementationen haben wir erst die GetEnumerator()-Funktionen abgehakt. Packen wir nun die noch fehlenden Funktionen und Properties an. Den in Teil 4 implementierten Enumerator wollen wir nun auch nutzen, um einige von diesen Funktionen zu schreiben.

Die Kopie ist besser als das Original

Nehmen wir uns der CopyTo-Methode an. Als erstes die Tests (ich erlaube mir, gleich zwei Tests zu erstellen).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
test CopyTo(int[], int)  
{
    sut.Add(5);
    sut.Add(8);
    int[] res = new int[2];
    runtest sut.CopyTo(res, 0);

    assert res[0] == 5;
    assert res[1] == 8;
}
       
test CopyTo(int[], int)  
{
    sut.Add(5);
    sut.Add(8);
           
    int[] res = new int[6];
    runtest sut.CopyTo(res, 4);

    assert res[4] == 5;
    assert res[5] == 8;
}

Dank dem Enumerator ist die Implemetation der Funktion fast schon geschenkt:

1
2
3
4
5
6
7
public void CopyTo(T[] array, int arrayIndex)
{
    foreach (T item in this)
    {
        array[arrayIndex++] = item;
    }
}

Und noch ein paar Test für die Fehlerfälle:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
test CopyTo(int[], int)  
{
    runtest sut.CopyTo(null, 0);

    assert thrown ArgumentNullException;
}
       
test CopyTo(int[], int)  
{
    int[] res = new int[5];
    runtest sut.CopyTo(res, -1);

    assert thrown ArgumentOutOfRangeException;
}
               
test CopyTo(int[], int)  
{
    sut.Add(5);
    sut.Add(8);
           
    int[] res = new int[5];
    runtest sut.CopyTo(res, 4);

    assert thrown ArgumentException;
}

Und die dazugehörige Implementation mit den Überprüfungen:

1
2
3
4
5
6
7
8
9
10
public void CopyTo(T[] array, int arrayIndex)
{
    if (array == null) throw new ArgumentNullException("array");
    if (arrayIndex  array.Length) throw new ArgumentException("The number of elements in the source collection is greater than the available space from arrayIndex to the end of the destination array.");

    foreach (T item in this)
    {
        array[arrayIndex++] = item;
    }
}

Ich möchte da noch etwas hinzufügen

Unter anderem fehlt noch die Funktion Insert. Auch hierzu zuerst wieder die Tests.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
test Insert(int, int)  
{
    sut.Add(5);
    sut.Add(8);
           
    runtest sut.Insert(1, 4);

    assert sut[1] == 4;
    assert sut[2] == 8;
}  
       
test Insert(int, int)  
{
    sut.Add(5);
    sut.Add(8);
           
    runtest sut.Insert(0, 4);

    assert sut[0] == 4;
    assert sut[1] == 5;
}  
       
test Insert(int, int)  
{
    sut.Add(5);
    sut.Add(8);
           
    runtest sut.Insert(2, 4);

    assert sut[1] == 8;
    assert sut[2] == 4;
}

Auch hier wieder die Implementation der Funktion, welche die Tests erfüllt.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void Insert(int index, T item)
{
    if (index == Count)
    {
        Add(item);
        return;
    }

    LinkedListElement element = new LinkedListElement();
    element.Element = item;

    if(index == 0)
    {
        element.Next = first;
        first = element;
        return;
    }

    LinkedListElement priorIndex = GetAt(index - 1);
    element.Next = priorIndex.Next;
    priorIndex.Next = element;
}

Wiederum benötigen wir noch Tests für die Fehlerfälle:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
test Insert(int, int)  
{
    sut.Add(5);
    sut.Add(8);
           
    runtest sut.Insert(-1, 4);

    assert thrown ArgumentOutOfRangeException;
}
       
test Insert(int, int)  
{
    sut.Add(5);
    sut.Add(8);
           
    runtest sut.Insert(3, 4);

    assert thrown ArgumentOutOfRangeException;
}

Diese führen dann zu der folgenden Implementation der Insert-Funktion.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public void Insert(int index, T item)
{
    if (index  Count) throw new ArgumentOutOfRangeException();

    if (index == Count)
    {
        Add(item);
        return;
    }

    LinkedListElement element = new LinkedListElement();
    element.Element = item;

    if(index == 0)
    {
        element.Next = first;
        first = element;
        return;
    }

    LinkedListElement priorIndex = GetAt(index - 1);
    element.Next = priorIndex.Next;
    priorIndex.Next = element;
}

Weg damit

Für die RemoveAt-Funktion implementiere gleich alle Tests auf einmal.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
test RemoveAt(int)  
{
    sut.Add(5);
    sut.Add(8);
           
    runtest sut.RemoveAt(0);

    assert sut.Count == 1;
    assert sut[0] == 8;
}
             
test RemoveAt(int)  
{
    sut.Add(5);
    sut.Add(8);
           
    runtest sut.RemoveAt(1);

    assert sut.Count == 1;
    assert sut[0] == 5;
}      
         
test RemoveAt(int)  
{
    sut.Add(5);
    sut.Add(8);
    sut.Add(4);
           
    runtest sut.RemoveAt(1);

    assert sut[0] == 5;
    assert sut[1] == 4;
}
       
test RemoveAt(int)  
{
    sut.Add(5);
    sut.Add(8);
           
    runtest sut.RemoveAt(2);

    assert thrown ArgumentOutOfRangeException;
}      
       
test RemoveAt(int)  
{
    sut.Add(5);
    sut.Add(8);
           
    runtest sut.RemoveAt(-1);

    assert thrown ArgumentOutOfRangeException;
}

Dann schaue ich, dass alle Tests grün werden.

1
2
3
4
5
6
7
8
9
10
11
12
public void RemoveAt(int index)
{
    if (index = Count) throw new ArgumentOutOfRangeException();
    if (index == 0)
    {
        first = first.Next;
        return;
    }

    LinkedListElement priorIndex = GetAt(index - 1);
    priorIndex.Next = priorIndex.Next.Next;
}

Und wenn wir schon am entfernen sind packen wir die Remove-Funktion auch noch an. Wie gewohnt zuerst die Tests:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
test Remove(int)  
{
    sut.Add(5);
    sut.Add(8);
           
    runtest bool res = sut.Remove(5);

    assert res;
    assert sut.Count == 1;
    assert sut[0] == 8;
}
         
test Remove(int)  
{
    sut.Add(5);
    sut.Add(8);
           
    runtest bool res = sut.Remove(8);

    assert res;
    assert sut.Count == 1;
    assert sut[0] == 5;
}
       
test Remove(int)  
{
    sut.Add(5);
    sut.Add(8);
           
    runtest bool res = sut.Remove(3);

    assert res == false;
    assert sut.Count == 2;
    assert sut[0] == 5;
    assert sut[1] == 8;
}
       
test Remove(int)  
{
    sut.Add(5);
    sut.Add(8);
    sut.Add(5);
                       
    runtest bool res = sut.Remove(5);

    assert res;
    assert sut.Count == 2;
    assert sut[0] == 8;
    assert sut[1] == 5;
}

Der letzte Test überprüft dabei, dass das erste Auftreten eines Elementes entfernt wird. In der dazu passenden Implementation können wir von bereits bestehenden Funktionen profitieren:

1
2
3
4
5
6
7
8
public bool Remove(T item)
{
    int pos = IndexOf(item);
    if (pos < 0) return false;

    RemoveAt(pos);
    return true;
}

Bitte nicht berühren!

Für das letzte Property kann man sicherlich darüber streiten, ob ein Test nötig ist. Aber der Vollständigkeit halber auch hier zuerst der Test.

1
2
3
4
5
6
test IsReadOnly get
{
    runtest bool res = sut.IsReadOnly;
           
    assert res == false;
}

Und das implementierte Property IsReadOnly:

1
2
3
4
public bool IsReadOnly
{
    get { return false; }
}

Somit ist die Klasse LinkedList fertig implementiert. Doch bevor wir uns wieder dem Tagesgeschäft zuwenden wollen wir noch einen Blick zurück werfen und schauen, was wir gelernt haben und wo noch Potential für Verbesserungen besteht.

Retrospektive

Als erstes stellt sich die Frage: Haben wir das Ziel erreicht?
Das dürfen wir mit gutem Gewissen mit ja beantworten, wir haben eine funktionstüchtige Klasse implementiert, die durch Tests abgesichert ist.

Was aber noch wichtiger ist: Was haben wir auf dem Weg zum Ziel gelernt?
Der Test-First-Ansatz hat mir gut gefallen, auch wenn ich mich manchmal zurückhalten musste, gleich mit der Implementation vorzupreschen, ohne den Test geschrieben zu haben. Einige wenige Male habe ich schon eine zusätzliche Überprüfung eingebaut, bevor ich diese durch einen Test nötig gemacht hatte. Aber wenn es sich um eine einzelne Überprüfung handelt und man den Test gleich nachliefert ist das aus meiner Sicht legitim. Wir wollen ja nicht päpstlicher als der Papst sein…
Was den Einsatz von Visual T# für die Tests anbelangt bin ich nicht so begeistert. Einige Sachen gefallen mir (z.B. die klare Trennung zwischen Vorbereitung, Ausführung und Überprüfung), aber ich frage mich, ob sich der Aufwand lohnt, eine neue Sprache dafür zu lernen. Mit Unit Tests in C# kann man die Tests ebenfalls schreiben und man muss dann nicht andauernd zwischen den beiden Sprachen wechseln. Vielleicht habe ich mich aber auch noch zu wenig mit Visual T# auseinander gesetzt und es verbergen sich noch interessante Konzepte darin, welche die Arbeit mit Unit-Tests vereinfachen oder verbessern. Was aber an Visual T# noch verbessert werden muss ist die Stabilität. Es ist mir einige Male abgestürzt und manchmal hat es unerklärliche Kompilierfehler gemeldet, die mit einem Neustart des Visual Studios behoben werden konnten.
Den Aufwand, die Klasse komplett zu implementieren, hatte ich etwas unterschätzt. Anhand der Properties und Methoden, die IList verlangt, habe ich die Dauer in etwa geschätzt, dabei aber nicht beachtet, dass ich noch einen Enumerator implementieren muss. Dass ich die Blog-Artikel parallel dazu geschrieben habe hat einen zusätzlichen Sprachwechsel erfordert, so dass ich immer zwischen C#, T# und Deutsch wechseln musste. Hier hat mir der Test-First-Ansatz geholfen, bei den einzelnen Schritten fokussiert zu bleiben und nicht in einer Sprache zu weit vorzupreschen und die anderen Sprachen dann mühsam (und fehleranfällig) „nachzuziehen“.
Das einfache Beispiel erlaubt es auch, einmal ein grobes Verhältnis zwischen Test-Code und Produktiv-Code zu schätzen. Sowohl bei der LinkedList als auch beim LinkedListEnumerator ist der Test-Code etwas doppelt so lang wie der Produktiv-Code. Das Verhältnis der Anzahl Test-Methoden gegenüber der Anzahl Properties und Methoden in den beiden Klassen ist dabei noch grösser (circa 2,5 mal so viele Test-Methoden wie produktive Methoden und Properties). Kurzfristig denkende Manager werden sich bei solchen Zahlen vermutlich die Haare raufen, wenn zwei drittel des Codes nicht im produktiven System verwendet wird. Wenn aber dadurch die Fehler, die den Kunden bei seiner Arbeit mit unserer Software behindern, verhindert oder zumindest stark reduziert werden können, ist dieser „unproduktive“ Code schlussendlich ebenso wertvoll wie der Produktiv-Code.

Den entwickelten Code der LinkedList will ich niemandem Vorenthalten. Was für einen produktiven Code noch fehlt sind die XML-Kommentare.

Die Beispiel-Lösung von Stephan Lieser ist in der Juni-Ausgabe erschienen.

Fazit
Test-First-Prinzip erfolgreich angewandt und als gut befunden, die Unit Tests werde ich aber wohl weiterhin mit C# schreiben.

Hat euch dieses Dojo gefallen? Habt ihr ähnliche Erfahrungen mit Visual T# gemacht? Gerne erwarte ich eure Kommentare.

Ein Gedanke zu „LinkedList Dojo mit Visual T# – Teil 5

  1. Pingback: LinkedList Dojo mit Visual T# – Teil 4 « of bits and bytes

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.