Foundations for Professionals .NET Professionals im Profil guide to C# guidgen.de

Blog

Nutze den Augenblick
und teile der Welt mit, was Du zu sagen hast.

Wann ist Code “fertig”?

Donnerstag, 22. Juli 2010, 11:21 Uhr
Permalink | Kommentare (4) | Kommentare als RSSRSS

Scrum als agile Methode verlangt ein dediziertes Dokument, in dem definiert wird, wann Code als “fertig” anzusehen ist. Dieses Dokument wird als Definition of Done bezeichnet und dient allen Beteiligten als gemeinsame Basis zur Abnahme von Code.

Mir gefällt diese Idee.

Allerdings fordert Scrum nur, dass es ein solches Dokument geben muss – es wird jedoch keinerlei Aussage dazu getroffen, wie eine entsprechende Definition aussehen könnte.

Hieran zeigt sich wieder das typische Problem von Scrum: Es bietet nur einen Rahmen, aber keinen Inhalt. Das macht Scrum sehr portabel und vielfältig einsetzbar, das macht Scrum aber auch sehr unspezifisch.

Aus diesem Grund habe ich mir Gedanken gemacht, welche Eigenschaften Code für mich aufweisen muss, um als “fertig” gelten zu können. Entstanden ist meine ganz persönliche Definition of Done.

Code is considered to be “done”, when:

1. It does satisfy all its functional and non-functional requirements.
2. It does not contain any known errors.
3. It has been commented and documented.

4. It has been either pair-programmed or reviewed.
5. It has been developed test-driven using 4-Step TDD.
6. It does not need to be refactored or rearranged.

7. It has been written according to well-known best practices.
8. It does conform to accepted coding standards.
9. It does pass static code analysis without any errors or warnings.

10. It has been integrated and does not break the integration build.
11. It has been checked in into source control.

Wenn Code all diese Punkte erfüllt, kann man den dazugehörigen Task in meinen Augen guten Gewissens als abgeschlossen betrachten.

Vier Schritte in TDD

Mittwoch, 21. Juli 2010, 17:52 Uhr
Permalink | Kommentare (7) | Kommentare als RSSRSS

Den Teilnehmern des dotnetpro.powerdays C# für Profis wurde im Anschluss an die eigentliche Veranstaltung die Teilnahme an einem .NET Coding Dojo angeboten, das von Ilker Cetinkaya durchgeführt wurde.

Im Rahmen dieses Dojos wurde die Frage diskutiert, wie der übliche TDD-Cycle Red – Green – Refactor auszulegen sei. Stein des Anstoßes war, dass einige Teilnehmer den Schritt Refactor als optional angesehen haben und ihn daher im konkreten Fall außen vor lassen wollten – was bei anderen Teilnehmern wiederum für Unmut sorgte.

Ich selbst habe die Meinung vertreten, dass der Schritt Refactoring keineswegs optional sei, denn stellt man diesen in Frage, öffnet man seinem gänzlichen Ausschluss Tür und Tor – dem verführerischen Weg zur Brownfield-Anwendung wird damit Vortrieb geleistet.

Einige Teilnehmer des Dojos – allen voran Stefan Lieser – haben argumentiert, dass ein Refactoring am Ende des TDD-Cycles keinen Sinn machen würde – sondern dass es vielmehr an den Beginn dieses Zyklusses verschoben werden sollte, schließlich sei Refactoring auf ein Ziel und somit auf einen Plan angewiesen – sonst wisse man nicht, in welche Richtung das Refactoring zu treiben sei.

Stefan hat dies in seinen Blogeinträgen Refaktorisieren im TDD Prozess und TDD 2.0 dargelegt – allerdings bin ich davon nicht überzeugt. Ein Refactoring zu Beginn des TDD-Cycles macht keinen Sinn: Erstens aus dem bereits genannten Grund, der Verschmutzung von Code Vortrieb zu leisten, zweitens weil ich direkt zu Beginn eines Projektes kein Refactoring betreiben kann.

Auch wenn es sich hierbei nur um eine einzige Ausnahme handelt, in der die von ihm vorgeschlagene Regel nicht greift – es ist eine Ausnahme, was ich persönlich nicht schön finde.

Für mein Empfinden gibt es zwei Arten von Refactorings: Einerseits kleine, kosmetische Korrekturen, andererseits grundlegende, Architektur-betreffende Änderungen. Stefan beschreibt diese Aufteilung ebenfalls – zieht für mein Empfinden aber die falschen Schlüsse.

Natürlich ist es – wenn überhaupt möglich – schwierig, grundlegende, Architektur-betreffende Änderungen durchzuführen, wenn man noch nicht weiß, in welche Richtung die Software weiterentwickelt werden soll.

Diese Art von Änderungen ist jedoch gar nicht das, was in der Regel von Refactoring betroffen ist: Ob ich Code in eine private Methode auslagere, um DRY umzusetzen, ist unabhängig von der Architektur. Ebenso, ob ich ein Literal in eine Konstante auslagere oder nicht.

Diese Art von Refactorings macht den bedeutend größeren Anteil aus – es wäre auch tragisch, wenn Entwickler und Architekten nach jedem Test beständig damit beschäftigt wären, die Architektur grundlegend anzupassen.

Es liegt auf der Hand, dass diese Art von kosmetischen Korrekturen direkt nach dem Schritt Green ausgeführt werden sollten – ganz so, wie TDD es in seiner Urform vorschreibt: Red – Green – Refactor.

Die Architektur-bezogenen Änderungen werden in diesem Schema aus drei Schritten also gar nicht berücksichtigt: Die logische Konsequenz daraus ist die Einführung eines vierten Schrittes, der getrennt von Refactor stattfinden kann.

Da dieser vierte Schritt wesentliche Änderungen behandelt, die das architektonische Grundgerüst des Codes betreffen, nenne ich diesen Schritt Rearrange – schließlich werden die einzelnen Komponenten und deren Verdrahtung zueinander in diesem Schritt neu arrangiert und komponiert.

Dieser vierte Schritt ist – naturgegeben – erst dann sinnvoll, wenn bekannt ist, in welche Richtung die Entwicklung laufen soll. Ihn aber direkt in den TDD-Cycle aufzunehmen, halte ich für unglücklich – schließlich wird er nicht nach jedem Durchlauf angewendet. Daher plädiere ich für zwei Zyklen, die einander enthalten:

TDD = Red – Green – Refactor

als klassischer TDD-Cycle für kosmetische Korrekturen und

TDD erweitert = TDD – TDD – … – TDD – Rearrange

als erweiterter TDD-Cycle für architektur-bezogene Anpassungen, Änderungen und Erweiterungen.

Den Schritt Rearrange habe ich bewusst am Ende des erweiterten TDD-Cycles untergebracht, denn vor der ersten Ausführung von TDD ergibt die Durchführung von Rearrange keinen Sinn – und da eine Software nie “fertig” ist und es daher stets einen weiteren TDD-Cycle geben wird, habe ich keinerlei Bedenken, Rearrange an die letzte Position zu schieben.

Der erweiterte TDD-Cycle hat in diesem Sinne nämlich keinen Anfang und kein Ende mehr, sondern stellt eher einen Fluss dar – welche Vorbedingungen für den jeweils nächsten Schritt erfüllt sein müssen, darüber wird keine explizite Aussage gemacht. Implizit jedoch erfordert Rearrange Planung, womit auch Stefans Wunsch nach mehr Planung berücksichtigt wäre.

Was ist “einfach”?

Samstag, 8. Mai 2010, 23:22 Uhr
Permalink | Kommentare (3) | Kommentare als RSSRSS

Eines der grundlegenden Prinzipien in der Softwareentwicklung ist das KISS-Prinzip: In der Formulierung Keep it simple, stupid ist im roten Grad von Clean Code Developer (CCD) enthalten, Extreme Programming propragiert Einfachheit als Regel und auch im Agilen Manifest ist Einfachheit als essenzielles Prinzip enthalten.

Softwareentwicklung soll sich also an Einfachheit orientieren – doch was bedeutet das? Was gilt als einfach und was nicht?

Eine eindeutige und allgemein akzeptierte Definition erscheint schwierig, entsprechend verschieden fallen die jeweiligen Erklärung auch aus. Während es auf der CCD-Webseite zunächst

