Generics + Spring: Möge die Macht mit dir sein

    Einmal in einer weit entfernten Bank ...


    Guten Tag, Habr. Heute bekam er endlich wieder die Hände hier zu schreiben. Im Gegensatz zu früheren Tutorials - Artikeln möchte ich heute meine Erfahrungen teilen und die Kraft eines solchen Mechanismus als Generika zeigen, der zusammen mit der Magie des Frühlings noch stärker wird. Ich möchte Sie nur warnen, dass Sie, um den Artikel zu verstehen, die Grundlagen des Springens kennen lernen und mehr über Generika wissen müssen als nur „Generika sind, was wir in ArrayList in Anführungszeichen angeben“.

    Episode 1:


    Beginnen wir mit der Tatsache, dass ich bei der Arbeit eine solche Aufgabe hatte: Es gab eine große Anzahl von Überweisungen mit einer bestimmten Anzahl gemeinsamer Felder. Darüber hinaus war jede der Übersetzungen mit Klassen verbunden - Anforderungen für die Übertragung von einem Status in einen anderen und die Umleitung in einen anderen. Dementsprechend gab es Bauunternehmer, die im Umbau tätig waren.

    Das Problem mit den gemeinsamen Bereichen entschied ich einfach - die Vererbung. Also bekam ich Klassen:

    publicclassTransfer{
        private TransferType transferType;
        ...
    }
    publicenum TransferType {
          INTERNAL, SWIFT, ...;
    }
    publicclassInternalTransferextendsTransfer{
        ...
    }
    publicclassBaseRequest{
        ...
    }
    publicclassInternalRequestextendsBaseRequest{
        ...
    }    
    ...

    Episode 2:


    Dann gab es ein Problem mit den Controllern - sie mussten alle die gleichen Methoden haben - checkTransfer, approveTransfer usw. Hier haben sich die ersten Generika, aber nicht das letzte Mal, als nützlich erwiesen: Ich habe mit den richtigen Methoden einen gemeinsamen Controller erstellt und den Rest davon geerbt:

    @AllArgsConstructorpublicclassTransferController<TextendsTransfer> {
            privatefinal TransferService<T> service;
            public CheckResponse checkTransfer(@RequestBody @Valid T payment){
                return service.checkTransfer(payment);
            }
            ...
        }
        publicclassInternalTransferControllerextendsTransferController<InternalTransfer> {
            publicInternalTransferController(TransferService<InternalTransfer> service){
                super(service);
            }
        }
    

    Nun, eigentlich der Service:

    publicinterfaceTransferService<TextendsTransfer> {
        CheckResponse checkTransfer(T payment);
        ApproveResponse approveTransfer(T payment);
        ...
    }

    So wurde das Copy-Paste-Problem nur auf den Aufruf des Superkonstruktors reduziert, und im Service haben wir es insgesamt verloren.

    Aber!

    Folge 3:


    Es gab immer noch ein Problem im Service:
    Je nach Art der Übersetzung mussten verschiedene Builder aufgerufen 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");
            thrownew UnknownPaymentTypeException();
        }
    }
    

    Generalisierte Builder-Schnittstelle:

    publicinterfaceRequestBuilder<TextendsBaseRequest, UextendsTransfer> {
          T createRequest(U transfer);
    }
    

    Zur Optimierung wurde hier die Factory-Methode eingeführt, so dass sich Switch / Case in einer separaten Klasse befindet. Es scheint besser zu sein, aber das Problem bleibt dasselbe: Wenn Sie eine neue Übersetzung hinzufügen, müssen Sie den Code ändern, und der umständliche Schalter / Fall hat mir nicht gepasst.

    Folge 4:


    Was war der Ausweg? Zunächst fiel mir ein, die Art der Übersetzungen anhand des Klassennamens zu ermitteln und den richtigen Builder mit Reflektion aufzurufen, wodurch Entwickler, die mit dem Projekt zusammenarbeiten würden, bestimmte Anforderungen für die Benennung ihrer Klassen erfüllen müssen. Es gab aber eine bessere Lösung. Wenn man das Gehirn schüttelt, kann man zu dem Schluss kommen, dass der Hauptaspekt der Geschäftslogik der Anwendung die Übersetzungen selbst sind. Wenn es keine gibt, wird es nicht den Rest geben. Warum also nicht alles loswerden? Es reicht aus, nur unseren Unterricht zu ändern. Und wieder kommen Generika zur Rettung.

    Klassen anfragen:

    publicclassBaseRequest<TextendsTransfer> {
        ...
    }
    publicclassInternalRequestextendsBaseRequest<InternalTransfer>  {
        ...
    }
    

    Und die Builder-Schnittstelle:

    publicinterfaceRequestBuilder<TextendsTransfer> {
        BaseRequest<T> createRequest(T transfer);
    }
    

    Und hier wird es interessanter. Wir sind mit einem Merkmal von Generika konfrontiert, das fast nie erwähnt wird und hauptsächlich in Frameworks und Bibliotheken verwendet wird. Schließlich können wir als BaseRequest den Nachfolger ersetzen, der dem Typ T entspricht, das heißt:

    publicclassInternalRequestBuilderimplementsRequestBuilder<InternalTransfer> {
            @Overridepublic InternalRequest createRequest(InternalTransfer transfer){
                return InternalRequest.builder()
                        ...
                        .build();
            }
    }

    Im Moment haben wir eine gute Verbesserung unserer Anwendungsarchitektur erreicht. Das Problem des Switch / Case hat dies jedoch noch nicht gelöst. Oder ...?

    Folge 5:


    Hier kommt die Magie des Frühlings ins Spiel.

    Tatsache ist, dass wir mit der Methode getBeanNamesForType (ResolvableType) ein Array von bin-Namen erhalten können, die dem gewünschten Typ entsprechen . Und in der ResolvableType-Klasse gibt es eine statische Methode fürClassWithGenerics (Klasse <?> Clazz, Klasse <?> ... Generics) , bei der die Klasse (Schnittstelle) als erster Parameter übergeben werden soll, die den zweiten Parameter als Generic 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 das Blatt hinzufügen, wobei das Interface generisch ist, es alle seine Implementierungen enthält:

    privatefinal List<RequestBuilder<T>> builders;


    Wir müssen es nur durchgehen und mit Hilfe des Instanz-Checks den entsprechenden finden:

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


    Ähnlich wie bei dieser Variante ist es immer noch möglich, ApplicationContext oder BeanFactory anzupassen und die Methode getBeanNamesForType () dort aufzurufen, an der unser Typ als Parameter übergeben werden soll. Dies wird jedoch als Zeichen des schlechten Geschmacks betrachtet und ist für diese Architektur nicht erforderlich (besonderer Dank an zolt85 für den Kommentar).
    Daher hat unsere Factory-Methode folgende Form:

    @Component@AllArgsConstructorpublicclassRequestBuildersFactory<TextendsTransfer> {
            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


    Daher haben wir ein Mini-Framework mit einer durchdachten Architektur, das alle Entwickler dazu verpflichtet, sich daran zu halten. Und was wichtig ist, wir haben uns den umständlichen Wechsel / Fall entledigt und das Hinzufügen neuer Übersetzungen wird die vorhandenen Klassen in keiner Weise beeinflussen, was eine gute Nachricht ist.

    PS: In
    diesem Artikel werden keine Generics verwendet, wo immer dies möglich und nicht möglich ist. Mit ihrer Hilfe möchte ich jedoch mitteilen, welche mächtigen Mechanismen und Architekturen Sie erstellen dürfen.

    Danksagung:
    Getrenntes Dankeschön an Sultansoy , ohne das diese Architektur nicht in den Sinn gekommen wäre und diesen Artikel höchstwahrscheinlich nicht hätte.

    Links:
    Quellcode auf Github

    Jetzt auch beliebt: