Design by Contract – jetzt auch mit C# – Teil 2
Geschrieben von Christina Hirth in Architektur, Clean Code Developing, tags: Architektur, Clean Code, Unit-TestsDer vorherige DbC-Artikel ist ziemlich “abstrakt” ausgefallen, es haben einfach Beispiele gefehlt. Das möchte ich hiermit nachholen.
Erstens muss man die IDE anpassen: im März kommt .NET 4.0 raus und da wird Design by Contract mitgeliefert. Man kann das Konzept aber jetzt schon anwenden, wenn man die Assembly zusätzlich installiert. Danach muss man die dll referenzieren und im Eigenschaftenfenster des Projektes im neuen Tab Code Contracts das Runtime Checking einstellen.
Jetzt zum Code: Nehmen wir eine ganz einfache Klasse Bill
deren Objekte mit einem IRepository
gespeichert bzw. geladen werden.
1 using System.Diagnostics.Contracts;
2 namespace ContractsPrototyp
3 {
4 public class Bill
5 {
6 public int Id { get; set; }
7 public string Number { get; set; }
8 public double Value { get; set; }
9 }
10
11
12 public interface IRepository
13 {
14 Bill GetBill(string number);
15 void SaveBill(Bill bill);
16 }
Die Kontrakte kann man in den einzelnen Methoden oder für eine ganze Klasse schreiben (unter dem Attribut ContractInvariantMethode
) aber ich finde am schönsten, dass man die auch auslagern kann: durch eine gegenseitige Markierung können Kontrakt-Klassen und Interfaces als “Paare” definiert werden:
11 [ContractClass(typeof(RepositoryContracts))]
12 public interface IRepository
13 {
14 Bill GetBill(string number);
15 void SaveBill(Bill bill);
16 }
17 [ContractClassFor(typeof(IRepository))]
18 public class RepositoryContracts:IRepository
19 {
20 public Bill GetBill(string number)
21 {
22 Contract.Requires(!string.IsNullOrEmpty(number));
23 return null;
24 }
25
26 public void SaveBill(Bill bill)
27 {
28 Contract.Ensures(bill.Id > 0);
29 }
30 }
Eine Vorbedingung wird mit Contract.Requires
und eine Nachbedingung mit Contract.Ensures
definiert. Beide Methoden bekommen boolische Ausdrücke. Diese Ausdrücke müssen frei von Seiteneffekten sein.
Die eigentliche Implementierung der Klasse schaut dann so aus:
31 public class Repository:IRepository
32 {
33 public Bill GetBill(string nummer)
34 {
35 //Würde das Objekt aus Datenhaltung laden
36 return new Bill();
37 }
38
39 public void SaveBill(Bill bill)
40 {
41 //Würde das Objekt speichern und ihm eine Id zuweisen
42 if (BillIsValid( bill )) bill.Id++;
43 }
44
45 private static bool BillIsValid(Bill bill)
46 {
47 return !string.IsNullOrEmpty(bill.Nummer);
48 }
49 }
Woher können wir wissen, dass das funktioniert? Es ist einfach, wir schreiben ein Paar Tests dazu!
Bei Kontraktverletzung wird eine Exception geworfen. Um diese – und dadurch die genaue Verletzung – überprüfen zu können braucht man etwas Workaround:
55 [TestFixture]
56 public class BillTests
57 {
58 private IRepository m_repository;
59 private string m_message;
60
61 [SetUp]
62 public void Setup()
63 {
64 m_repository = new Repository();
65 m_message = string.Empty;
66 Contract.ContractFailed += ( sender, e ) =>
67 {
68 e.SetUnwind();
69 m_message = e.Message;
70 };
71 }
Danach sind die Tests dann einfach:
73 [Test]
74 public void Laden_mit_leerer_Nummer_verletzt_Kontrakt()
75 {
76
77 try
78 {
79 m_repository.GetBill( null );
80 }
81 catch
82 {
83 //Nichts
84 }
85
86 Assert.That( m_message, Is.EqualTo( "Precondition failed: !string.IsNullOrEmpty(number)" ) );
87 }
88
89 [Test]
90 public void Speichern_Rechnung_ohne_Nummer_verletzt_Kontrakt()
91 {
92
93 try
94 {
95 m_repository.SaveBill( new Bill{Value = 25} );
96 }
97 catch
98 {
99 //Nichts
100 }
101
102 Assert.That( m_message, Is.EqualTo( "Postcondition failed: bill.Id > 0" ) );
103 }
Ich hoffe, das Beispiel ist ausführlich genug, um die Vorteile von DbC zu highlighten. Stefan, vielen dank noch mal für den Artikel, ich habe mich natürlich von dir inspirieren lassen.