Wer mehr tut als das Einfachste, lässt den Kunden warten und macht die Lösung unnötig kompliziert.

heißt, was sich mit dem im blauen Grad enthaltenen YAGNI-Prinzip überschneidet, wird dann im weiteren Text jedoch erläutert, es sei

für die Evolvierbarkeit des Codes zwingende Voraussetzung, dass der Code verständlich sei.

Zwar ist verständlich ein ebenso subjektiver Begriff wie einfach, deutet aber schon eher in die gewünschte Richtung. Verständlichkeit war auch das Ziel, das ich in meinem Blogeintrag Anmut und Eleganz verfolgt habe – insofern ziehen CCD und ich hier an einem Strang.

Schön ist, dass CCD dabei auch auf Code zu sprechen kommt:

Wenn ein IEnumerable reicht, sollte kein ICollection oder sogar IList verwendet werden.

Auch das entspricht meiner Vorstellung und deckt sich mit dem, was ich als eine gelungene innere Form von Code bezeichne.

XP nennt ebenfalls Verständlichkeit, allerdings nur als einen von vier Aspekten, die gemeinsam Einfachheit definieren. Nach XP zählen hierzu noch die drei Aspekte Testbarkeit, Navigierbarkeit und Erklärbarkeit:

  • Testbarkeit: Nur Code, der sich auf das Notwendige beschränkt, ist zugleich auch einfach zu testen – ansonsten müssen nämlich Spezial- und Sonderfälle wie auch die Auswirkungen von Seiteneffekten im Test berücksichtigt werden. Auch wenn weder CCD noch ich in Anmut und Eleganz die Testbarkeit explizit im Zusammenhang mit Einfachheit ansprechen – auch hier sind wir letztlich einer Meinung.
  • Navigierbarkeit: Zur Einfachheit von Code zählt neben einer guten Verständlichkeit und einer guten Testbarkeit auch eine gute Navigierbarkeit: Das heißt, in einfachem Code findet man sich zurecht, und es fällt leicht, zwischen den beteiligten Klassen und Komponenten hin und her zu navigieren.
  • Erklärbarkeit: Während die Testbarkeit in gewissem Sinne messbar ist, sind Einfachheit und Navigierbarkeit in höchstem Maße subjektiv: Negativ fällt dies vor allem dann auf, wenn ein Team eingespielt ist und bereits lange Zeit an einer Software arbeitet – in diesem Fall wird sich jeder Entwickler des Teams im Code zurechtfinden und ihn verstehen. Aus diesem Grund muss Code für Einfachheit zusätzlich auch einem Uneingeweihten einfach zu erklären sein.

Das Agile Manifest schließlich definiert Einfachheit als

the art of maximizing the amount of work not done.

Hier wird also letztlich nur der Effekt der Reduktion, des minimalen Codes betont – was jedoch für mein Empfinden nicht genügt. Sicherlich kann man argumentieren, dass in dieser Aussage implizit auch die Arbeit angesprochen wird, die zum späteren Verstehen von Code investiert werden muss – viele Entwickler werden diese Interpretation jedoch zunächst überlesen.

All zu oft wird Einfachheit ansonsten nämlich mit dem YAGNI-Prinzip gleichgesetzt – dabei handelt es sich jedoch um zwei verschiedene Konzepte: Das YAGNI-Prinzip empfiehlt, keine Funktionalität im guten Glauben auf die zukünftige Erforderlichkeit umzusetzen, ohne eine konkrete Anforderung dafür zu haben. Das ist dann auch genau das, was im Rahmen von CCD kritisiert wird:

Wer mehr tut als das Einfachste, lässt den Kunden warten und macht die Lösung unnötig kompliziert.

Einfachheit im Sinne des KISS-Prinzips läuft jedoch Gefahr, dass während der Entwicklung lediglich simple und naheliegende Sprachkonstrukte verwendet werden – obwohl ein fortgeschritteneres Konstrukt eventuell deutlich besser geeignet wäre. Ein typisches Beispiel hierfür ist das Schlüsselwort yield, das viel zu selten Verwendung findet.

Außerdem wird dabei häufig auf Design für zukünftige Erweiterbarkeit verzichtet, obwohl dies mit nur unwesentlich mehr Aufwand möglich wäre – so wird beispielsweise eher auf verschachtelte und zahlreiche if-Anweisungen statt auf besserpassende objektorientierte Konstrukte gesetzt.

Kurzum: Einfachheit bedeutet nicht zwingend, das Naheliegendste und mit möglichst wenig Aufwand umsetzbare zu implementieren, sondern erstens, keine Funktionalität zu entwickeln, für die keine Anforderung besteht, zweitens, der Verständlichkeit des Codes höheren Stellenwert einzuräumen als der Entwicklung, und drittens, die zukünftige Erweiterbarkeit nicht außer Acht zu lassen.

Schlussendlich kann man daher festhalten, dass der Begriff einfach nicht besonders glücklich gewählt ist: Häufig wird darunter nämlich die minimalistische Reduktion auf einfache Sprachkonstrukte und wenige Codezeilen verstanden – die Verständlichkeit und zukünftige Erweiterbarkeit bleiben hierbei außen vor.

Daher wäre es meines Erachtens sinnvoll, an Stelle von Einfachheit – oder zumindest zusätzlich – einen anderen, treffenderen Begriff zu propagieren, der Verständlichkeit und zukünftige Erweiterbarkeit einschließt. Am ehesten treffen dies meiner Meinung nach die Begriffe Anmut und Eleganz.

Unit- vs Integrationtests

Freitag, 2. April 2010, 12:53 Uhr
Permalink | Kommentare (5) | Kommentare als RSSRSS

Warum der Einsatz von Unittests generell Sinn ergibt, habe ich in vor einigen Tagen in meinem Blogeintrag Vom Saulus zum Paulus beschrieben. Doch was ist überhaupt ein Unittest? Wie grenzt er sich von anderen Arten von Tests ab?

Häufig werden insbesondere Unit- und Integrationtests vermischt – doch um überhaupt eine Trennung erreichen zu können, ist zunächst eine Klärung des Begriffs Unit nötig, ist sie schließlich das individuelle Merkmal eines Unittests. Es gilt also, zwei Fragen zu beantworten:

  • Wie wird der Begriff Unit definiert?
  • Welcher Unterschied besteht zwischen Unit- und Integrationtests?

Einen ersten Anhaltspunkt liefert die deutschsprachige Wikipedia auf ihrer Seite zum Thema Unittest, werden Unittests dort nämlich als

Modultest[s] (auch Komponententest[s])

bezeichnet. Ein Unittest testet also eine Komponente. Dieser Begriff hat nicht zuletzt dank der zunehmenden Verbreitung des Entwurfsmusters Inversion of Control und entsprechenden Dependency Injection-Containern wie beispielsweise LightCore eine gewisse Verbreitung erfahren.

Eine Komponente wird in diesem Zusammenhang in der Regel als ein Softwarepart definiert, der folgende Anforderungen erfüllt:

  • Unabhängig: Eine Komponente ist unabhängig von einem konkreten Projekt, sondern kann flexibel in verschiedenen Projekten genutzt und nach Bedarf hinzugefügt werden. Komponenten dürfen dabei natürlich Abhängigkeiten auf andere Komponenten aufweisen, wobei diese jedoch die gleichen Anforderungen erfüllen müssen.
  • Kontrakt: Eine Komponente verfügt über einen wohldefinierten Kontrakt, der neben der Syntax auch eine gewisse Semantik vorgibt. Der Verwender einer Komponente kennt nur deren Kontrakt, weshalb Komponenten gegen andere Komponenten austauschbar sind, sofern von beiden der gleiche Kontrakt erfüllt wird.
  • Blackbox: Eine Komponente fungiert als Blackbox, das heißt, die Interna – auf welche Art ist welche Funktionalität implementiert – spielen für den Verwender keine Rolle. Da der Verwender eine Komponente nur auf Basis ihres Kontrakts verwendet, interessieren ihn diese Interna in der Regel auch nicht.

Unittests sind zunächst lediglich ein weiterer Verwender von Komponenten. Das heißt, dass die Unittests die an eine Komponente gestellten Anforderungen respektieren und keinen Weg suchen, diese zu umgehen.

