Tracking-Info
Blog

+49 - 511 - 270 416 0

info@aldebaran.de

Jörn Klapschus

Ein Anti-Pattern der Softwareentwicklung: Der Tick-Tack-Tyrann

von Jörn Klapschus, 17.07.2014 Tags: Softwareentwicklung, Anti-Pattern, SWE Know-How

Der Tick-Tack-Tyrann

Erinnern Sie sich vielleicht noch an eine populäre britische Fernsehserie aus den 70ern, die hierzulande oft wiederholt ausgestrahlt wurde und mittlerweile wie so viele andere Serien aus alten Zeiten in den DVD-Regalen aufgetaucht ist — in der es einen kauzigen Zauberer aus dem 11. Jahrhundert in die damalige Gegenwart von 1970 verschlagen hat? Der Witz der Serie bestand unter anderem darin, welche Bezeichnungen der alte Mann für die aus seiner Sicht fremdartigen Gegenstände des modernen Alltags ersann; so nannte er beispielsweise das Telefon einen "Sprechknochen".

Für den Wecker, der seinen jungen Freund aus der Gegenwart morgens aus den Federn holte, der ihm sagte, wann es Zeit zum Essen und zum Schlafen ist, fand er aufgrund dieses zeitlichen Diktats einen wenig schmeichelhaften Namen: Er nannte ihn den "Tick-Tack-Tyrannen".

In der Softwareentwicklung kann uns der "Tick-Tack-Tyrannen" in anderer Gestalt begegnen, nämlich in Form der Systemuhr. Diese sonst so nützliche Institution wird schnell zu einer ungeahnten Hürde, wenn es darum geht, zeitlich abhängige Geschäftsregeln testbar zu bekommen.

Stellen wir uns einmal folgende Situation vor: Unser Auftrag ist es, in C# ein Aufgabensystem zu erstellen, vielleicht ähnlich einem Ticketsystem zum Bugtracking oder der Aufgabenverwaltung in MS Outlook. Kernstück unserer Geschäftsdomäne ist dabei die Klasse Aufgabe, welche folgendermaßen lautet:

Quelltext:  Alles auswählen  |  Zeilennummerierung an/aus
  1. public class Aufgabe
  2. {
  3.     public string Text { get; set; }
  4.     public DateTime Erstellungsdatum { get; set; }
  5.     public TimeSpan Alter
  6.     {
  7.         get
  8.         {
  9.             return DateTime.Today - this.Erstellungsdatum;
  10.         }
  11.     }
  12.     public bool IstFaellig
  13.     {
  14.         get
  15.         {
  16.             return this.Alter >
  17.                 Benutzereinstellungen.AufgabenSindFaelligNach;
  18.         }
  19.     }
  20. }

Wie man sehen kann, verfügt eine Aufgabe neben einem Text (dem eigentlichen Inhalt) und einem Erstellungsdatum über eine Eigenschaft namens Alter, welche die Differenz zwischen dem Erstellungsdatum und dem heutigen Tag als TimeSpan zurückliefert. So weit, so gut. Allerdings wollen wir zusätzlich folgendes Feature in unserem Beispiel umsetzen: Wenn das Alter einer Aufgabe eine bestimme, vom Benutzer eingestellte Anzahl von Tagen überschreitet, so soll diese als fällig gelten und dementsprechend in der Benutzerschnittstelle hervorgehoben werden, freundlich an die Bearbeitung erinnernde Pop-Ups bewirken oder auf sonstige Weise den Anwender auf baldige Erledigung hinweisen.

Dafür sieht die Klasse Aufgabe bereits eine Eigenschaft IstFaellig vor, die einen Wahrheitswert zurückliefert. Hier wird davon ausgegangen, dass wir die vom Benutzer gewünschte Einstellung per Aufruf von Benutzereinstellungen.AufgabenSindFaelligNach erhalten, also beispielsweise ein TimeSpan-Objekt zurückgeliefert bekommen, welches einen Zeitraum von 3 Tagen darstellt. Unsere Eigenschaft IstFaellig prüft nun einfach, ob das Alter einer Aufgabe diese Zeitspanne überschritten hat, und liefert je nachdem den Wert true oder false zurück.

