Brooks‘ law

Adding manpower to a late software project makes it later.

Neue Mitarbeiter in Projekte hineinzustecken bedeutet immer zusätzliche Kommunikation und die Produktivität des Teams wird reduziert, weil es die neuen Mitarbeiter einarbeiten muss. Wenn das Team dann schon dem Zeitplan hinterherläuft, wird die Situation nur schlimmer. Außerdem ist oft das Team schon groß genug und die Entlastung durch neue Mitarbeiter geringer als der Einarbeitungsaufwand. Für ausreichend große Teams gibt es daher folgende lustige Variante dieser Regel „Eine Frau braucht neun Monate um ein Baby zu bekommen; neun Frauen auch“

 

 

Hierarchien

Schon bemerkt, dass auf jeder Konferenz folgender Vortrag erscheint?

  • Scrum eingeführt
  • Enttäuschung wegen fehlender positiver Effekte
  • Externer Berater streicht die Hierarchien
  • Die Firma nimmt fahrt auf

Verfolgt man diese Vorträge, dann ist es offensichtlich was falsch läuft. Die alten Hierarchien regieren weiter und die positiven Effekte von Scrum können nicht zum Tragen kommen.

Verwunderlich ist nur, warum die Verantwortlichen dieses eklantante Problem nicht sehen. Wollen sie Besitzstände nicht anrühren, verstehen sie den Prozess nicht oder vertrauen sie nur ihren Führungskräften?

 

Tolle Tests

schaut man in die Ansammlungen von Unit Tests, die sich im Laufe der Zeit in Projekten anhäufen, stellt man häufig folgendes fest:

  • Es ist nicht klar, was alles getestet wird. Weder der Name noch der Inhalt der Methode lassen direkt auf den Zweck schließen
  • Die Konstruktion der Testdaten ist in andere Klassen ausgelagert
  • Der Abgleich der Testausgabe gegen die Erwartungswerte ist spartanisch
  • Die Tests verwenden den zu testenden Code

Was zeichnet einen guten Test aus?

Eine Testmethode baut zuerst die Vorbedingung für den Test auf, indem die notwendigen Datenstrukturen initialisiert oder durch Mock-Objekte simuliert werden. Danach erfolgt der Aufruf der zu testenden Methoden um dann den erreichten Zustand oder die Ergebnisse der Berechnung zu prüfen.

Die Tests sollen kurz und kompakt sein, weil sonst die Verständlichkeit leidet. Testmethoden werden sehr selten angeschaut, daher ist die schnelle Verständlichkeit eine der wichtigsten Anforderungen.

Viele kleine Tests, die jeweils eine einzelne Eigenschaft prüfen, sind einigen großen Tests vorzuziehen, weil sich so das Problem für den Entwickler sehr viel schneller analysieren lässt. Schlagen mehrere Tests fehl, gibt das dem Entwickler mehr Anhaltspunkte für eine gezielte Fehlersuche.

Für die Voraussetzungen werden nur die notwendigsten Daten erzeugt, denn alle weiteren Daten sind unnötig und ihre Erzeugung kostet Zeit. Wenn mehrere Tests die gleiche Funktionalität testen und auf den gleichen Daten arbeiten, dann kann man diese in einer Tests Klasse gruppieren und eine gemeinsame Initialisierungsmethode nutzen. In JUnit sind dafür z.B. Methoden vorgesehen, die mit @Before und @BeforeClass annotiert sind.

Die Tests sollten vor der Entwicklung geschrieben werden, um die Anforderungen an den Produktionscode zu beschreiben. Eine Implementierung unter Beachtung dieser Tests, garantiert zum einem die Testbarkeit, aber man gewinnt dadurch gleichzeitig auch eine compilierbare Spezifikation des Produktionscodes.

Test Anti-Pattern

Auch bei der Entwicklung von Tests kann man eine Menge falsch machen. Ein paar gängige Fehler sind hier mal unter dem Label Test Anti-Pattern aufgelistet.

Code Reuse

Da die Unit Test meist von den Entwicklern der Software geschrieben werden, greifen diese auch immer wieder auf existierenden Programmcode zurück.  Dies kann fatale Folgen haben, wenn der Erwartungswert im Test identisch zum Programmergebnis erzeugt wird. Der Test ist erfolgreich, aber das Programm arbeitet dennoch fehlerhaft.

assertThat(document.getElementById(Constant.ID).getLocalName(), equalTo(Constant.START_TAG));