Insbesondere bedeutet das, dass ein Unittest nur den Teil einer Komponente testet, der über ihren Kontrakt öffentlich erreichbar ist. Konkrete Implementierung ist gemäß dem Blackbox-Prinzip von Komponenten nicht nach außen – also auch nicht für Unittests – sichtbar.

Außerdem muss ein Unittest eine Komponente unabhängig von anderen Komponenten testen – ansonsten könnten Fehler nicht nur von der Komponente an sich, sondern auch von der Interaktion zwischen verschiedenen Komponenten verursacht werden. Stubs und Mocks sind aus diesem Grund essenziell für die Entwicklung von Unittests.

Zusammengefasst garantiert ein Unittest also, dass eine Komponente ihrem Kontrakt in syntaktischer und semantischer Hinsicht entspricht, Ein Integrationtest hingegen testet das Zusammenspiel verschiedener Komponenten, wobei implizit von der Korrektheit der einzelnen Komponenten ausgegangen wird.

Wann immer also eine Komponente nicht isoliert, sondern im Verbund mit anderen Komponenten getestet wird, handelt es sich bei einem Test um einen Integration- und nicht mehr um einen Unittest. Daraus ergibt sich, dass sich Unit- und Integrationtests ergänzen, aber sauber getrennt werden sollten – schließlich handelt es sich auch um verschiedene Arten von Tests.

Vom Saulus zum Paulus

Mittwoch, 31. März 2010, 19:39 Uhr
Permalink | Kommentare (2) | Kommentare als RSSRSS

Am 1. November 2009 habe ich den Blogeintrag Wie viel Sinn machen Unittests? verfasst, in dem ich mich skeptisch gegenüber dem Einsatz von Unittests geäußert habe – und empörte Reaktionen geerntet habe.

Meine Skepsis gründete sich im Wesentlichen auf drei Kritikpunkte, die im folgenden noch einmal aufgelistet werden:

  • Der bestehende objektorientierte Aufbau ist unter OOP- und stilistischen Aspekten sauber umgesetzt, läuft einer guten Testbarkeit allerdings zuwider.
  • Es bestehen zahlreiche Abhängigkeiten zu externen Komponenten, die sich nicht oder nur mit sehr viel Aufwand simulieren lassen.
  • Es bestehen Abhängigkeiten zur konkreten Laufzeitumgebung, die sich nicht oder nur mit sehr viel Aufwand nachbilden lassen.

Wie sich inzwischen herausgestellt hat, können alle Aspekte zufriedenstellend gelöst werden. Letztlich läuft es im Zweifelsfall in der Regel darauf hinaus, eine zusätzliche Abstraktion einzuführen, um besser entkoppeln zu können – schließlich ermöglicht erst ein gewisses Abstraktionsniveau den sinnvollen Einsatz von Stubs und Mocks.

Doch auch wer – wie ich – helfende Hände gereicht bekommt und es daraufhin geschafft hat, alle technischen Hindernisse aus dem Weg zu räumen, mag noch über den Sinn von Unittests grübeln: Die Frage lautet also nicht, wie viel, sondern warum der Einsatz von Unittests überhaupt Sinn ergibt?

Auf diese Frage möchte ich heute eingehen. Da die Entwicklung einer Anwendung irgendwann bei Null auf der grünen Wiese beginnt, liegt es nahe, sich zunächst über den Einsatz von Unittests bei neu zu schreibendem Code Gedanken zu machen: Hier wirken Unittests häufig als Bremse – schließlich müssen neben dem eigentlichen produktiven Code noch zahlreiche Unittests geschrieben werden.

Wer dieser Aussage in dieser Form ohne Widerspruch zustimmt, tappt in die gleiche Falle wie ich vor einigen Monaten: Er vergisst, dass dieser neu geschriebene produktive Code gründlich getestet werden muss – wenn nicht vom Entwickler, dann hoffentlich von irgendjemandem.

Hier liegt der Hund bereits begraben: Wenn nicht der Entwickler selbst sich um das Testen kümmert, muss es jemand anderes machen – der nicht weiß, was sich der Entwickler im Detail gedacht hat, warum manche Entscheidungen so und nicht anders getroffen wurden, der auch nicht weiß, welche potenziellen Sonderfälle berücksichtigt werden müssen.

Zudem weiß dieser jemand auch nicht, ob er alle potenziellen Codepfade getestet hat – er kennt schließlich die Interna nicht. Er kann nur hoffen, alles abgedeckt zu haben, weiß es aber nie.

Schließlich werden Tests, die erst zu einem relativ späten Zeitpunkt durchgeführt werden, in der Regel nicht automatisiert durchgeführt: Die Durchführung hängt also von der Tagesform des Testers ab und kann – unbeabsichtigt – schwanken und eventuell bei zwei Testläufen in verschiedenen Ergebnisse resultieren.

Kurzum: Die Qualität der Tests ist nicht so hoch, wie sie sein könnte. Die Quantität stimmt gegebenenfalls nicht. Und die Ergebnisse sind weder reproduzierbar noch verlässlich.

Wird hingegen von Anfang an mit Unittests entwickelt, ergeben sich all diese Probleme erst gar nicht: Der Entwickler weiß, was zu testen ist, er kennt die Spezialfälle, er weiß oder kann zumindest herausfinden, wie hoch die Testabdeckung ist, und die Testergebnisse sind – da die Unittests automatisiert ausgeführt werden – jederzeit reproduzierbar.

Außerdem entsteht zwar zusätzlicher Aufwand für das Schreiben der Unittests, aber die Ausführung erfordert kaum noch Aufwand. Da Unittests nur einmal geschrieben, aber immer und immer wieder ausgeführt werden, ergibt sich mittelfristig eine deutliche Zeitersparnis – bei qualitativ besseren Ergebnissen.

Aber es kommt noch besser: Da zumindest bei der testgetriebenen Entwicklung gilt, dass immer nur gerade so viel Code geschrieben werden sollte, wie notwendig ist, um einen Unittest zu erfüllen, wird zum einen nur Code geschrieben, der auch getestet ist, zum anderen wird aber auch nur jener Code geschrieben, der tatsächlich erforderlich ist. Das heißt, der Einsatz von testgetriebener Entwicklung hilft, das YAGNI-Prinzip umzusetzen.

Nun kommt aber in jedem Projekt irgendwann der Zeitpunkt, an dem nicht mehr nur neuer Code hinzugefügt, sondern ab dem auch bestehender Code geändert werden muss. Sei es, weil sich Anforderungen geändert haben oder weil Fehler behoben werden müssen.

In jedem Fall muss bestehender Code geändert werden. Typisch für gewachsene Projekte ist, dass die ursprünglichen Entwickler gar nicht mehr verfügbar sind, oder sich nicht mehr genau an spezielle Implementierungsdetails erinnern können. Da zudem in den meisten Softwareprodukten innere Abhängigkeiten bestehen, findet eine Änderung von Code selten isoliert statt, sondern beeinflusst anderen Code ebenfalls – mal mehr, mal weniger.

Das Problem liegt dann meistens darin, dass Änderungen, die im guten Glauben auf Korrektheit durchgeführt werden, Seiteneffekte haben, die ihrerseits neue Fehler produzieren. Irgendwann werden aus diesem Grund keine Änderungen mehr durchgeführt, sondern vermehrt Workarounds geschrieben – und irgendwann dann Workarounds um die Workarounds, und wieder später Workarounds um die Workarounds um die Workarounds, und so weiter …

Was also fehlt, ist ein Sicherheitsnetz, das den Entwickler beim Auftreten von unerwünschten Seiteneffekten auffängt und ihn darauf hinweist, dass seine Änderung an anderer Stelle ungewollte Auswirkungen hat. Genau dies erfüllen Unittests, wenn sie gepflegt werden und stets sämtlichen Code abdecken.

Das ist meines Erachtens der größte Vorteil von Unittests: Sie erlauben es Entwicklern, Code unbefangen ändern zu können, ohne Befürchtungen wegen Nebenwirkungen haben zu müssen.

Fasst man all diese Vorteile zusammen, so überwiegen die Vorteile ganz deutlich den initialen Aufwand, sich in Unittests einzuarbeiten und die eigene Arbeitsweise auf testgetriebene Entwicklung umzustellen: Unittests vereinfachen die Entwicklung und helfen, sie zielgerichteter und sicherer zu gestalten.

