Statische vs. Dynamische Typisierung: Overengineering im Kleinen?

10 Juni 2010 von Heiko Kommentieren »

Ein anderes Flurthema auf der JUG-KA war mal wieder statisch vs. dynamisch typisierte Programmiersprachen (konkret: Ruby und Python vs. Java)
Die Java-Fraktion fühlt sich sicher dank Typüberprüfung schon zur Compilezeit oder während der Code-Erstellung und  fühlt sich wohl durch die IDE, die Code Completion und die Refactoring Tools.

Die Sicht eines LessCoders ist hier aber eine andere. Fragen wir uns doch mal: Was sind die wirklichen Vorteile und mit welchem Preis erkauft man sich die?

Klar, Typüberprüfung zur Compilezeit ist in normalerweise eine wasserdichte Sache, zur Laufzeit gibt es keine Typfehler – wenn man nicht gerade selbst eine falsche Typenumwandlung vornimmt und nicht viel Dynamik, also Reflection und Dynamic Invocation etc. im Code hat, oder Klassennamen-Referenzen außerhalb des Codes, z.B. in einer XML-Config.
In meiner inzwischen 3-jährigen Praxis mit Skriptsprachen stelle ich aber fest, dass Laufzeitfehler aufgrund falscher Typisierung in der Tat sehr selten vorkommen, und wenn, dann schon sehr früh meist direkt nach dem erstmaligen Start von frisch geschriebenem Code. Der Grund für Fehler dieser Art ist dann oft, dass einer Methode mit vielen Parametern einer zu wenig oder zuveil übergeben wurden. So etwas ist aber sehr schnell gefunden und korrigiert.
Da man aber sowieso den Code mit automatisierten Tests versieht, sind Fehler dieser trivialen Art sehr einfach zu finden und zu beheben.
Früher war ich der Meinung, dass es in der Praxis einen großen Unterschied gäbe zwischen einem in der IDE in Echtzeit grafisch rot unterkringelten Fehler und einem Fehler, der erst zur Testzeit auftritt. Dem ist aber nicht so, das macht kaum einen Unterschied in der Praxis – fast im Gegenteil, man lässt sichnämlich  nicht durch vorübergehende Rot-Unterkringel-Fehler ablenken, nur weil noch Teile vom Code fehlen.

Heute entwickle ich aber in aller Regel so, dass ich die gerade zu bearbeitenden Codestellen im Kontext von Tests regelmäßig zur Ausführung bringe und dann aus einer Laufzeitfehler-Meldung direkt an die richtige Stelle im Code springe.
Daher bin ich inzwischen der Meinung, dass dies in der Praxis kein großer Nachteil ist. Insbesondere deshalb, weil man in untypisierten Sprachen sowieso viel weniger Code hat, den man pflegen muss. Ja, der Vorteil einer typsicheren Sprache ist eindeutig, aber ich schätze ihn aus meiner Erfahrung heraus nicht als sonderlich wertvoll ein.

Kommen wir nun zu dem Preis, den man sich durch Typisierung erkauft: Die Sprachsyntax ist in Ihren kleinsten Strukturen schon alles andere als DRY (Don’t repeat yourself). Und durch superaufwendige Features in IDEs, durch MDA usw. wird nun versucht dieser schier endlosen Wiederholung von Methodensignaturen und Typdefinitionen Herr zu werden.
Wer kennt nicht die typischen Javaklassen mit einer Zeile Businesscode (z.B. einer Definition zur Sortierung von Elementen einer Liste), die aber aus 5 Zeilen besteht?

import example.Employee;
import java.util.Comparator;
public class EmployeeIdComparator implements Comparator<Employee>{
   public int compare(Employee e1, Employee e2) {
     return e1.getId().compareTo(e2.getId());
  }
}

In Ruby, meiner Haus- und Hofsprache sieht das z.B. so aus:

employee_id_comparator = Proc.new { |e1,e2| e1.id<=>e2.id}

und die Wiederverwendbarkeit dieses Konstrukts (in Ruby ein sogenannter Block) ist durch die Nichttypisierung sogar noch viel höher, da alle Elemente, die eine id-Methode haben, verwendet werden können.
Um diese Wiederverwendbarkeit in Java zu erreichen müsste man den EmployeeIdComparator nun generalisieren, indem man Employee abstrahiert zu einem Interface, das getId() definiert hat, deklarieren, dass Employee dieses Interface implementiert, usw.

Und der LessCoder fragt sich, ob das nicht schon wieder der Anfang eines „Overengineering“ ist? Denn wenn Tests vorhanden sind, ist der Umgang mit falschen Typen fast genauso einfach wie bei typisierten Sprachen. Erscheint der Preis einer typisierten Sprache dafür nicht zu hoch?
Ich freue mich auf eure Kommentare dazu.