In diesem Beispiel wird geprüft, ob ein Element mit der ID aus Constant.ID in einem XML Dokument den Namen hat, der in der Konstanten Constant.START_TAG definiert wurde. Obwohl der Test erfolgreich ist, kann die XML Datei fehlerhaft sein, wenn die Konstanten fehlerhaft definiert wurden. Besser ist hier immer:

assertThat(document.getElementById("id").getLocalName(), equalTo("start"));

Uneindeutige Prüfungen

Mancher Entwickler freut sich schon, wenn seine Methoden ein Ergebnis liefern, das nicht null ist, oder die Anzahl der Ergebnisse einen bestimmten Wert hat. Im ersten Moment scheint dies meist ausreichend, bei späteren Erweiterungen tauchen dann aber die Probleme auf. In der Regel schlagen die Test bei Anpassungen fehl, weil es mehr oder weniger Einträge gibt. Aber wie lange bleibt ein Fehler unentdeckt, wenn die Liste die gleiche Länge behält, aber falschen Einträge beinhaltet?

assertEquals(3, getAdminUsers().size());

Liefert diese Methode nun die falschen drei User, dann fällt der Fehler im Produktionscode nicht auf. Hier sollte man besser die tatsächlich zurückgelieferten User prüfen. Der Einsatz der Hamcrest Bibliothek liefert hier das Handwerkszeug für sehr kompakte Prüfungen.

assertThat(getAdminUsers(), contains(hasName("Tick"), hasName("Trick"), hasName("Track")));

Tests in Schleifen und bedingenten Anweisungen

Manche Prüfungen in Testmethoden sind kompliziert und beinhalten Schleifen und Bedingungen. Sie erschweren nicht nur das Verständnis, sie sorgen häufig auch für fehlerhafte Prüfungen.

for (User user : getAdminUsers()) {
  assertTrue(user.isAdmin());
  assertEquals("T", user.getName().substring(0, 1));
}

Hier wird geprüft, ob die Namen der User in der Admin Liste alle mit einem „T“ beginnen und ob das Admin Flag gesetzt ist. Leider wird nicht geprüft, ob überhaupt ein User in der Liste ist. Bei einer leeren Liste wird keine einzige Prüfung vorgenommen. Solche Prüfungen schreibt man besser um, damit keine Schleifen und Bedingen mehr vorkommen.

assertThat(getAdminUsers(), everyItem(both(isAdmin()).and(hasName(startsWith("T")))));

Testdaten Generatoren

Wenn die Testdaten von verschiedenen Methoden verwendet werden sollen, dann kann man deren Generierung in eine Initialisierung Methode auslagern oder sich ein paar einfache Utility Methoden schreiben. Manches Mal mutiert aber dieser Ansatz wie ein Krebsgeschwür und aus den Hilfsmethoden erwächst ein ganzer Zoo von Hilfsklassen, der einem eigenen Framework gleichkommt. Dabei entgleitet den Teams der Überblick über die tatsächlich notwendigen Daten für einen einzelnen Tests und ein übergroßer universeller Satz wird für alle Tests verwendet. Das Team kann zwar immer sicher sein, dass für jeden Test die notwendigen Daten vorhanden sind, aber es verliert unnötig Zeit bei der Durchführung der Tests. Dadurch wird das Team ausgebremst und es ist außerdem ein Zeichen, dass ein Team die Kontrolle über die Software verliert.

Nur für die Tests geschrieben

Der Testcode ruft Methoden im Produktcode auf und prüft Ergebnisse die der Produktcode liefert. Dafür greift er an Stellen ein, die sich durch die lose Kopplung zwischen den Produktkomponenten ergeben. Bei Test Driven Development, also dem Schreiben von Test bevor der Produktcode entwickelt wird, verschränkt sich dieses Miteinander noch mehr. Den hier ergibt sich die lose Kopplung auch durch die Test.

Manche Entwickler fügen jedoch in den Programmcode öffentliche Konstanten, Methoden oder Konstruktoren ein, die Anfangs nur im Testcode verwendet werden. Meistens aus falsch verstandener Optimierung des Testcodes. Die Tests sind so zwar einfacher zu schreiben, aber es entsteht Produktcode, der nur im Testumfeld korrekt funktioniert.  Wird der Code nicht mehr benötigt, dann entsteht so schwer zu entdeckender toter Code oder dieser Code wird fälschlicherweise im Produktcode verwendet. Manchmal findet man ganze Klassen, die nur für die Tests im Produktcode vorgehalten werden.

 

 

Nur noch assertThat in Unit Tests