Auch wenn der Anfang schwer ist und eine saubere testgetriebene Entwicklung viel Reflektion und Disziplin erfordert – es lohnt sich.

Vorbedingungen in Unittests

Donnerstag, 18. März 2010, 17:43 Uhr
Permalink | Kommentare (6) | Kommentare als RSSRSS

Im November 2009 haben Peter Bucher und ich uns im Rahmen unseres monatlichen Streitgesprächs mit der Frage Wie viel Sinn machen Unittests? beschäftigt. Meine damals noch sehr skeptische Haltung ist inzwischen einer regelrechten Begeisterung für Unittests gewichen.

Auch Kirchen haben Kragsteine – zu dieser Einsicht hat mir Ralf Westphal verholfen, wodurch meine mentale Barriere in Bezug auf Unittests gelöst war und ich begonnen habe, mich tiefer mit der Materie zu beschäftigen. Erstes Ergebnis war mein Vorschlag, wie ein Ordnungssystem für Unittests aussehen könnte.

Seither ist nun ein Vierteljahr vergangen, in welchem ich mich viel mit dem tatsächlichen Schreiben von Unittests befasst habe. Auch Themen wie Stubs und Mocks wie auch der innere Aufbau von Tests an Hand des Arrange-Act-Assert-Musters (AAA) standen auf der Tagesordnung.

Dabei hat mir vor allem der Austausch mit anderen Entwicklern geholfen, doch auch das Buch Pragmatic Unit Testing konnte mir diverse Fragen und Unklarheiten beantworten, weshalb ich dieses Buch jedem an der Thematik Interessierten nur empfehlen kann.

Heute hat mich nun Roberto Bez mit einer interessanten Frage konfrontiert: Wenn ein einzelner Unittest nur einen einzelnen Aspekt testen soll, wie wird dann gesichert, dass die Vorbedingungen ihrerseits nicht fehlschlagen?

Unser Diskussiongegenstand war ein Unittest, der einen Datensatz in einer Datenbank löschen soll – dazu aber aus ebendieser zunächst gelesen werden muss. Dass das eigentliche Löschen in die Assert-Phase fällt, liegt auf der Hand – doch was, wenn bereits das Lesen fehlschlägt?

Meines Erachtens fällt das Lesen noch in die Arrange-Phase des Unittests, denn es dient der notwendigen Vorbereitung für den eigentlichen Test: Schließlich soll der Unittest das Löschen des Datensatzes testen – und nur das Löschen!

Alle Vorbedingungen, die erfüllt sein müssen, dürfen zwar ebenfalls nicht fehlschlagen, gehören aber nicht direkt zu dem getesteten Aspekt:

[TestFixture]
public class WhenDeleteIsCalled
{
    [Test]
    public void WithAnExistentRecord_TheRecordIsDeleted()
    {
        // Arrange.
        var input = this._repository.ReadRecord(this._id);
        var expected = this._repository.ReadRecords().Count - 1;
        // Act.
        this._repository.Delete(input);
        var actual = this._repository.ReadRecords().Count;
        // Assert.
        Assert.That(actual, Is.EqualTo(expected));
    }
}

Was also, wenn bereits das Lesen des Datensatzes fehlschlägt?

Naheligend wäre, auch das Lesen in die Assert-Phase zu ziehen, doch dies würde eine Vermischung von Vorbereitung und tatsächlicher Durchführung bedeuten. Ruft man sich zudem in Erinnerung, dass ein einzelner Unittest nur einen einzelnen Aspekt testen soll, wird klar, dass dies keine Lösung darstellt.

Um einen besseren Ansatz zu finden, muss man sich zunächst zwei Fakten vor Augen führen:

  • Ein fehlgeschlagener Unittest liefert nur in den seltensten Fällen auf den ersten Blick den Grund, warum etwas fehlgeschlagen ist – in der Regel steht nur die Information zur Verfügung, dass etwas fehlgeschlagen ist, so dass eine weitere Analyse erforderlich ist.
  • Neben dem exemplarisch genannten Unittest existieren weitere Unittests in dem Projekt – unter anderem auch solche, die das Lesen von Datensätzen testen.

Tritt also bereits beim Lesen des Datensatzes ein Problem auf, so schlägt nicht nur der Unittest fehl, der das Löschen abdeckt, sondern auch jener, der bereits das Lesen testet. Welcher von beiden in der zeitlichen Reihenfolge dabei zuerst ausgeführt wird, darf keine Rolle spielen, da Unittests per Definition unabhängig voneinander gestaltet werden müssen.

Nun stellt sich also die Frage – welcher Unittest wird zuerst begutachtet? Wird zuerst der Unittest analysiert, der das Lesen abdeckt, wird der Fehler direkt gefunden und behoben. Im nächsten Durchlauf wird dann auch der Unittest, der das Löschen abdeckt, erfolgreich durchgeführt.

Wird hingegen zuerst der Unittest analysiert, der das Löschen abdeckt, so stößt man während der Analyse auf den Punkt, dass das Problem eben nicht in der Act- oder Assert-, sondern bereits in der Arrange-Phase auftritt. An dieser Stelle wird klar, dass es einen anderen Test geben muss, der diese Funktionalität abdeckt, so dass der Fehler nun dort gesucht werden kann.

Es spricht im Hinblick auf die Fehlersuche also nichts dagegen, Funktionalität als Vorbedingung für andere Funktionalität zu nutzen – allerdings liegt ebenfalls auf der Hand, dass ein solches Vorgehen problematisch sein kann. Dann nämlich, wenn auf diese Art gegenseitige Abhängigkeiten von Funktionalitäten entstehen.

Wenn sich zwei Bereiche gegenseitig bedingen, kann es aufwändig werden, den eigentlichen Fehler zu finden, ohne sich ständig im Kreis zu drehen. Einen äußerst interessanten Ansatz, wie man diesem Problem begegnen kann, hat Ralf Westphal in Zustand als Abhängigkeit beschrieben.

Accuracy und Präzision

Dienstag, 9. März 2010, 17:37 Uhr
Permalink | Kommentare (2) | Kommentare als RSSRSS

Im August 2009 habe ich Accuracy als Charakteristik für einen guten Entwickler gefordert.

Im Wesentlichen habe ich mich bei der Definition des Begriffs von dem Computerspiel Quake III Arena leiten lassen, in dem Accuracy für eine überdurchschnittlich hohe Treffgenauigkeit steht. Auf dieser Basis und übertragen auf die Softwareentwicklung habe ich Accuracy damals als

Die Liebe für Details, und die Fähigkeit, auch auf jeden Punkt und jedes Komma zu achten – und nicht nur oberflächlich mal eben schnell über Code hinweg zu gehen.

definiert. Nachdem ich diese Begriffsdefinition inzwischen eine Weile verwende, ist mir bewusst geworden, dass Accuracy alleine nicht genügt, sondern noch ein weiteres Maß erforderlich ist, um die Arbeitsweise eines Entwickler bewerten zu können: Präzision.

Auch – zumindest die englischsprachige – Wikipedia trennt zwischen Accuracy und Präzision, wobei dort wiederum die Analogie zur Waffenkunde aufgegriffen wird:

  • Accuracy bezeichnet die Treffgenauigkeit, wie weit Treffer also vom gewünschten Ziel abweichen.
  • Präzision hingegen bezeichnet die Wiederholbarkeit, wie groß die Varianz der Treffer also ist.

Natürlich sind beide Werte unabhängig voneinander, denn sie lassen sich in jeder beliebigen Ausprägung miteinander kombinieren – weder erfordert eine hohe Accuracy eine hohe Präzision, noch gilt dies umgekehrt.

Was bedeuten diese beiden Begriffe nun übertragen auf die Softwareentwicklung?

Accuracy bedeutet für mich, dass ein Entwickler der idealen Form von Quellcode nahe kommt, das heißt, akkurat geschriebener Code zeichnet sich durch die Abwesenheit von Formfehlern aus. Als Merkmale für derartigen Code können beispielsweise

  • die korrekte Einrückung sowie der korrekte Gebrauch von Leerzeilen,
  • die korrekte Klammerung,
  • die korrekte Schreibweise von Bezeichnern
  • und korrekte Rechtschreibung und Grammatik in Kommentaren

dienen. Entwicklern, die in der Lage sind, solchen Code zu erzeugen, kann man daher durchaus eine hohe Treffgenaugkeit zugestehen.

