Softwerkskammer

 

Refactoring Workshop mit Rusi

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 und alle hatten 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 vortgeschritten 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.

Sind Header-Interfaces YAGNI?

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 persönlichen Rationale darzustellen.

In der frühen Zeit der TDD Geschichte, wo Mocking-Frameworks noch nicht populär waren, hat man manuelles Mocking verwendet. Das Bedeutet, dass man Kollaborateure mit Hand-gemachten Mocks ersetzt hat und diese in dem zu testenden Objekt injiziert hat. Dies erfortdert natürlich 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 eine Klasse BookingService, die mit Hilfe eines MeetingCalendar eine Konferenz bucht, so hat man beim old-school Mocking ein Interface für MeetingCalendar extrachiert und in dem BookingService gegen das Interface programmiert und in dem Test einen manuellen Mock injiziert.

Heutzutage können moderne Frameworks wie Mockito automatische Mocks on-the-fly generieren. Das funktioniert indem für Interfaces dynamische Proxies generiert werden, oder Bytecode-Manipulationen für konkrete Klassen angewendet werden. Diese neue Tatsache - Mocks für konkrete Klassen - verringert natürlich die Notwendigkeit von Interfaces für das Testen. Man braucht sie jetzt nur wenn man finale Klassen mocken will, aber abgesehen davon, warum dann noch den Interface-Tradeoff? 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 zur 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 5% der Typen, die 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. Repräsentanten dafür sind die final Klassen aus der Standardbibliothek - ProcessBuilder (Java) oder Timer (.NET), die kein Interface haben und damit nicht mockbar sind. Um hier Tests zu verwenden ist man auf selbstgemachte Adapter angewiesen. Aber auch das könnte man zur Seite legen, wenn man diese als seltene Ausnahmen betrachtet.

Wo man sich das Interface nicht 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.

Fazit

Man kann also nicht pauschal sagen dass Interfaces mit einer einzigen Implementierung 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