Generika + Frühling: Möge die Macht mit dir sein

Es war einmal in einer fernen, fernen Bank ...


Guten Tag, Habr. Heute endlich erreichten meine Hände wieder hier, um zu schreiben. Aber im Gegensatz zu den vorherigen Tutorials - Artikeln heute möchte ich meine Erfahrungen teilen und die Kraft eines solchen Mechanismus wie Generika zeigen, der zusammen mit der Frühlingsmagie noch stärker wird. Ich möchte Sie sofort warnen, dass Sie zum Verständnis des Artikels die Grundlagen des Springens kennen und Ideen zu Generika haben müssen, die mehr sind als nur „Generika sind genau das, was wir in ArrayList in Anführungszeichen angeben“.

Folge 1:


Bei der Arbeit hatte ich zunächst ungefähr so ​​eine Aufgabe: Es gab eine große Anzahl von Geldtransfers mit einer bestimmten Anzahl gemeinsamer Felder. Darüber hinaus entsprach jede der Übersetzungen Klassen - Anforderungen für die Übertragung von einem Zustand in einen anderen und die Umleitung zu einer anderen API. Dementsprechend gab es Bauherren, die an der Umstellung beteiligt waren.

Ich habe das Problem mit gemeinsamen Feldern einfach durch Vererbung gelöst. Also habe ich Unterricht bekommen:

public class Transfer { private TransferType transferType; ... } public enum TransferType { INTERNAL, SWIFT, ...; } public class InternalTransfer extends Transfer { ... } public class BaseRequest { ... } public class InternalRequest extends BaseRequest { ... } ... 

Folge 2:


Dann gab es das Problem mit den Controllern - sie mussten alle die gleichen Methoden haben - checkTransfer, genehmigenTransfer usw. Hier waren Generika zum ersten, aber nicht zum letzten Mal nützlich: Ich habe einen allgemeinen Controller mit den erforderlichen Methoden erstellt und den Rest davon geerbt:

  @AllArgsConstructor public class TransferController<T extends Transfer> { private final TransferService<T> service; public CheckResponse checkTransfer(@RequestBody @Valid T payment) { return service.checkTransfer(payment); } ... } public class InternalTransferController extends TransferController<InternalTransfer> { public InternalTransferController(TransferService<InternalTransfer> service) { super(service); } } 

Naja, eigentlich der Service:

 public interface TransferService<T extends Transfer> { CheckResponse checkTransfer(T payment); ApproveResponse approveTransfer(T payment); ... } 

Daher wurde das Problem des Kopierens und Einfügens nur auf das Aufrufen des Superkonstruktors reduziert, und im Dienst haben wir es im Allgemeinen verloren.

Aber!

Folge 3:


Es gab immer noch ein Problem im Service:
Je nach Art der Übertragung mussten verschiedene Bauherren angerufen werden:

 RequestBuilder builder; switch (type) { case INTERNAL: { builder = beanFactory.getBean(InternalRequestBuilder.class); break; } case SWIFT: { builder = beanFactory.getBean(SwiftRequestBuilder.class); break; } default: { log.info("Unknown payment type"); throw new UnknownPaymentTypeException(); } } 

verallgemeinerte Builder-Oberfläche:

 public interface RequestBuilder<T extends BaseRequest, U extends Transfer> { T createRequest(U transfer); } 

Die Factory-Methode wurde hier zur Optimierung entwickelt, sodass Schalter / Gehäuse in einer separaten Klasse sind. Es schien besser zu sein, aber das Problem blieb das gleiche - wenn Sie eine neue Übersetzung hinzufügen, müssen Sie den Code ändern, und der sperrige Schalter / Koffer passte nicht zu mir.

Folge 4:


Was war der Ausweg? Zuerst kam mir der Gedanke, die Art der Übersetzungen anhand des Klassennamens zu bestimmen und den gewünschten Builder mithilfe von Reflection aufzurufen, wodurch Entwickler, die mit dem Projekt arbeiten würden, gezwungen würden, bestimmte Anforderungen an die Namen ihrer Klassen zu erfüllen. Aber es gab eine bessere Lösung. Wenn Sie sich gefragt haben, können Sie zu dem Schluss kommen, dass der Hauptaspekt der Geschäftslogik der Anwendung die Übersetzungen selbst sind. Das heißt, wenn es keine gibt, wird es nichts anderes geben. Warum also nicht alles zusammenbinden? Es reicht aus, unsere Klassen ein wenig zu modifizieren. Und wieder kommen Generika zur Rettung.