Soweit sieht unsere Implementation ganz gut aus, aber die eigentlichen Probleme beginnen, sobald wir uns die Testseite der Medaille ansehen. Als pflichtbewusste Unit-Tester haben wir mit Hilfe von NUnit zwei Testfälle zu unserer Klasse Aufgabe verfasst, die folgendermaßen lauten:

Quelltext:  Alles auswählen  |  Zeilennummerierung an/aus
  1. [TestFixture]
  2. public class AufgabeTest
  3. {
  4.     private Aufgabe aufgabe;
  5.  
  6.     [SetUp]
  7.     public void BereiteVor()
  8.     {
  9.         this.aufgabe = new Aufgabe
  10.         {
  11.             Text = "Test",
  12.             Erstellungsdatum = new DateTime(2012, 01, 01)
  13.         };
  14.  
  15.         Benutzereinstellungen.AufgabenSindFaelligNach =
  16.             new TimeSpan(3, 0, 0, 0);
  17.     }
  18.  
  19.     [Test]
  20.     public void AufgabeIstZuBeginnNichtFaellig()
  21.     {
  22.         // TODO: Testobjekte vorbereiten
  23.         Assert.That(this.aufgabe.IstFaellig, Is.False);
  24.     }
  25.  
  26.     [Test]
  27.     public void AufgabeIstNachDreiTagenFaellig()
  28.     {
  29.         // TODO: Testobjekte vorbereiten
  30.         Assert.That(this.aufgabe.IstFaellig, Is.True);
  31.     }
  32. }

Die beiden TODO's deuten dabei bereits darauf hin, dass uns die Formulierung der Testfälle schwergefallen ist.
Zunächst war alles noch recht einfach:
In der mit dem Attribut Setup gekennzeicheten Methode haben wir uns ein Testobjekt der Klasse Aufgabe erstellt, einen (nicht unbedingt notwendigen) Beispieltext gewählt und ein willkürliches Erstellungsdatum als Testwert gesetzt. Die Benutzereinstellung, welche über die Fälligkeit oder Nicht-Fälligkeit von Aufgaben nach Verstreichen einer bestimmten Zeit entscheidet, haben wir probehalber auf drei Tage festgelegt. Der nächste Gedanke war, zwei Unit-Tests zu schreiben: Einen, der zeigt, dass eine frisch erstellte Aufgabe zunächst einmal natürlich nicht fällig ist, sowie einen weiteren, der die Fälligkeit der Aufgabe nach drei Tagen unter Beweis stellt.
Aber wie sollen wir das nur bewerkstelligen?
Unsere Klasse Aufgabe greift direkt auf die von System.DateTime bereitgestellte Systemzeit zu. Liegt vielleicht darin der Kern des Problems, in dem Fehlen einer Indirektion im Umgang mit der aktuellen Zeit? Genau das ist der Fall.

Formulierung eines Anti-Patterns

Ich möchte das vorliegende Problem einmal genau fassen, indem ich es als Anti-Pattern formuliere. Was genau verstehen wir dabei unter einem Anti-Pattern? William J. Brown et. al. haben dazu ein ganzes Buch verfasst: AntiPatterns. Entwurfsfehler erkennen und vermeiden, in welchem sie bekannte Entwicklerfehltritte wie Spaghetti-Code und Blob-Klassen in einer Form beschreiben, die wir ansonsten von den Patterns/Entwurfsmustern der Gang of Four und nachfolgender Autoren kennen. Im Grunde ist also ein Anti-Pattern das Spiegelbild zum gängigen Pattern der Softwareentwicklung: Anstatt ein gewünschtes, positives und vor allem auf andere Problemstellungen übertragbares Muster der Softwareentwicklung aufzuzeigen, erhalten wir hier den ausformulierten guten Rat, welche häufig naheliegenden Entscheidungen und Vorgehensweisen es besser zu vermeiden gilt. Die Anti-Pattern-Autoren bringen es mit folgender Definition auf den Punkt:

Ein AntiPattern ist die Beschreibung einer allgemein verbreiteten Problemlösung, die entschieden negative Konsequenzen nach sich zieht.