Nun stellt sich zusätzlich die Frage nach der Präzision. Dass Fehler vorkommen, kann kaum verhindert werden – die Frage ist aber, wie breit gestreut die Varianz der Arten von Fehlern ist.

Wird beispielsweise korrekt eingerückt, korrekt geklammert und es finden sich lediglich ab und an Kommafehler in Kommentaren, so kann man durchaus eine hohe Präzision zugestehen. Umgekehrt gilt – wenn die Arten von Fehlern weit gestreut sind, verfügt ein Entwickler über eine nur niedrig ausgepägte Präzision – wobei er dennoch, wenn er nur wenige Fehler macht, eine hohe Accuracy aufweisen kann.

Ich glaube, dass diese Trennung in Accuracy und Präzision wichtig ist, um die Qualität von Code und damit auch die Fähigkeiten des entsprechenden Entwicklers angemessen beurteilen zu können.

Natürlich beziehen sich beide Werte nicht nur auf die formale Gestalt von Code, sondern beispielsweise auch auf das Durchdenken dessen, was eigentlich codiert wird – auch hier können Ergebnisse mit beiden Maßen gemeinsam besser beurteilt werden als nur mit einem.

Dependency Injection ist keine Silberkugel

Montag, 7. Dezember 2009, 07:01 Uhr
Permalink | Kommentare (3) | Kommentare als RSSRSS

Das Inversion of Control-Paradigma folgt in der Regel einer von zwei Varianten: Entweder wird ein dedizierter Service Locator in Form eines Microkernels verwendet, oder es wird auf ein geeignetes Dependency Injection-Framework samt entsprechendem Container zurückgegriffen.

Der wesentliche Unterschied zwischen diesen Varianten besteht in unterschiedlichen Graden der Explikation beziehungsweise Implikation, und in der Abhängigkeit von der Infrastruktur:

  • Während der Service Locator eine generische Factory darstellt und explizit beauftragt werden muss, eine Instanz zu einem gegebenen Kontrakt zu erzeugen, geschieht dies beim Einsatz eines Dependency Injection-Containers implizit.
  • Da der Aufruf des Service Locators explizit geschieht, bedeutet dies in Konsequenz, dass eine Referenz auf den Service Locator vorhanden sein muss – diese Referenz entfällt beim Einsatz eines Dependency Injection-Containers.

Aus diesen grundlegenden Unterschieden ergeben sich einige spezifische Vor- und Nachteile, interessant ist jedoch, dass der Service Locator in der Regel von Einsteigern, denen das Inversion of Control-Paradigma noch nicht vertraut ist, bevorzugt wird. Die Gründe hierfür liegen auf der Hand: Der explizite Aufruf einer Factory zur Erzeugung einer Instanz ist näher am klassischen Aufruf von new.

Je ausgiebiger sich ein Entwickler danach mit Inversion of Control auseinandersetzt, desto eher lernt er die Vorzüge von Dependency Injection schätzen. Dazu zählen unter anderem:

  • Es ist keine Referenz auf einen Service Locator oder ein ähnliches Konstrukt notwendig – der Dependency Injection-Container bleibt verborgen, so dass die Abhängigkeit von externen Komponenten vermindert wird.
  • Da die einzelnen Abhängigkeiten von außen über den Konstruktor oder entsprechende Eigenschaften aufgelöst werden können, ist es ein Leichtes, innerhalb eines Unittests passende Mockobjekte in eine Komponente hineinzureichen. Darüber hinaus kann in Unittests gänzlich auf den Dependency Injection-Container verzichtet werden, was den Aufwand zur Bereitstellung einer passenden Infrastruktur deutlich senkt.
  • Da die gesamte Verdrahtung der Anwendung implizit geschieht, sinkt der Aufwand für den einzelnen Entwickler deutlich.

Vor allem auf Basis dieser Gründe erscheint Dependency Injection dann häufig als die besser durchdachte Alternative zu einem Service Locator, so dass man als Entwickler versucht ist, sämtliche Abhängigkeiten mit Hilfe von Dependency Injection aufzulösen.

Doch Dependency Injection ist keine Silberkugel. Anders als häufig behauptet, verfügt Dependency Injection nämlich auch über einige Nachteile:

  • Zum einen ist es mit Dependency Injection nicht möglich, lokale Variablen zu instanziieren: Zugriff besteht – entweder über den Konstruktor oder über passende Eigenschaften – nur auf Felder. Der vermeintliche Ausweg, jede lokale Variable in ein Feld zu überführen, erweist sich als Irrweg: Schließlich weisen lokale Variablen und Felder eine unterschiedliche Semantik auf und sind nicht per se gegeneinander austauschbar.
  • Zum anderen ist die dynamische Erzeugung von Instanzen per Dependency Injection nicht möglich: Der Lebenszyklus einer injizierten Komponente ist weitestgehend an den der enthaltenden Komponente gebunden. Für dynamische Instanziierung – beispielsweise in Abhängigkeit von anderem Code – eignet sich Dependency Injection somit nicht.

Das heißt, dass immer dann, wenn lokale Variablen gefüllt oder mehrfach vorhandene Abhängigkeiten dynamisch aufgelöst werden sollen, Dependency Injection an ihre Grenzen stößt: In diesem Fall bleibt nichts anderes, als auf einen Service Locator oder ein ähnliches Konstrukt zu setzen.

Dies bedeutet jedoch, dass Dependency Injection nicht alle Probleme auf einen Schlag löst, ebenso wenig wie ein Service Locator dies macht. Vielmehr liegt die ideale Lösung darin, beide Ansätze miteinander zu vereinen, um das Beste aus beiden Welten nutzen zu können.

Kurzum: Dependency Injection ist kein Allheilmittel. Die Nutzung empfiehlt sich immer dann, wenn genau eine einzige Komponente verwendet werden soll, deren Lebenszyklus dem des enthaltenden Objekts gleicht. Für alle anderen Fälle eignet sich ein Service Locator besser.

Die beiden Konzepte konkurrieren also nicht exklusiv miteinander, sondern ergänzen sich gegenseitig: Dies kann sogar so weit gehen, dass der Service Locator mit Hilfe von Dependency Injection in die entsprechenden Komponenten injiziert wird, um die sonst vorhandene Abhängigkeit zu vermeiden.

Ein Ordnungssystem für Unittests

Sonntag, 22. November 2009, 07:06 Uhr
Permalink | Kommentare (2) | Kommentare als RSSRSS

Auf den Tag drei Wochen sind vergangen, seit Ralf Westphal mich in Reaktion auf den Blogeintrag Wie viel Sinn machen Unittests? ermuntert hat, mich nochmals intensiv mit diesem Thema auseinanderzusetzen:

Unit Tests […] konsequent und kompetent mal für einen Monat einzusetzen. Mit der Kompetenz mag es da schwierig sein, weil man sich dann eine Veränderung der Handlungs- und Denkgewohnheit selbst beibringen muss... aber das ist zumindest ein Anfang.

Ich bin seinem Rat gefolgt und habe bemerkt, dass die wesentlichen Probleme für mich nicht technischer, sondern gedanklicher Natur waren. Zu viele unbeantwortete Fragen standen im Raum – Fragen, die zunächst einfach erscheinen, deren Beantwortung aber viel Nachdenken voraussetzt.

Man könnte mein Problem als die Herausforderung bezeichnen, ein tragfähiges Konzept für Unittests zu finden, das sich nahtlos in meine bisherige Arbeitsweise integriert, sie zwar erweitert, aber nicht von Grund auf umkrempelt.

Bemerkenswert fand ich, dass die wenigsten Entwickler, die ich mit diesen Fragen konfrontiert habe, ihre Antworten fundiert begründen konnten. Einige haben sich sogar daran gestört, dass ich diese Fragen überhaupt stelle, und haben eher dazu geraten, einfach loszulegen – frei nach dem Motto: TDD ist keine Wissenschaft!

Doch ohne fundierte Antworten auf grundlegende Fragen zu haben, die den eigenen Wissensdurst stillen, verbleibt immer ein ungutes Gefühl – ob das, was man macht, tragfähig für zukünftige Erweiterungen sein wird. Diese zukünftige Tragfähigkeit bedeutet mir sehr viel – anders wäre ich vermutlich auch von Extreme Programming nicht so begeistert.