Mit JUnit 4.4 wurde die Hamcrest Bibliothek über die unscheinbare Methode assertThat Bestandteil des Test Frameworks. Obwohl schon einige Zeit vergangen ist, haben viele Entwickler das Potential dieser Ergänzung noch nicht entdeckt.

Bislang werden in den Unit Test die aktuellen Werte gegen die zu erwartenden Werte geprüft. Am beliebtesten sind da die Methoden assertEquals und assertTrue.

assertEquals("Hello World", object.toString());
assertTrue(file.exists());

In der ersten Zeile wird geprüft ob das Resultat der Methode toString mit dem Erwartungswert übereinstimmt und in der zweiten Zeile wird geprüft ob der übergebene Ausdruck true zurückliefert. Neben diesen beiden Methoden gibt es noch eine lange Liste weiterer Methoden, die unter anderen Arrays und Collections auf Gleichheit, Objekte auf Identität und reelle Zahlen auf einen Näherungswert testen. Zur besseren Lesbarkeit werden die assert Methoden über einen statischen Import eingefügt und der Klassenname Assert kann entfallen.

Der Ansatz bei der assertThat Methode ist ein anderer, hier wird ein Matcher  gegen das aktuelle Objekt ausgewertet. Die Schnittstelle ist sehr flexibel und erlaub eine Vielzahl von Prüfungen auf dem Objekt.

