Gute und schlechte Software-Praktiken gestern und heute

Gute und schlechte Software-Praktiken gestern und heute

Aus dem Blickwinkel von nahezu 40 Jahren Praxis in der Softwareentwicklung ziehen sich immer wiederkehrende gute und schlechte Praktiken wie rote Linien durch das Tätigkeitsfeld des Software-Entwicklers. Statt die schlechten Praktiken zu beseitigen, werden gerne neue Hypes und Buzzwords in den Ring geworfen. Von ihnen wird immer wieder die quasi automatische Lösung von Softwareproblemen erwartet. Dabei ist Software oftmals Auftragsarbeit, die die sogenannte "Bottom Line" eines Wirtschaftsunternehmen verbessern und den Kunden robuste und betreibbare Lösungen liefern soll. Ich möchte daher dafür plädieren, nicht zu viel Zeit mit der Suche nach dem Heiligen Gral in Form des aktuellen Hypes zu suchen, sondern die eigenen Fähigkeiten zu entwickeln, wie man robuste Software schreibt.

Illustrieren möchte ich das an einem hübschen kurzen Beispiel aus der Praxis, das ein hartnäckig verfolgtes Anti-Pattern zeigt. Eine Software zur Erstellung eines Suchindexes zeigt merkwürdiges Verhalten in Bezug auf die Handhabung mehrerer Sprachen. So landen im englischen Sprachindex deutsche Text, obwohl die Indizierungslogik die Sprache doch augenscheinlich korrekt setzt. Zentral ist hier eine Klasse Indexer, die in reduzierter Form ien Pseudocode wie folgt aussieht:


class Indexer  
{
  private int langId = 0;
  private String langIsoCode = null;


  public Indexer()
  {
  }

  public setLanguageId(int id)
  {
    this.langId = id;
  }

  public int getLanguageId()
  {
    return this.langId;
  }

  public setLanguageIsoCode(String isoCode)
  {
    this.langIsoCode = isoCode;
  }

  public String getLanguageIsoCode()
  {
    retrurn this.langIsoCode;
  }

  public index(String key)
  {
    String view = Database.getLanguageViewFor(this.langId);
    String value = Database.readFromView(view, key);
    this.sendToIndex(key, value);
  }

  private sendToIndex(key, value)
  {
    // not included here
  } 
}

Diese hübsche Klasse macht noch ein paar weitere Dinge (vielleicht keine klare "Separation of Concerns"?), aber sie wird wie folgt verwendet:

String keyToIndex = 'myKey';
Indexer indexer = new Indexer();

foreach (Languages: String langIsoCode) {
  indexer.setLanguageIsoCode(langIsoCode);
  indexer.index(keyToIndex);
}

Das sieht beim flüchtigen Lesen, vor allem, wenn es in eine grössere Code-Base eingebettet ist, vernünftig aus. Das Problem ist jedoch, dass Indexer zwei Member-Variablen langId und langIsoCode hat, die eigentlich einem Constraint unterworfen sein sollten (die Id muss immer zum ISO-Code passen). Dieser Constraint existiert jedoch nicht im Code, sondern bestenfalls im Kopf des Entwicklers und er wird durch die Konstruktion dieser Klasse nicht erzwungen. Die Probleme beginnen bereits in der Konstruktionsphase, die das Anlegen eines nicht-funktionsfähigen Objekts erlaubt. Die public Setter und Getter erlauben eine beliebige änderung von aussen, die den Zusammenhang von ID und ISO-Code verletzen können. Diese Probleme bleiben über den gesamten Lebenszyklus des Objektes bestehen: Der Verwender muss peinlich genau aufpassen, immer BEIDE Argumente vorher ÜBEREINSTIMMEND zu setzen. Somit hat diese Klasse auch ein großes Usability-Problem.

Am besten wäre es, Indexer als eine nicht-mutierbare Klasse anzulegen, die zudem nur entweder die ID oder den ISO-Code der Sprache als Konstruktor-Parameter nimmt und damit nur korrekt an gelegt werden kann und auch nicht mehr durch den Aufruf einer mutierenden Methode in einen inkorrekten Zustand versetzt werden kann. Wenn die Klasse schon mutierbar sein soll (was sehr oft vermieden werden kann), dann sollte es keine Mutation geben, die einen ungültigen Zustand erzeugt. Vorstellbar wäre ein Setter für entweder ID und Iso-Code oder ein Setter, der beide als Parameter nimmt und eine fehlerhafte Kombination der beiden Parameter erkennen kann.

Wie oft wurde aber ein Design wie das obige von einem Programmierer als erstrebenswert, weil “flexibel” verteidigt?. Schliesslich wird doch in vielen Büchern (die vielleicht nie hätten gedruckt werden sollen) genau solcher Stil vorgemacht. Da ich die Zahl der Bugs, die auf dieses Anti-Pattern zurückzuführen sind aber schon nicht mehr zählen kann, nenne ich es nur noch "Das Schicksal herausfordern". Die geliebte “Flexibilität” führt nämlich allzu oft dazu, dass eine Instanz einer solchen Klasse in einen ungültigen Zustand versetzt wird und der Zusammenhang zwischen Ursache und Wirkung oft nicht leicht zu erkennen ist, da beide in der Code-Base weit voneinander entfernt sind. Bei genauer Betrachtung handelt es sich bei dem geschilderten Anti-Pattern nämlich nicht um eine Flexibilisierung des Codes – z.B. durch Interfaces und Polymorphie – sondern um das Einbringen von Non-Determinismen.

Sehr viel mehr zur Robustheit von Software trägt hingegen das Inversion-Of-Control- oder Hollywood-Prinzip ("don't call us, we call you") bei. In der OO-Welt findet sich das in Frameworks bzw. in Form des "Template Method"-Patterns wieder, in der funktionalen Welt findet es sich in Erlang/OTP in sogenannten Behaviours wieder. Der Grund dafür, dass diese Entwurfstechnik zu robustem Code führt, ist einfach: Hier wird Wiederverwendung in bester DRY-Manier (Don't repeat yourself) betrieben und durch jede Ausprägung eines Frameworks (oder Behaviours) werden die Fehler in diesem reduziert. Angenehmer Nebeneffekt ist, dass das Verhalten einer Software, die auf solchen Prinzipien fußt, vorhersagbarer ist und kein unerwartetes Verhalten zeigt. Auf diese Weise fordert der Entwickler das Schicksal nicht mehr heraus, sondern begibt sich selber in dessen Kontrolle.

Das Interessante aus langjähriger Software-Praxis ist nun, dass es robuste Programmiertechniken schon lange gibt und dass sie auch in modernen Software-ökosystemen immer noch mit Erfolg angewendet werden. Trotzdem sind schlechte Praktiken immer noch verbreitet und eben deswegen auch so schwer auszurotten. Man muß wohl erst selber die Erfahrung gemacht haben, dass man sein Leben als Software-Entwickler nicht durch "flexible" Entwürfe erleichtert, sondern eher durch robuste. Der Grund dafür liegt darin, dass der Hauptaufwand nicht im schnellen Erstellen eines "Proof of Concepts" liegt, sondern in der sogenannten Wartungsphase, in der Fehler behoben und neue Anforderungen so realisiert werden müssen, dass sie möglichst keine neuen Fehler einführen.

Die Prinzipen hinter der Erstellung robuster, evolvierbarer Software können im Kurs "Product Line Engineering mit Frameworks" der Digicomp AG, Schweiz, vertieft werden.