Das AntiPattern-Buch schlägt auch einige Schablonen zur strukturierten Formulierung vor, von denen ich hier die Kurzform verwenden möchte, dort "Mini-AntiPattern" genannt. Eine solche Beschreibung besteht aus folgenden Bestandteilen:

  • Der Name des Anti-Patterns
  • Die Beschreibung des eigentlichen Problems und seiner Auswirkungen.
  • Refactoring: Auf welche Art und Weise lässt sich das Problem bei einem Auftreten lösen bzw. von Anfang an vermeiden?

Unser unerwünschter Effekt lässt sich dann wie folgt formulieren:

Anti-Pattern: Der Tick-Tack-Tyrann

Das Problem

Eine Aufgabenstellung beinhaltet, Geschäftsregeln zu implementieren, deren Verhalten von Datum und / oder Uhrzeit abhängen. Dabei wählt der Entwickler den naiven Ansatz, im Code der betroffenen Geschäftsklassen direkt auf Systemdatum und Systemzeit des ausführenden Systems zurückzugreifen. Als Konsequenz dieser Designentscheidung wird das Testen der zeitabhängigen Programmfunktionen auf Unit-Ebene quasi unmöglich. Die Kette der Auswirkungen lautet folgendermaßen:

  • Sowohl Entwickler als auch dedizierter Tester können das zeitabhängige Verhalten nur testen, indem sie ihre Systemzeit manuell manipulieren.
  • Das manuelle Manipulieren der Systemzeit ist zeitaufwändig und fehleranfällig.
  • Aus hohem Aufwand und Fehleranfälligkeit des manuellen Testens folgen eine schlechte Testabdeckung, damit eventuell nicht aufgefundene Bugs für spezielle Szenarien. (Wurden Effekte in Schaltjahren getestet?)
  • Die Tatsache, dass Tests manuell durchgeführt werden, erschwert wirkungsvolles und kostenseitig vertretbares Regressionstesten bei Änderungen des zeitabhängigen Codes erheblich.

Die Problematik des gewählten Lösungsansatzes lässt sich auch allgemeiner als Verletzung eines Entwurfsprinzips beschreiben, das unter anderem von den Pragmatic Programmers beschrieben wurde: Die Klasse Aufgabe ist nicht orthogonal zur Systemumgebung, hier der Systemuhr. Fehlende Orthogonalität bedeutet hier, dass die Umsetzung der fachlichen Regeln im Code und die aktuellen Systembedingungen nicht unabhängig voneinander sind.

Refactoring des Problems

Abhilfe schaffen können wir durch eine Indirektion, die verhindert, dass der fachliche Code unmittelbar mit der Systemzeit hantiert. Statt sich direkt bei den statischen Methoden des eingebauten Datentyps System.DateTime zu bedienen, schaffen wir uns zunächst eine Schnittstelle mit den beiden benötigten Eigenschaften, um diese zu ersetzen:

Quelltext:  Alles auswählen  |  Zeilennummerierung an/aus
  1. using System;
  2.  
  3. namespace Beispiel
  4. {
  5.     public interface IZeitgeber
  6.     {
  7.         DateTime Heute { get; }
  8.  
  9.         DateTime Jetzt { get; }
  10.     }
  11. }

Damit dies auch Wirkung zeigt, passen wir den Code der Klasse Aufgabe an, um von dem Interface Gebrauch zu machen. Dabei gehen wir davon aus, dass eine die Schnittstelle IZeitgeber implementierende Instanz über GlobaleEinstellungen.Zeitgeber verfügbar ist, so dass wir die Eigenschaft Alter wie folgt abändern können:

Quelltext:  Alles auswählen  |  Zeilennummerierung an/aus
  1. public TimeSpan Alter
  2.   {
  3.       get
  4.       {
  5.           return GlobaleEinstellungen.Zeitgeber.Heute -
  6.               this.Erstellungsdatum;
  7.       }
  8.   }

Für die Echtausführung der Anwendung schaffen wir dann eine Implementation, welche tatsächlich die Systemzeit verwendet, um die Werte für Heute (Datum des aktuellen Tages) und Jetzt (aktuelles Datum sowie aktuelle Uhrzeit) zu ermitteln:

Quelltext:  Alles auswählen  |  Zeilennummerierung an/aus
  1. using System;
  2.  
  3. namespace Beispiel
  4. {
  5.     public class Zeitgeber : IZeitgeber
  6.     {
  7.         public DateTime Heute
  8.         {
  9.             get
  10.             {
  11.                 return DateTime.Today;
  12.             }
  13.         }
  14.  
  15.         public DateTime Jetzt
  16.         {
  17.             get
  18.             {
  19.                 return DateTime.Now;
  20.             }
  21.         }
  22.     }
  23. }

Gemockte Zeit

Was wir nun erreicht haben, könnte man als Zeitgeber-Muster bezeichnen. Aber damit nicht genug:
Um die Testbarkeit des fachlichen Codes herzustellen, müssen wir noch die Möglichkeit schaffen, unseren Unit-Tests beliebige Zeitwerte vorzugaukeln. Im schlichtesten Falle erstellen wir ein handgeschriebenes Mock-Objekt für unsere automatisierten Testsuites.

Quelltext:  Alles auswählen  |  Zeilennummerierung an/aus
  1.  
  2. namespace Beispiel
  3. {
  4.     public class ZeitgeberMock : IZeitgeber
  5.     {
  6.         private DateTime testzeitpunkt;
  7.  
  8.         public ZeitgeberMock(DateTime testzeitpunkt)
  9.         {
  10.             this.testzeitpunkt = testzeitpunkt;
  11.         }
  12.  
  13.         public DateTime Heute
  14.         {
  15.             get
  16.             {
  17.                 return this.testzeitpunkt.Date;
  18.             }
  19.         }
  20.  
  21.         public DateTime Jetzt
  22.         {
  23.             get
  24.             {
  25.                 return this.testzeitpunkt;
  26.             }
  27.         }
  28.     }
  29. }

Über den Parameter testzeitpunkt des Konstruktors schaffen wir dabei die Möglichkeit, Testdatum und -uhrzeit genau vorzugeben. Zu beachten ist, dass wir für die Eigenschaft Jetzt der Schnittstelle IZeitgeber den unveränderten Wert des Testzeitpunktes zurückgeben (also inklusive einer eventuell angegebenen Uhrzeit-Komponente), wohingegen wir für die Eigenschaft Heute eine DateTime-Instanz zurückgeben, in der nur der Datumsanteil gefüllt ist, indem wir für unseren gemerkten Testwert testzeitpunkt.Date aufrufen.

Jetzt haben wir alle Voraussetzungen geschaffen, um die beiden TODO's in unserer Unit-Test-Suite zu erledigen; wir nutzen unsere Mock-Klasse, um zur Testlaufzeit das „aktuelle Datum" auf gewünschte Testwerte zu setzen:

Quelltext:  Alles auswählen  |  Zeilennummerierung an/aus
  1. [Test]
  2. public void AufgabeIstZuBeginnNichtFaellig()
  3. {
  4.     GlobaleEinstellungen.Zeitgeber =
  5.         new ZeitgeberMock(new DateTime(2012, 01, 01));
  6.  
  7.     Assert.That(this.aufgabe.IstFaellig, Is.False);
  8. }
  9.  
  10. [Test]
  11. public void AufgabeIstNachDreiTagenFaellig()
  12. {
  13.     GlobaleEinstellungen.Zeitgeber =
  14.         new ZeitgeberMock(new DateTime(2012, 01, 05));
  15.  
  16.     Assert.That(this.aufgabe.IstFaellig, Is.True);
  17. }

Wer sich das händische Erstellen einer Mock-Klasse sparen möchte, kann natürlich auf bewährte Mocking Frameworks wie beispielsweise Moq zurückgreifen. Noch eleganter wird die Lösung übrigens, wenn ein Dependency Injection Framework wie Ninject oder Spring.NET zum Einsatz kommt; in diesem Falle können wir es dem DI-Container überlassen, je nach Laufzeitmodus die korrekten Instanzen von Zeitgeber oder ZeitgeberMock bereitzustellen.

Wie geht es weiter mit den Anti-Patterns?

Wer sich mit dem Thema Anti-Patterns weitergehend beschäftigen will, dem sei neben dem bereits genannten Buch auch das Anti-Pattern-Verzeichnis des Portland Pattern Repository ans Herz gelegt. Der deutsche Wikipedia-Artikel zu Anti-Patterns liefert ebenfalls einige kurze Beschreibungen zu diversen unerwünschten Mustern.