Etwa 25 Enthusiasten aus der Region haben sich letzte Woche mit ihren Laptops in der Walhalla in Karlsruhe getroffen.
Das Thema Refactoring for Testability war nichts neues auf dem Radar, hat aber anscheinend Interesse geweckt. Die Teilnahme war wie immer sehr lebhaft und es sind einige interessante Diskussionen entstanden. Die Folien sind mittlerweile Online.
Für mich war der Ablauf sehr positiv und gleichzeitig unerwartet. Kaum war ich nach der Theorie für zwei Minuten aufs Klo, hatten alle mit den Aufgaben angefangen. Irgendwann ist dann die Struktur verloren gegangen und ein Impro-Theater entstanden, aber es hat mich trotzdem gefreut.
Schön fand ich das viele sehr fortgeschritten in dem Bereich Refactoring und TTD waren. Gut waren auch die interessanten Diskussionen über Entfurf-Stil und auch die Art wie wir die letzte Aufgabe mit dem FtpClient interaktiv gelöst haben.
Eine der Fragen die dabei entstanden ist war ungewöhnlich, aber durchaus interessant - sind Interfaces mit einer einzigen Implementierung YAGNI? Ist es ein Design-Smell wenn man ein Interface extrachiert, welches nur den Overhead mit sich bringt, ohne hinreichenden Nutzen? Diese Frage hat mich in den letzten Tagen beschäftigt, und ich versuche hier die Hintergründe zu beleuchten und meine Rationale darzustellen.
In der frühen Zeit der TDD Geschichte, wo Mocking-Frameworks noch nicht populär waren, hat man manuelles Mocking verwendet. Man hat Kollaborateure mit Hand-gemachten Mocks ersetzt hat und diese in dem zu testenden Objekt injiziert. Dies erfortdert dass der Kollaborator ein Interface implementiert, damit man ihn in dem Test mit einem Fake/Dummy/Stub/Mock transparent für das zu testende Objekt ersetzen kann.
Hat man beispielsweise einen BookingService, die mit Hilfe eines MeetingCalendars eine Konferenz bucht, so hat man beim old-school Mocking ein Interface für MeetingCalendar extrachiert und in dem BookingService gegen das Interface programmiert. In dem Test injiziert man dann einen manuellen Mock.
Heutzutage können Frameworks wie Mockito automatische Mocks on-the-fly generieren. Für Interfaces werden dabei dynamische Proxies generiert, und für konkrete Klassen werden Bytecode-Manipulationen angewendet. Diese neue Tatsache - Mocks für konkrete Klassen - verringert die Notwendigkeit von Interfaces für das Testen. Man braucht sie jetzt nur noch wenn man finale Klassen mocken will, aber abgesehen davon, warum dann noch den Interface Overhead wenn eine einzige Implementierung vorliegt? Ist final wirklich so viel Wert dass es eine zusätzliche Datei, und ein zusätzlicher Typ rechtfertigt?
Nun final ist eine Sache mit Pro und Contra. Gut für Value Objects und bei der Idee einer normalisierten Klassenhierarchie. Gleichzeitig erläutert Feathers aber auch die Schwierigkeiten fürs Testen, die bei excessiver Verwendung entstehen können.
Lassen wir das final also bei Seite. Es gibt aber weitere Situationen, wo ein Interface selbst bei einer einzigen Implementierung sinnvoll sein kann.
Eine solche Stelle ist bei der API eines Moduls. Das sind die wenigen zentralen Abstraktionen, die von Clients verwendet werden, die 10% der Typen, die für Nuntzung nach aussen vorgesehen sind. Wenn z.B. der MeetingCalendar mit einer Outlook-Datenbank kommuniziert, so ist es für den Client einfacher diese externe Verbindung zu isolieren wenn er ein Interface in der Hand hat.
Weiterhin verhilft ein Interface der Testbarkeit wenn die Klasse systemnahen Code ausführt. Schmerzhafte Repräsentanten dafür sind final Klassen aus der Standardbibliothek - ProcessBuilder in Java oder Timer in .NET, die kein Interface haben und damit nicht mit konventionellen Frameworks mockbar sind. Um hier Tests zu verwenden ist man auf selbstgemachte Adapter oder Power-Mocking Frameworks angewiesen, die aber eine Reihe von anderen Problemen mit sich bringen. Aber auch dies könnte man zur Seite legen, wenn man diese Fälle als seltene Ausnahmen betrachtet.
Wo man sich ein Interface nicht so gut ersparen kann ist bei zwei Prinzipien des OO-Entwurfs:
Dependency Inversion Principle. "High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions." -- Die typische Situation ist wenn das Interface in einem Modul liegt und von den Klassen dieses Moduls verwendet wird, aber die Implementierung des Interfaces in ein anderes Modul liegt.
Interface Seggregation Principle. "ISP states that no client should be forced to depend on methods it does not use. ISP splits interfaces which are very large into smaller and more specific ones so that clients will only have to know about the methods that are of interest to them. Such shrunken interfaces are also called role interfaces." -- Die typische Situation ist wenn man zwei Interfaces in einer Klasse implementiert und ein Client das erste Interface verwendet und ein anderer das zweite.
Man kann also nicht pauschal sagen dass Header-Interfaces per se YAGNI sind. Oftmals kann man sie wahrscheinlich weglassen, besonders bei internen Klassen innerhalb eines Moduls. Anders sollte man jedoch auf dem API-Level abwägen. Nichts desto trotz ist das eine gute Einsicht, denn es gibt vielleicht mehr Stellen wo man auf ein Interface verzichten kann wie man sich denkt!
CU,
Rusi