assertThat(object.toString(), is(equalTo("Hello World"));
assertThat(file, exists());

Bei dieser Methode steht das zu testende Objekt links und der Ausdruck, der erfüllt werden muss auf der linken Seite.  In der ersten Zeile wird mit dem equalTo Matcher geprüft, ob die Methode den Ausdruck „Hello World“ liefert und in den zweiten Zeile wird mit einem selbstgeschriebenen Matcher die Existenz der Datei geprüft.  Wenn man sich eine bessere Lesbarkeit wünscht, kann man wie oben geschehen den Matcher is einfügen.

Neben der guten Lesbarkeit und der Erweiterbarkeit trumpft der Matcher Ansatz aber noch mit einigen weiteren Vorteilen auf. Die Matcher sind typsicher und miteinander kombinierbar. Im folgenden sind die assertEquals und die assertThat im fehlerhaften Einsatz. Während  assertEquals unerkannt einen falschen Vergleich durchführt, kann der assertThat Ausdruck erst gar nicht kompilieren.

assertEquals(object.hashCode(), "HelloWorld");
assertThat(object.hashCode(), equalTo("Hello World"));

Die Matcher können auf viele Arten miteinander kombiniert werden. Eine sehr einfache Kombination ist die Verwendung des not Matchers oder die Anwendung eines Matchers auf eine Liste.

assertThat(file, not(exists()));
assertThat(listOfFiles, everyItem(exists());
assertThat(listOfFiles, hasItem(exists());

In der ersten Zeile wird geprüft, ob die Datei nicht existiert und in den beiden anderen Zeilen, ob jede Datei in der Liste oder mindestens eine Datei existiert. Häufig müssen mehrere Bedingungen auf einem Object geprüft werden, auch solche Prüfungen sind mit den bereitgestellten Matchern leicht formulierbar:

assertThat(marxBrother, anyOf(chico, harpo, groucho, gummo, zeppo));
assertThat(subject, either(pinky).or(brain));

Und wem die vorhandenen Matcher nicht ausreichen, der kann sich recht einfach eigene schreiben. Der oben vorgestellte exists Matcher könnte wie folgt formuliert werden.

public static Matcher<Path> exists() {
  return new FeatureMatcher<Path, Boolean>(Matchers.is(true), "exists", "exists") {
    @Override
    protected Boolean featureValueOf(Path actual) {
      return Files.exists(actual);
    }
  };
}

Die statische Methode exists liefert einen FeatureMatcher, der für ein Path Objekt true zurückliefert, wenn die Datei existiert, ansonsten false.

Abschließend kann man allen Entwicklern die Verwendung der assertThat Methode und dem Hamcrest Framework nur wärmsten ans Herz legen. Die Kombinierbarkeit und Erweiterbarkeit der Matcher führt zu übersichtlichen, kompakt formulierten Unit Test, die Entwicklungs- und Einarbeitungszeit sparen.

 

 

Cargo Cult Programming

Manche Entwickler halten Copy&Paste für das schlimmste Anti-Pattern in der Software Entwicklung. Leider muss man ihnen sagen, dass es noch schlimmer geht. Cargo Cult Programming ist die Verwendung von Pattern ohne sie zu verstehen. Dabei kopieren die Entwickler bestehenden Programmcode in der oft irrigen Annahme, dieser Programmcode wäre notwendig.

Der folgende Programmcode entstammt einer tatsächlichen existierenden Unit Test Klasse und wurde aus didaktischen Gründen ein wenig angepasst.

  protected void assertFolders(Path root, int expectedOrgs, int expectedSources) throws Exception {
    for (File file : root.toFile().listFiles()) {
      if (file.getName().startsWith("java")) {
        for (File file2 : file.listFiles()) {
          if (file2.getName().startsWith("src")) {
            int currentSources= 0; 
              for (File file3: file2.listFiles()) {
                currentSources++;
                if (file3.getName().equals("org")) {
                  Assert.assertEquals(expectedOrgs, file3.listFiles().length);
                }
              }
              Assert.assertEquals(expectedSrcs, currentSrcs);
            }
          }
        }
      }
    }
  }

Augenfällig ist insbesondere die tiefe Verschachtelung innerhalb dieser Methode. Hier wird innerhalb von Verzeichnissen nach einer speziell benannten Datei gesucht und das auf verschiedenen Ebenen. Woher auch immer das Grundmuster  stammt, hier wird es exzessiv durch Copy&Paste ausgelebt. Bevor wir aber etwas näher auf den Code und seine Schwachstellen eingehen, kommt hier erst einmal die korrigierte Version der Methode, an der man die gewollte Funktionalität ablesen kann.

  protected void assertFolders(Path root, int expectedOrgs, int expectedSources) throws IOException {
      Path src = root.resolve("java/src");
      Assert.assertEquals(expectedSources, src.toFile().listFiles().length);
      Path org = src.resolve("org");
      Assert.assertEquals(expectedOrgs, org.toFile().listFiles().length);
 }

In der ersten Zeile wird das Unterverzeichnis java/src bestimmt und dann die Anzahl der darin enthaltenen Dateien und Verzeichnisse geprüft. Danach wird sein Unterverzeichnis org bestimmt und die Anzahl der darin enthaltenen Dateien und Verzeichnisse geprüft. Es ist unschwer zu erkennen, dass diese Version kürzer und leichter zu lesen ist. Beide Methoden unterscheiden sich aber in einem sehr wichtigen Detail. Die korrigierte Version wirft einen Fehler, wenn kein Verzeichnis java/src existiert, während die ursprüngliche Version keinerlei Überprüfungen durchführt. Das in diesem Fall tatsächlich das Verhalten der korrigierten Methode erwünscht ist, basiert auf Insiderinformationen, kann aber im Allgemeinen mit Kaiser’s Razor abgeleitet werden.

Cargo Cult Programming äußert sich durch gedankenloses Verwenden von Vorgehensweisen, die nicht hinterfragt werden. Es werden keine APIs auf der Suche nach Alternativen durchsucht oder neue Ansätze erprobt. Durch das geringe Repertoire an Vorgehensweisen werden dann auch häufig schlechte und auch fehlerhafte Lösungen per Copy&Paste in den Code eingebaut. Gegen Cargo Cult Programming gibt es nur ein Heilmittel, das Gespräch unter Kollegen.

 

Kaiser’s Razor

Never assume software developer skills when stupidity will suffice

Basierend auf Hanlon’s Razor und dem Paretoprinzip,  besagt diese Regel, dass man grundsätzlich von fehlerhaften Sourcecode ausgehen sollte. Man investiert ansonsten zu viel Zeit, um nicht vorhandene Design Pattern oder absichtsvolle Algorithmen zu entdecken.

 

Atwood’s Law

Any software that can be written in JavaScript will eventually be written in JavaScript.

 

 

Des Kaisers neue Kleider

Wie verwandelt man eine Firma in eine agile Firma? Wenn man es schnell und billig haben will, dann kann man einen alten Kläpper mit schwarzer Schuhcreme in einen stolzen Rappen verwandeln. Der Traum endet jedoch beim ersten Regenguss und so ergeht es auch dem schnellen Start in die Agilität.

Der frühere Projektleiter wird zum Product Owner, das alte Team zum Scrum  Team und irgendjemand wird der Scrum  Master. Von Projektleiter, Architekt, QA Beauftragten und Senior Developer kann jeden der Fluch des (Teilzeit-) Scrum  Master treffen.

Auf den ersten Blick scheint es erst einmal nicht besonders befremdlich aus einem Team das Scrum Team zu machen und aus  dem Projektleiter den PO. Aber ohne große Unterstützung werden viele  in ihren neuen Rollen unglücklich und damit zu Bremsen im agilen Prozess.

Der Projektleiter führte ein Team, erstellte Pläne und wies Arbeiten  an. Nun hat er plötzlich die Aufgabe dem Team die Stories des Kunden zu verkaufen. In seinen Augen nur noch zu verkaufen. Aus einem Führer und Planer wird so ein Verkäufer. Nicht jedem Naturell gefällt dieser Wechsel.

Das klassische Entwicklungsteam  soll sich in ein Scrum Team verwandeln. Vielleicht wäre das Märchen vom hässlichen Entlein die bessere Metapher? Ein selbstorganisiertes, kreatives, unabhängiges, innovatives und selbstsicheres Team, dessen Mitglieder zuvor jahrelang die Aufgaben abarbeiten mussten, die ein Planer ihnen vorgesetzt hat. Die weder bei der Planung oder Umsetzung mitentscheiden durften. Die den Vorgabenkatalogen der Firma und Zeitplänen ihrer Vorgesetzt folgen mussten. Der ein oder andere wird jetzt in seinem Großraumbüro herumschielen und sich fragen, wie die Eigenbrödler, die kontaktscheuen Experten, die autistischen Überfliegen in ein Scrum Team integriert werden könnten.

Erst einmal müssen die Teams neu aufgeteilt werden. Die Zusammensetzung der alten Teams war ihren Projekten, der hierachischen Arbeitsweise, den Launen der Vorgesetzten und einer großen Priese Zufall geschuldet. Scrum Team müssen eine neue Mischung erhalten, die Expertise und Selbstbehauptung ermöglicht.

Damit dies möglich ist, benötigen Firmen starke Scrum Master. Keine Mitarbeiter, die aufpassen, dass die Meetings abgehalten werden, sondern Streiter für gesunde Team, Unterstützer aggressive Veränderungen und Mentoren und manchmal auch Therapeuten  aller Prozessbeteiligten.

Wenn eine Firma sich nicht wirklich bei der Scrum Einführung bemüht, steht sie irgendwann da, wie der Kaiser aus dem Märchen.

 

Warum denn nicht mal negativ?

Hin und wieder fragt  ein Kollege, warum er seinen Code umstellen soll. Er würde ihn gut verstehen und andere könnten sich doch auch reinfuchsen.

Das Einarbeiten in fremden Code ist aber immer wieder anstrengend und jede sinnlos vergeudete Minute summiert sich am Ende des Jahres in Tage oder gar Wochen.

Ein recht triviale Regel lautet daher, das Wichtige, die Ausnahme nach vorne stellen . Dadurch fallen diese Randbedingungen sofort ins Auge und man kann sich danach auf den eigentlichen Algorithmus konzentrieren.

Der naive Entwickler prüft gerne die regulären Bedingungen, um dann die Berechnung durchzuführen:

void test (String value) {
    if (value != null} {
        // hier ganz viel Code
    } else {
        throw new IllegalArgumentException ();
    }
}

Jenachdem wie lange der if Teil in der Methode ist, wird man durch das else am Ende etwas überrascht oder aber noch schlimmer, man übersieht es. Die einfache Lösung hier ist den boolschen Ausdruck zu negieren und die throw Anweisung nach vorne zu stellen. Vielen Entwicklern graust es davor einen boolschen Ausdruck umzuformen, obwohl es dafür schon lange feste Regeln gibt.

Durch die Throw Anweisung im if wird außerdem das else obsolet, da ja der andere Zweig mit einer Exception die Methode verlässt . Der Code wird lesbarer, da wir auf eine Einrückung verzichten können:

void test (String value) {
    if (value == null} {
        throw new IllegalArgumentException ();
    }
    // hier ganz viel Code
}

Normalerweise enthalten solche Methoden aber mehrere geschachtelt Prüfungen , die der Lesbarkeit zusätzlich schaden. Diese Verschachtelung lässt sich aber in der Regel, wie oben beschrieben beseitigen. Ein noch besserer Ansatz ergibt sich in solchen Fällen durch Delegation. Dazu aber ein andermal.

 

You aren’t gonna need it!

YAGNI ist wohl eines der Prinzipien, die am schwierigsten einzuhalten sind in einem Entwickler-Team. Insbesondere wenn die Kollegen sich weiterhin daran klammern, dass Änderungen immer aufwendig und zeitraubend sind.