Außerdem ist die Beantwortung solcher Fragen entscheidend, um klare Regeln und Richtlinien zu haben, auf deren Basis man Entscheidungen treffen kann: Je genauer diese Regeln und Richtlinien formuliert werden, desto größer wird die persönliche Liebe zum Detail, desto besser wird die Accuracy – meines Erachtens eine essenzielle Eigenschaft eines guten Enwicklers.

Die erste dieser Fragen, ob Code zum Zwecke einer besseren Testbarkeit angepasst werden darf, wurde bereits in Auch Kirchen haben Kragsteine diskutiert: Wie so oft ist es Ralf Westphal gelungen, eine anschauliche Analogie zur realen Welt zu finden, die das digitale Problem schlagartig löst.

Die zweite dieser Fragen, wie Unittests sinnvoll und übersichtlich organisiert werden können, wurde inzwischen auch beantwortet: Auch hierzu hat Ralf Westphal einen wesentlichen Teil beigetragen, aber auch Neno Loje, Bernd Marquardt und Peter Bucher haben mein nun gefundenes System beeinflusst.

Die grundlegende Erkenntnis ist zunächst, dass sich eine Testklasse nicht zwingend auf eine zu testende Klasse beziehen muss – dass also nicht zwingend eine 1:1-Beziehung zwischen beiden vorliegt. Statt dessen bezieht sich eine Testklasse auf ein sogenanntes System under test (SUT). Ein solches SUT kann durchaus eine Klasse sein – genausogut kann es aber auch eine einzelne Methode sein.

Die zweite Erkenntnis ist, dass ein Unittest ein SUT immer in einem gegebenen Kontext testet. Bei einer Methode ist das fast immer der Aufruf derselben, bei einer Klasse hingegen kann es verschiedene Ausgangssituationen geben: Ein Kontext wäre das Initialisieren der Klasse, ein weiterer die Arbeit mit der gerade initialisierten Klasse, wieder ein weiterer das Disposen einer bereits verwendeten Klasse.

Da sich in der Regel mehrere Unittests auf einen solchen Kontext beziehen, macht es durchaus Sinn, diese in einer Testklasse zusammenzufassen, deren Name den Kontext beschreibt, und deren Namespace den Platz des SUTs angibt.

Auf diese Art könnten Unittests, die den Aufruf der Methode ToDictionary in der Klasse goloroden.de.Common.ExtensionMethods testen, beispielsweise in der Testklasse

  • goloroden.de.Tests.Common.ExtensionMethods.WhenToDictionaryIsCalled

abgelegt werden. Die einzelnen Unittests beschreiben dann mit ihrem Namen innerhalb dieser Klasse nur noch die übergebenen Parameter sowie das erwartete Ergebnis:

  • WithNull_AnArgumentNullExceptionIsThrown()
  • WithAnEmptyString_AnArgumentExceptionIsThrown()

Der Vorteil dieser Terminologie liegt klar auf der Hand: Schlägt ein Unittest fehl, lassen sich aus Testklasse und –methode auf einen Blick alle relevanten Informationen ablesen: Der Aufruf welcher Codestelle verursacht in welchem Kontext mit welchen Parametern ein Problem?

Zudem fördert diese Terminologie das Prinzip von TDD, pro Unittest nur einen einzelnen Aspekt zu testen – versucht man, mehrere Aspekte in einem Unittest zusammen zu fassen, führt dies unweigerlich zu unhandlichen Methodennamen.

Nachdem ich auf dieser Basis nun eine Reihe von Unittests geschrieben habe, bemerke ich, dass sie mir bereits sehr ans Herz gewachsen sind. Interessant fand ich vor allem, dass ich allein durch das nachträgliche Schreiben von Unittests einen Fehler in einer zehnzeiligen Methode entdeckt habe, der trotz dem häufigen Einsatz dieser Methode in den vergangenen zwölf Monaten nicht aufgefallen ist.

Allein das gute Gefühl, zukünftig Änderungen und Erweiterungen an dieser Methode durchführen zu können, ohne Angst haben zu müssen, diesen Fehler unbemerkt wieder einzubauen, war die vergangenen drei Wochen allemal wert.

Insofern kann ich – auch wenn das Schreiben von Unittests für mich noch nicht intuitiv geschieht – guten Gewissens behaupten, dass Unittests sehr wohl Sinn machen. Dies zugebenermaßen sogar mehr, als ich noch vor vier Wochen gedacht hätte.

Auch Kirchen haben Kragsteine

Samstag, 7. November 2009, 18:41 Uhr
Permalink | Kommentare (3) | Kommentare als RSSRSS

Vor knapp einer Woche habe ich mit meinem Blogeintrag Wie viel Sinn machen Unittests? – der im Rahmen der monatlichen Streitgespräche von Peter Bucher und mir erschienen ist – für einige Verwunderung gesorgt. Viele haben sich gefragt, warum gerade ich Unittests dermaßen skeptisch gegenüberstehe.

Ralf Westphal hat in seinem Kommentar vermutet, dass

etwas hinter den drei “problematischen Aspekten” das Grundproblem zu sein [scheint]

und ich muss zugeben, dass diese Vermutung nicht von der Hand zu weisen ist. Als problematisch habe ich an Unittests im Wesentlichen die Tatsache betrachtet, dass Code zum Zweck einer besseren Testbarkeit potenziell angepasst werden muss, da sich einige Konstrukte ansonsten nur schlecht oder gar nicht testen lassen.

Als Beispiel seien an dieser Stelle abstrakte Klassen angeführt, die per Definition nicht instanziiert und deshalb nicht direkt getestet werden können. Prinzipiell kann dieses Problem auf zwei Arten gelöst werden:

  • Entweder werden die bereits bestehenden abgeleiteten Klassen getestet – immerhin ist die abstrakte Basisklasse in diesen implizit enthalten.
  • Alternativ wird eine dedizierte abgeleitete Klasse erstellt, welche die abstrakte Basisklasse beerbt – die Basisklasse ist also auch in dieser Variante implizit enthalten.

Der relevante Unterschied zwischen beiden Varianten liegt in der Tatsache, dass die bereits bestehenden abgeleiteten Klassen eigene Funktionalität mitbringen, die den Test nicht unbedingt verfälschen, aber zumindest beeinflussen könnte: Es wird schwierig, den Test auf die abstrakte Basisklasse zu beschränken.

Eine dedizierte Klasse verfügt nicht über diese Nachteile: Sie sorgt lediglich dafür, dass die abstrakte Basisklasse in möglichst natürlicher Form instanziiert werden kann.

Technisch ist dieses Vorgehen unproblematisch, aber mental bereitete es mir Probleme: Seit Jahren wird Entwicklern vor allem in der akademischen Welt impliziert, dass Code nur dann “gut” ist, wenn er kompakt, elegant und im mathematischen Sinne schön ist.

Wie sind nun Änderungen, die nur um einer besseren Testbarkeit durchgeführt werden, mit diesem Paradigma vereinbar? Die kurze, aber schmerzhafte Antwort lautet: Gar nicht.

Wie bereits in Wie viel Sinn machen Unittests? angesprochen, beschreibt auch Roy Osherove in seinem Buch The Art of Unit Testing in dem Abschnitt Overcoming the encapsulation problem dieses Problem:

Some people feel that opening up the design to make it more testable is
a bad thing because it hurts the object-oriented principles the design is
based on. I can wholeheartedly say to those people, “Don’t be silly.”

Seine zugegebenermaßen sehr pragmatische Lösung

“Don’t be silly.”

dieses Problems stellt für mich keine zufriedenstellende Antwort dar – schließlich kommen die Bedenken nicht von ungefähr, und diese Antwort spiegelt dies in keinerlei Hinsicht wieder.

Auch die im Gespräch mit einigen anderen Entwicklern geäußerte Ansicht

Das ist halt so.

ist nicht zufriedenstellend. Diese Aussage beschreibt nämlich lediglich den Status Quo, begründet aber nicht, warum es in Ordnung oder vielleicht sogar gut ist, auf diese Art vorzugehen. Letztlich spiegelt diese Aussage lediglich das resignierte Abfinden mit einer – potenziell – unbequemen Tatsache dar.

Doch nun hat Ralf Westphal eine Begründung geliefert, die den Ansprüchen der Skeptiker – zumindest aus meiner Sicht – genügt:

das ist völlig ok. maschinen haben ja auch revisionsklappen für wartungsarbeiten. die haben sonst keine funktionalität. oder denk an kragsteine an alten kirchen, damit man gerüste anbringen kann.

Nach einigen Jahren des Zweifels und der Skepsis empfinde ich diese Analogie als eine wahre Erlösung – endlich wird begründet, warum ein solches Vorgehen in der Software in Ordnung ist.

Man mag einwenden, dass dieser Vergleich zur realen Welt doch offensichtlich und naheliegend sei, doch war in den vergangenen Jahren sonst niemand, mit dem ich mich über dieses Thema unterhalten habe, in der Lage, einen ähnlich klaren, einleuchtenden Vergleich zu ziehen.

Persönlich bemerke ich, dass diese Erkenntnis eine Art Wendepunkt darstellt – die mentale Barriere, mit der ich mich bislang gegen Unittests gesträubt habe, weil sie Anpassung von Code erfordern, ist aufgelöst. Alles, was bleibt, sind technische Probleme – und diese können mit Sicherheit gelöst werden.

Als Konsequenz werde ich das umsetzen, was Ralf Westphal vorgeschlagen und empfohlen hat:

Unit Tests […] konsequent und kompetent mal für einen Monat einzusetzen. Mit der Kompetenz mag es da schwierig sein, weil man sich dann eine Veränderung der Handlungs- und Denkgewohnheit selbst beibringen muss... aber das ist zumindest ein Anfang.

Wie es mir damit ergeht – darüber werde ich berichten …

Stirnrunzeln über “Bugtracking ist sinnlos”

Mittwoch, 21. Oktober 2009, 09:33 Uhr
Permalink | Kommentare (5) | Kommentare als RSSRSS

Ilker Cetinkaya hinterfragt in seinem aktuellen Blogeintrag den Sinn von Bugtacking und schließt mit einem provokanten Fazit:

Bugtracking ist sinnlos!

Für ihn steht der Verwaltungsaufwand, der für ein entsprechendes System notwendig ist, in keinem Verhältnis zum Nutzen – zumindest dann nicht, wenn man ohnehin agile Methoden wie XP oder Scrum einsetzt.

Sein Vorschlag zum Umgang mit Bugs lautet:

Wenn ein Bug wirklich ein wichtiger Bug ist, dann wird er sofort gefixed. Alles andere ist entweder nicht wichtig oder wird dann erst wichtig, wenn die wichtigeren Dinge schon erledigt sind.

Doch nicht nur das Bugtracking, auch das –reporting hinterfragt Ilker. So schlägt er zusätzlich vor,

ein leichtgewichtiges Bugreporting [zu] etablieren und die Entscheidung über den Fix oder No-Fix des Bugs schnell herbei[zu]führen

Insgesamt verwundern mich diese Vorschläge sehr, denn es gibt einige Aspekte, die Ilker vollständig ignoriert: Die Entscheidbarkeit, die Nachvollziehbarkeit und zuletzt auch die Transparenz.

Er geht davon aus, dass für jeden Bug sofort entschieden werden kann, ob dessen Relevanz genügt, um zeitnah behoben zu werden. Die Entscheidung, ob ein Bug dermaßen relevant ist, dass er sofort beseitigt wird, wird in der Regel möglich sein.

Im Endeffekt bedeutet das nämlich, dass all jene Bugs gefixt werden, die bei Verwendung eines Bugtrackingsystems mit oberster Priorität und als höchst kritisch gekennzeichnet worden wären.

Die Frage ist aber, was mit einem Bug geschehen soll, der diese Kriterien nicht erfüllt. Dazu schlägt Ilker vor:

Ist er nicht wichtig genug (also ist entweder ein anderer Bug oder ein anstehendes Feature wichtiger), so wird der Report weggeworfen.

Interessanterweise schreibt er direkt im nachfolgenden Satz:

Der Benutzer wird benachrichtigt dass der Fehler momentan nicht korrigiert wird.

Beachtenswert an diesem Satz ist für mich das Wort momentan – das heißt, Ilker räumt ein, dass es sinnvoll sein könnte, den Bug zu einem späteren Zeitpunkt zu fixen. Doch wie merken sich die Entwickler diesen Bug und dessen Details, wenn sie ihn nirgends notieren dürfen?

Es liegt auf der Hand, dass dabei über kurz oder lang Bugs vergessen werden, oder dass zumindest Details verloren gehen, die zur Behebung potenziell wichtig gewesen wären.

Interessant finde ich auch, welches Resultat Ilker anführt: Durch den Verzicht auf Bugtracking und die sofortige Entscheidung wird direkt an den Benutzer kommuniziert, ob der von ihm gemeldete Bug behoben wird oder nicht. Dies nennt er:

Ehrlich, sachlich, klar.

Ohne Frage wird die Transparenz über die sofortige Entscheidung erhöht – jegliche sonstige Transparenz geht allerdings verloren – nicht nur für die Entwickler, auch für den Benutzer:

  • Ohne Bugtracking weiß niemand, welche Bugs vor langer Zeit abgelehnt wurden, insbesondere aus welchen Gründen dies geschehen ist.
  • Ohne Bugtracking weiß niemand, wie oft welcher Bug auftritt – dies kann man nur noch nach persönlichem Gutdünken schätzen.
  • Ohne Bugtracking bekommt der Benutzer nur die Aussage, dass sein Bug momentan nicht behoben wird – was damit zukünftig geschieht, kann er nicht nachverfolgen.
  • Ohne Bugtracking weiß niemand, wie viele gefundene, aber ungefixte Bugs es in einer Software gibt.

Diese Liste ließe sich noch lang fortsetzen. Worauf ich hinauswill, wird aber deutlich: Bugtracking ist nicht nur nicht sinnlos, es ist sogar enorm wichtig für den Erfolg eines Projekts!

In einem einzigen Punkt kann ich Ilker allerdings dennoch zustimmen:

[…] wenn man wirklich das Produkt stetig weiterentwickeln möchte auch immer sich zum Ziel setzen muss, ein möglichst fehlerfreies Produkt auszuliefern.

Dieser Aussage stimme ich vollkommen zu – lediglich die Konsequenzen, die er daraus zieht, kann ich weder nachvollziehen noch gutheißen.

Wozu überhaupt Programmierstandards?

Donnerstag, 27. August 2009, 09:45 Uhr
Permalink | Kommentare (1) | Kommentare als RSSRSS

Gestern habe ich in meinem Blogeintrag Accuracy unter anderem versucht, die Frage zu beantworten, warum Peter Bucher und mir Programmierstandards und deren strikte Einhaltung so wichtig sind:

Aus diesem Grund erachten auch Peter und ich Programmierstandards als dermaßen wichtig: Sie erfüllen eben nicht nur die im Extreme Programming verfolgte Intention, gemeinsame Verantwortlichkeit zu etablieren, sondern ermöglichen uns auch, den Code des anderen schneller zu verstehen und uns gegenseitig zu helfen.

Im Grunde steckt darin auch schon die Antwort auf die im Titel dieses Blogeintrages gestellte Frage: Wozu sollte man sich als Entwickler an Programmierstandards halten?

Nun mag der ein oder andere einwenden, dass er ohnehin nicht im Team, sondern nur alleine entwickelt, dass für ihn also das Argument der Etablierung einer gemeinsamen Verantwortlichkeit nicht zählt. Doch selbst dann bin ich der Meinung, dass es durchaus Sinn ergibt, sich an Programmierstandards zu halten.

Warum? Dafür gibt es mehrere Gründe:

  • Verstehen: Auch einem allein arbeitenden Entwickler helfen Programmierstandards, sich auf das Wesentliche zu konzentrieren und der äußeren Form zunehmend weniger Aufmerksamkeit schenken zu müssen, da er sie nach und nach verinnerlicht. Dennoch degeneriert die Form nicht, so dass der Code auch mittel- und langfristig gut lesbar und damit verständlich bleibt.
  • Fragen: Viele Entwickler suchen – auch wenn sie prinzipiell alleine arbeiten – ab und an Hilfe in der Community, sei es in Foren oder in Newsgroups. Sofern dann Code gepostet werden muss, ist es für die Antwortenden ausgesprochen hilfreich, wenn sich der Autor an gängige Programmierstandards gehalten hat – das Verständnis fällt leichter, als wenn man sich erst durch die Syntax kämpfen muss, bevor man die eigentliche Frage nachvollziehen kann. Die Einhaltung der Programmierstandards erleichtert in diesem Fall also die Kommunikation nach außen.
  • Lernen: Analog dazu vereinfacht die Gewöhnung an Programmierstandards auch die Kommunikation nach innen, denn die meisten Autoren halten sich ebenfalls an gängige und etablierte Standards.