Klassen anfordern:

 public class BaseRequest<T extends Transfer> { ... } public class InternalRequest extends BaseRequest<InternalTransfer> { ... } 

Und die Builder-Oberfläche:

 public interface RequestBuilder<T extends Transfer> { BaseRequest<T> createRequest(T transfer); } 

Und hier wird es interessanter. Wir sind mit einer Funktion von Generika konfrontiert, die fast nirgendwo erwähnt wird und hauptsächlich in Frameworks und Bibliotheken verwendet wird. In der Tat können wir als BaseRequest seinen Erben ersetzen, der dem Typ T entspricht, d.h.

 public class InternalRequestBuilder implements RequestBuilder<InternalTransfer> { @Override public InternalRequest createRequest(InternalTransfer transfer) { return InternalRequest.builder() ... .build(); } } 

Im Moment haben wir eine gute Verbesserung unserer Anwendungsarchitektur erreicht. Das Switch / Case-Problem wurde jedoch noch nicht behoben. Oder ...?

Folge 5:


Hier kommt die Frühlingsmagie ins Spiel.

Tatsache ist, dass wir die Möglichkeit haben, mithilfe der Methode getBeanNamesForType (ResolvableType type) ein Array von Bin-Namen abzurufen , die dem gewünschten Typ entsprechen. In der ResolvableType-Klasse gibt es eine statische Methode für ClassWithGenerics (Klasse <?> Clazz, Klasse <?> ... Generika) , bei der Sie die Klasse (Schnittstelle) als ersten Parameter übergeben müssen, die den zweiten Parameter als Generikum verwendet und den entsprechenden Typ zurückgibt. T e:

 ResolvableType type = ResolvableType.forClassWithGenerics(RequestBuilder.class, transfer.getClass()); 

Gibt Folgendes zurück:

 RequestBuilder<InternalTransfer> 

Und jetzt ein bisschen mehr Magie - Tatsache ist, dass, wenn Sie ein Blatt mit der Schnittstelle als Generikum automatisch verdrahten, alle seine Implementierungen darin enthalten sind:

 private final List<RequestBuilder<T>> builders; 


Wir müssen es nur durchgehen und mit der Instanzprüfung die passende finden:

 builders.stream() .filter(b -> type.isInstance(b)) .findFirst() .get(); 


Ähnlich wie bei dieser Option besteht weiterhin die Möglichkeit, ApplicationContext oder BeanFactory automatisch zu verdrahten und die Methode getBeanNamesForType () aufzurufen, um unseren Typ als Parameter zu übergeben. Dies wird jedoch als Zeichen für schlechten Geschmack angesehen und diese Architektur ist nicht erforderlich (besonderer Dank geht an zolt85 für den Kommentar).
Infolgedessen hat unsere Fabrikmethode die folgende Form:

  @Component @AllArgsConstructor public class RequestBuildersFactory<T extends Transfer> { private final List<RequestBuilder<T>> builders; public BaseRequest<T> transferToRequest(T transfer) { ResolvableType type = ResolvableType.forClassWithGenerics(RequestBuilder.class, transfer.getClass()); RequestBuilder<T> builder = builders.stream() .filter(b -> type.isInstance(b)) .findFirst() .get(); return builder.createRequest(transfer, stage); } } 

Folge 6: Fazit


Wir haben also ein Mini-Framework mit einer durchdachten Architektur, das alle Entwickler dazu verpflichtet, sich daran zu halten. Und was wichtig ist, wir haben den umständlichen Schalter / Fall beseitigt und das Hinzufügen neuer Übersetzungen hat keinerlei Auswirkungen auf vorhandene Klassen, was eine gute Nachricht ist.

PS:
Dieser Artikel fördert nicht die Verwendung von Generika, wo immer dies möglich und unmöglich ist, aber mit seiner Hilfe möchte ich mitteilen, welche leistungsstarken Mechanismen und Architekturen Sie erstellen können.

Danksagung:
Besonderer Dank geht an Sultansoy , ohne den diese Architektur nicht in den Sinn gekommen wäre, und höchstwahrscheinlich wäre dieser Artikel nicht gewesen.

Referenzen:
Github-Quellcode

Source: https://habr.com/ru/post/de423655/


All Articles