Es gibt in der Programmierwelt unzählige Prinzipien, Patterns und was nicht noch alles, doch die SOLID-Prinzipien gehören zu den nützlichsten. Eines dieser Prinzipien, das «I», steht für das «Interface segregation principle» und besagt Folgendes:
"Many client-specific interfaces are better than one general-purpose interface." (Viele clientspezifische Interfaces sind besser als ein universelles Interface.)
Was hat das mit Spring Data Repositories zu tun? Lesen Sie weiter, dann erfahren Sie es!
Spring Data Repositories sind
ein hervorragendes Mittel, um Boilerplate-Code zu reduzieren, wenn man aus einer Spring-Boot-Applikation heraus auf
Datenbankressourcen zugreift. Wir erstellen einfach ein Interface, das org.springframework.data.jpa.repository.JpaRepository
erweitert (sofern wir JPA verwenden):
public interface ProductRepository
extends JpaRepository<Product, String>
{
Optional<Product> findByItemNumber(String itemNumber);
}
Und schon können wir es in unseren Komponenten verwenden:
public class CreateProduct implements CatalogDataCommand<Product>
{
private final ProductRepository products;
public CreateProduct(ProductRepository products, /* ... */)
{
this.products = products;
}
public Result<Product> execute(Action action)
{
Optional<Product> existing = products
.findByItemNumber(action.getItemNumber());
// ...
}
}
So weit, so gut. Was aber, wenn wir unsere Klasse CreateProduct mit einem Unit-Test prüfen wollen? Da ProductRepository
nur ein Interface ist, gibt es keine Implementierung, die wir beim Instanziieren unserer Klasse übergeben könnten. Spring Data
erzeugt eine Implementierung des Repositorys beim Bootstrapping der Applikation. In unserem Unit-Test gibt es aber keine
Applikation.
An diesem Punkt haben wir zwei Möglichkeiten, zu einer Implementierung unseres Repositorys zu kommen:
- eine Test-Implementierung von
ProductRepositoryvon Hand schreiben - eine Mocking-Library wie Mockito verwenden
Die erste Möglichkeit ist nicht wirklich praktikabel, weil die API von ProductRepository (durch die Vererbung) riesig ist.
Nicht nur, was die Anzahl verfügbarer Methoden betrifft (es sind mehr als 20, die wir alle irgendwie implementieren müssten),
sondern auch, was den Umfang der bereitgestellten Funktionalität angeht. Erinnern Sie sich an das I in SOLID? 😭
Für die zweite Möglichkeit müssen wir nur Mockito zu unserem Classpath hinzufügen und können es dann so verwenden:
ProductRepository repository = Mockito.mock(ProductRepository.class);
Mockito
.when(repository.findByItemNumber(Mockito.anyString())
.thenReturn(Optional.of(...));
Das ist besser, aber immer noch nicht wirklich schön. Das Hauptproblem ist, dass wir wissen müssen, welche Methoden die
Klasse CreateProduct während des Tests aufruft. Wird aus irgendeinem Grund statt findByItemNumber() eine andere der
unzähligen Methoden des Repositorys verwendet, schlägt der Test fehl. Das ist unglücklich, denn Tests sollten nur dann
fehlschlagen, wenn wir tatsächlich einen Bug eingebaut haben.
Wie können wir also die Testbarkeit verbessern, ohne auf die einfache Handhabung zu verzichten, die uns die Spring Data Repositories bieten?
Zuerst erstellen wir ein Interface, das nur die Methoden bereitstellt, mit denen wir unsere Produkte finden:
public interface ProductLookup
{
Optional<Product> findByItemNumber(String itemNumber);
}
In unserer Klasse CreateProduct verlangen wir nun dieses Interface anstelle von ProductRepository.
public class CreateProduct implements CatalogDataCommand
{
private final ProductLookup products;
public CreateProduct(ProductLookup products, /* ... */)
{
this.products = products;
}
}
Eine Implementierung für Tests zu schreiben, ist jetzt ganz einfach:
ProductLookup products = itemNumber -> Optional.of(...);
(Sobald unser Lookup-Interface mehr als eine Methode hat, wird der Code etwas ausführlicher, bleibt aber dennoch einfach zu schreiben und zu verwenden.)
Damit sind die Tests erledigt. Jetzt startet aber unsere Applikation nicht mehr, weil es keine Bean gibt, die ProductLookup
implementiert. Um das zu beheben, fügen wir unser neues Interface dem bestehenden ProductRepository hinzu. So injiziert der
Spring-IoC-Container das Repository in unsere Komponente, weil es das benötigte Interface implementiert.
public interface ProductRepository
extends ProductLookup, JpaRepository<Product, String>
{
}
Et voilà, wir haben das Spring Data Repository erfolgreich vor unserer Klasse CreateProduct verborgen. Die Interface
Segregation ist wiederhergestellt, und alle sind glücklich. Eine Runde Drinks für alle!