4 Kommentare

  1. Robert sagt:

    Also ich schreibe relativ viel in Python, aber mich kotzt die extrem schlechte Dokumentation dazu ziemlich an. Die Dynamische- (bzw. Nicht-)Typisierung scheint dahingehend einen ziemlichen Beitrag zu leisten. Wenn in der Doku zu irgendnem Modul steht, dass diese und jene Funktion irgendwelche Objekte zurückgeben, ist oft völlig unklar, von welchem Typ diese sind – naja, also wie sie beschaffen sind und was man mit ihnen machen kann. Das sind dann die Stellen, wo es besonders zu Typfehlern kommen kann – bei einem noch unbekannten Libraries/APIs.
    Wenn man bei Java in den JavaDoc-Output reinschaut (oder eben ne IDE verwendet) ist alles schön klar und eindeutig. Aber insgesamt bloated Statische Typisierung den Code natürlich schon immer extrem auf.
    Also ich kann für mich nicht so richtig sagen, was mir besser gefällt.

  2. Für mich ist die Kombination von Statischer Typisierung und immutable classes ein absoluter Killer. Was nutzt dir die Definition eines Interfaces mit getId Methode, wenn du nicht nachträglich alle Klassen damit ausstatten kannst, die die Methode ohnehin anbieten? Alles, was du dahingehend in Java anstellen könntest, riecht massiv nach Workaround.

    Noch ein kleiner Tipp:
    id_comparator = :id.to_proc
    employees.sort_by &id_comparator
    oder einfacher:
    employees.sort_by &:id

    Wollte nur mal Klugscheißen ;o)

  3. Heiko sagt:

    @Mihael: Ja, bei typtisierten Sprachen hat man typischerweise „deterministisches und sicheres Refactoring“. Der Punkt, den ich mit diesem Blogpost zeigen möchte, ist, dass Refactoring auch mit dynamischen Sprachen genau dann fast so einfach ist, wenn eine vernünftige Testinfrastruktur gepflegt wird (was aus vielen anderen Gründen sowieso sinnvoll ist). Dass dies nur die zweitbeste Lösung ist gegenüber dem wirklich sicheren Refactoring von Java ist klar.
    In der Gesamtbetrachgung überwiegen für den LessCoder jedoch trotzdem die Vorteile die sich aus dynamischen Sprachen ergeben.
    Gestern hatte ich gerade wieder eine interessante Diskussion zu diesem Thema. Ergebnis war, dass deutlich häufiger am Infrastrukturcode als am BusinessCode refactored wird. Z.B. soll aus einem Singleton eine mehrfach instanziierbare Klasse werden. Da bei dynamischen Sprachen das Verhältnis zwischen Businesslogik und Infrastrukturcode typischerweise aber meist sehr gut ist (d.h. deutlich weniger Infrastrukturcode) gibt es auch entsprechend weniger zu refactoren.
    Zudem bieten viele dynamische Sprachen flexiblere Reaktionsmöglichkeiten auf Gegenheiten, unter denen in Java im größeren Stil refactored werden müsste. Das Hinzufügen eines Parameters in einer Methode z.B. lässt sich in Ruby und Groovy evtl. mit Vergabe von Defaultwerten realisieren. Und meist sorgen auch schon die Best-Practices oder der Stil der dynamischen Programmiersprache für mehr Flexibiltät, z.B. bekommen komplexe Methoden in Ruby statt einer langen Parameterliste gerne einen Hash mit Schlüsselwert-Paaren übermittelt, auf die sie dann entsprechend flexibel reagieren.
    Wenn man z.B. eine Methode umbenennen möchte, dann greift man gerne auf die Volltextsuche zurück und tut gut daran, vorher aussagekräftige Namen vergeben haben (also nicht viele unabhängige Methoden einfach „doIt()“ genannt zu haben).
    Scala zeigt in der Tat, wie eine Sprache aussehen könnte, die beide Welten sinnvoll vereint. Es ist sicher nicht so, dass der LessCoder Typisierung nicht mag. Er mag sie nur nicht, wenn der Preis dafür nicht angemessen ist…

  4. Mihael Vrbanec sagt:

    Ich gebe Dir recht, dass der Preis für Typsicherheit in Java (zu) hoch ist. Jedoch ist für mich der Hauptgewinn von Java (und den vielen guten IDEs) deterministisches und sicheres Refactoring. Ich persönlich hoffe, dass sich neuere statische Sprachen wie Scala mittelfristig durchsetzen, weil sie statische Typisierung und die Ausdrucksstärke sowie Kürze von dynamischen Sprachen vereinen.

Schreibe einen Kommentar