Unabhängig von der Frage, ob man sich an solche Standards halten sollte, ist übrigens die Frage, welche Standards denn als Referenz gelten sollen: Hier gibt es schließlich – je nachdem wen man fragt oder in welchem Projekt man beteiligt ist – ausgesprochen unterschiedliche Auffassungen.

Als Beispiel hierzu kann ein einfaches Hallo Welt-Programm dienen, einmal in C# und einmal in Java. Obwohl es sich bei C# und Java um objektorientierte Sprachen handelt und sich das grundlegende Vorgehen prinzipiell sehr ähnelt, weisen beide Quellcodes doch einige Unterschiede auf.

Während es in C# beispielsweise etabliert ist, geschweifte Klammern in einer eigenen Zeile zu positionieren und Methodennamen groß zu schreiben, gilt in Java das genaue Gegenteil. Zweifelsohne kann ein Entwickler, der C# beherrscht, das Java-Programm problemlos lesen und verstehen – ebenso umgekehrt.

Sobald jedoch Modifikationen vorgenommen werden sollen, fällt die Einhaltung der jeweiligen Standards schwer: Es wird vorkommen, dass geschweifte Klammern falsch gesetzt werden, was dann letztlich die Lesbarkeit verschlechtert.

Natürlich kann man nicht sagen, dass das eine richtig und das andere falsch wäre – es handelt sich bei Programmierstandards immer nur um formale und vor allem willkürliche Übereinkünfte zwischen mehreren Entwicklern: Man hätte es ebensogut anders machen können, hat sich aber für eine Variante entschieden.

Daraus kann man dann die erste Regel im Zusammenhang mit Programmierstandards herleiten: Prinzipiell ist es egal, wie Code formatiert wird – so lange die Formatierung einheitlich durchgeführt wird.

Nichtsdestotrotz haben sich einige Programmierstandards mehr und andere minder etabliert. Microsoft selbst schlägt für C# entsprechende Regeln vor, die im Kontext von .NET etabliert sind und als allgemein anerkannt gelten: Sofern von diesen Best Practices abgewichen wird, sollte dies wohlüberlegt und begründet geschehen.

Peter und ich haben uns daher für die nächsten Wochen vorgenommen, die von uns verwendeten Programmierstandards vorzustellen, zu erläutern, warum wir gewisse Dinge so und nicht anders machen, und hoffen, das Thema Programmierstandards auf diesem Wege dem ein oder anderen Entwickler schmackhaft machen zu können.

Wie bereits erwähnt sind natürlich auch die von uns vorgestellten Programmierstandards nur eine mögliche Variante von vielen – sie haben sich jedoch in unserer tagtäglichen Praxis als nützlich erwiesen und etabliert.

Extreme Programming

Dienstag, 18. August 2009, 22:58 Uhr
Permalink | Kommentare (0) | Kommentare als RSSRSS

Keine tausend Meter von der Technischen Universität Kaiserslautern entfernt liegt das Fraunhofer-Institut für Experimentelles Software Engineering (IESE). Obgleich der Name auf Grund des Wortes experimentell zunächst unwissenschaftlich wirkt, befasst sich das IESE mit einer ausgesprochen anspruchsvollen Thematik: Der Erforschung von Methoden und Prozessen zur industriellen Softwareentwicklung.

Der Leiter des IESE, Prof. Dr. Dieter Rombach, zählt zu den von mir – und nicht nur während meines Studiums – am meisten geschätzten Professoren: Er hat meine Sicht auf Softwareentwicklung im Allgemeinen und das ingenieursmäßige Vorgehen dabei wesentlich geprägt.

Das Studium der Angewandten Informatik an der TU Kaiserslautern war – zumindest während meiner Studienzeit – derart strukturiert, dass das Grundstudium um eine Serie von drei Vorlesungen namens Entwicklung von Softwaresystemen aufgebaut war: Darin wurden die Grundlagen der Programming an Hand von Java vermittelt – angefangen bei den grundlegenden OOP-Konzepten bis hin zu Multithreading.

Was Entwicklung von Softwaresystemen für die Entwicklung im Kleinen war, waren die darauf aufbauenden Vorlesungen Software Engineering während des Hauptstudiums für die Entwicklung von großen Softwaresystemen. Wurde der Schwerpunkt während des Grundstudiums noch auf das Schreiben von Code gelegt, ging es nun um Methoden und Prozesse der Softwareentwicklung.

In einer dieser Vorlesungen habe ich den Begriff Extreme Programming zum ersten Mal gehört: Damals entwickelte sich an Hand dieses Begriffs eine interessante und spannende Diskussion über das Für und Wider agiler Vorgehensweisen.

Für lange Zeit danach war Extreme Programming für mich jedoch nicht mehr als ein Schlagwort, dessen nähere Beschäftigung nicht lohnte – wurden agile Methoden doch häufig als unseriös oder kaschierende Bezeichnung für bewusst in Kauf genommenes Chaos abgetan.

In den vergangenen Jahren habe ich nun immer wieder festgestellt, dass sich meine Art zu entwickeln verändert hat: Man könnte sagen, dass eine gewisse Professionalisierung eingetreten ist – zumindest zu einem bestimmten Grad. Allerdings ist das Ende der Fahnenstange dabei noch lange nicht in Sicht, nach wie vor lerne ich täglich neues dazu.

Interessant an dieser Entwicklung ist, dass es nicht einen einzelnen Faktor gibt, an dem ich dies festmachen könnte – statt dessen gibt es eine ganze Reihe von Faktoren, die mich beeinflusst haben: Das Spektrum reicht dabei von Büchern wie C# in Depth oder Framework Design Guidelines über Personen wie Ralf Westphal oder Peter Bucher bis hin zu Initiativen wie Clean Code Developer.

Vergangene Woche habe ich nun das Buch eXtreme Programming von Henning Wolf, Stefan Roock und Martin Lippert gelesen, weil ich zum einen endlich wissen wollte, was genau sich hinter diesem Begriff verbirgt, und weil ich zum anderen einen Einstieg in das Thema agiler Methoden gesucht habe.

Dabei habe ich festgestellt, dass Extreme Programming ziemlich genau dem Vorgehen entspricht, das ich mir in den vergangenen Jahren angeeignet und für mich als meine persönliche Best Practice erkoren habe. Die meisten der im Extreme Programming vorhandenen Methoden setze ich bereits ein, insbesondere auch in Zusammenarbeit mit Peter Bucher:

  • Programmierstandards, gemeinsame Verantwortlichkeit, fortlaufende Integration – all das sind für uns Selbstverständlichkeiten in unserer Entwicklung geworden. Da wir auf Grund der räumlichen Distanz kein Programmieren in Paaren durchführen können, haben wir Reviews als ständigen Prozess für uns etabliert – was seinerseits wiederum der gemeinsamen Verantwortlichkeit zu Gute kommt.
  • Auch den Punkt des einfachen oder besser gesagt des iterativen Designs schätzen wir enorm: Wir gestalten unsere Architekturen derart, dass sie nicht mehr erfüllen als das derzeit benötigte, dass sie aber für Erweiterungen offen sind. Gerade dieser Aspekt spielt nicht nur für uns eine sehr große Rolle, sondern auch für das Extreme Programming.
  • Einige der in Extreme Programming genannten Methoden wie Retrospektive, Testen oder Planungsspiel setzen wir derzeit noch zu wenig ein – wir sind uns dessen aber bewusst und steuern dieses Ziel an.

Nimmt man all dies zusammen, so kann ich sagen, dass Extreme Programming sehr gut funktioniert – wenn man das richtige Team hat, und das Projekt die erforderliche Agilität zulässt, und zudem eine gewisse Größe nicht übersteigt.

Aus dieser Erkenntnis ziehe ich für mich den Schluss, auf dem richtigen Weg zu sein und diesen Weg auch in Zukunft fortzuschreiten und auszubauen.