[JAVA] Tipps zum Generieren von API

Überblick

Generika sind praktisch, nicht wahr? Aber ist Ihr Kopf nicht mit vielen Symbolen (Typargumenten) durcheinander? Die empfohlenen Schritte zum Generieren der API sind:

  1. Schreiben Sie zunächst den Prozess, den Sie ausschneiden möchten, und verallgemeinern Sie ihn mit dem jeweiligen Typ.
  2. Identifizieren Sie dabei, was Sie am Typ ändern möchten
  3. Ersetzen Sie sie durch Typargumente und machen Sie sie generisch

Nehmen wir ein konkretes Beispiel.

Einfaches Beispiel

Ich habe den folgenden Code.

        // In: List<SalesLine>
        // Out:Karte mit dem Produktcode als Schlüssel und der Gesamtzahl der Verkäufe als Wert
        Map<String, Integer> map =
                list.stream().collect(
                        Collectors.groupingBy(SalesLine::getProductCode,
                                Collectors.summingInt(SalesLine::getQuantity)
                        ));

Nehmen wir nun an, wir verwenden häufig den Prozess des "Gruppierens von Listenelementen mit einem bestimmten Schlüssel, Summieren einiger numerischer Werte für jedes Element und Zurückgeben in einer Karte". Machen wir es also zu einer Dienstprogrammmethode. Anstatt auf einmal in Generics zu schreiben, schreiben Sie zuerst solide.

    public static Map<String, Integer> groupTotal(List<SalesLine> list) {
        return list.stream().collect(
                Collectors.groupingBy(SalesLine::getProductCode,
                        Collectors.summingInt(SalesLine::getQuantity)
                ));
    }

Identifizieren Sie als Nächstes in diesem Prozess den Typ, den Sie variieren möchten. Das erste, was klar ist, ist die Art des Listenelements namens "SalesLine". Ersetzen Sie dies durch "T". (Zu diesem Zeitpunkt tritt ein Kompilierungsfehler auf.)

    public static <T> Map<String, Integer> groupTotal(List<T> list) {
        return list.stream().collect(
                Collectors.groupingBy(T::getProductCode,
                        Collectors.summingInt(T::getQuantity)
                ));
    }

Als nächstes möchten wir in der Lage sein, "Integer" - und proprietäre Typen zu verwenden, ohne den Schlüssel auf einen String zu beschränken. Ersetzen Sie also "String" durch "S". Der Kartenwert wird als Gesamtwert berechnet, belassen Sie ihn also als "Ganzzahl".

    public static <S, T> Map<S, Integer> groupTotal(List<T> list) {
        return list.stream().collect(
                Collectors.groupingBy(T::getProductCode,
                        Collectors.summingInt(T::getQuantity)
                ));
    }

Lassen Sie uns die T :: getProductCode und T :: getQuantity loswerden, die den Kompilierungsfehler verursachen. Von diesen wird erwartet, dass sie Werte aus Objekten des Listenelementtyps (T) extrahieren. Übergeben Sie sie daher als Argumente in der Funktionsschnittstelle. In Anbetracht dessen, dass die Umwandlung in "T-> S" bzw. die Umwandlung in "T-> Ganzzahl" erfolgt, ist dies wie folgt.

    public static <S, T> Map<S, Integer> groupTotal(
            List<T> list, Function<T, S> keyExtractor, Function<T, Integer> valueExtractor) {
        return list.stream().collect(
                Collectors.groupingBy(keyExtractor::apply,
                        Collectors.summingInt(valueExtractor::apply)
                ));
    }

Das ist es. Das Programm auf der Benutzerseite ist wie folgt.

        Map<String, Integer> map = groupTotal(list, SalesLine::getProductCode, SalesLine::getQuantity);

Ein etwas schwierigeres Beispiel

Betrachten Sie das folgende Berichtsausgabeprogramm. Dies ist der sogenannte "Schlüsselunterbrechungs" -Prozess, der in Geschäftsanwendungen üblich ist, bei dem eine sortierte Liste von Verkaufsabrechnungen eingegeben und eine Zwischensummenzeile ausgegeben wird, wenn sich der Produktcode ändert.

ReportComponent.java


    public String outputReport(List<SalesLine> sales) {
        StringBuilder sb = new StringBuilder();

        String currentProductCode = null;
        int subtotalQty = 0;
        int subtotalAmount = 0;

        for (SalesLine sl: sales) {
            String productCode = sl.getProductCode();
            if (!productCode.equals(currentProductCode)) {
                //Zwischensummenzeile bei Tastenumbruch ausgeben
                if (currentProductCode != null) {
                    sb.append(makeSubtotalLine(currentProductCode, subtotalQty, subtotalAmount)).append("\n");
                }
                currentProductCode = productCode;
                subtotalQty = 0;
                subtotalAmount = 0;
            }
            sb.append(makeNormalLine(sl)).append("\n");
            subtotalQty += sl.getQuantity();
            subtotalAmount += sl.getAmount();
        }
        //Verarbeiten Sie die letzte Zwischensummengruppe
        sb.append(makeSubtotalLine(currentProductCode, subtotalQty, subtotalAmount)).append("\n");

        return sb.toString();
    }

Das Ausgabebeispiel dieses Programms lautet wie folgt.

Details 2020-04-01 A 2 Stück 200 Yen
Details 2020-04-01 A 3 Stück 300 Yen
Details 2020-04-02 A 1 Stück 100 Yen
Details 2020-04-02 A 1 Stück 100 Yen
Zwischensumme A 7 Stück 700 Yen
Details 2020-04-01 B 1 Stück 150 Yen
Details 2020-04-02 B 2 Stück 300 Yen
Details 2020-04-02 B 2 Stück 300 Yen
Zwischensumme B 5 Stück 750 Yen
Details 2020-04-01 C 2 Stück 400 Yen
Zwischensumme C 2 Stück 400 Yen

Angenommen, Sie möchten den Prozess verallgemeinern, da die Verarbeitung von Schlüsselunterbrechungen in Geschäftsanwendungen häufig auftritt. Schreiben Sie zunächst eine solide Klasse für die Schlüsselunterbrechungsverarbeitung. Es sollte nur der Verarbeitungsablauf gemeinsam genutzt und die spezifische Ausgabelogik als Argument übergeben werden.

KeyBreakProcessor.java


public class KeyBreakProcessor {

    private List<SalesLine> lines;

    public KeyBreakProcessor(List<SalesLine> lines) {
        this.lines = lines;
    }

    public void execute(Function<SalesLine, String> keyGenerator, Consumer<SalesLine> lineProcessor,
                        BiConsumer<String, List<SalesLine>> keyBreakLister) {
        String currentKey = null;
        List<SalesLine> subList = new ArrayList<>();
        for (SalesLine line : lines) {
            String key = keyGenerator.apply(line);
            if (!key.equals(currentKey)) {
                if (currentKey != null) {
                    keyBreakLister.accept(currentKey, subList);
                    subList = new ArrayList<>();
                }
                currentKey = key;
            }
            lineProcessor.accept(line);
            subList.add(line);
        }
        keyBreakLister.accept(currentKey, subList);
    }

}

Betrachten Sie den Typ, den Sie in diesem Prozess variieren möchten. Der Typ "SalesLine", der die Detailzeile darstellt, und der Typ "String" des Unterbrechungsschlüssels sind die Ziele. Wenn Sie sie also durch "L" bzw. "K" ersetzen, lautet das Ergebnis wie folgt.

GeneralKeyBreakProcessor.java


public class GeneralKeyBreakProcessor<L, K> {

    private List<L> lines;

    public GeneralKeyBreakProcessor(List<L> lines) {
        this.lines = lines;
    }

    public void execute(Function<L, K> keyGenerator, Consumer<L> lineProcessor,
                        BiConsumer<K, List<L>> keyBreakLister) {
        K currentKey = null;
        List<L> subList = new ArrayList<>();
        for (L line: lines) {
            K key = keyGenerator.apply(line);
            if (!key.equals(currentKey)) {
                if (currentKey != null) {
                    keyBreakLister.accept(currentKey, subList);
                    subList = new ArrayList<>();
                }
                currentKey = key;
            }
            lineProcessor.accept(line);
            subList.add(line);
        }
        keyBreakLister.accept(currentKey, subList);
    }
}

Damit ist die generische API abgeschlossen. Die Seite des Berichtsausgabeprogramms ist wie folgt.

ReportComponent.java


    public String outputReportWithGenerics(List<SalesLine> sales) {
        GeneralKeyBreakProcessor<SalesLine, String> gkbp = new GeneralKeyBreakProcessor<>(sales);
        final StringBuilder sb = new StringBuilder();
        //Schlüsselgenerierung
        Function<SalesLine, String> keyGenerator = SalesLine::getProductCode;
        //1 Detailzeile verarbeiten und ausgeben
        Consumer<SalesLine> processLine = sl -> sb.append(makeNormalLine(sl)).append("\n");
        //Prozessposten gruppiert nach Schlüssel und Zwischensumme der Ausgabe
        BiConsumer<String, List<SalesLine>> subTotal = (code, lines) -> {
            int qty = lines.stream().mapToInt(SalesLine::getQuantity).sum();
            int amount = lines.stream().mapToInt(SalesLine::getAmount).sum();
            sb.append(makeSubtotalLine(code, qty, amount)).append("\n");
        };
        gkbp.execute(keyGenerator, processLine, subTotal);

        return sb.toString();
    }

Zusammenfassung

Wenn Sie die Schritte befolgen, ist die Implementierung einer API mit Generics nicht beängstigend!

Bonus

Dies ist eine generische Version des Berichtsausgabeprogramms, die jedoch weiterhin schwer zu lesen ist. Durch Überprüfen der API mithilfe der "Fluent API" -Technik wird die Lesbarkeit etwas verbessert.

ReportComponent.java


    public String outputReportWithFluent(List<SalesLine> sales) {
        FluentKeyBreakProcessor<SalesLine, String, String, String> processor = new FluentKeyBreakProcessor<>();
        List<String> groupList =
                processor.source(sales)
                .key(SalesLine::getProductCode)
                .eachLine(sl -> makeNormalLine(sl))
                .whenKeyChanged((key, list1, list2) -> {
                    String lines = list2.stream().collect(Collectors.joining("\n")) + "\n";
                    int qty = list1.stream().mapToInt(SalesLine::getQuantity).sum();
                    int amount = list1.stream().mapToInt(SalesLine::getAmount).sum();
                    return lines + makeSubtotalLine(key, qty, amount) + "\n";
                })
                .execute();

        return groupList.stream().collect(Collectors.joining());
    }

Die Erklärung dieser Implementierung finden Sie ausführlich in Blog.

Recommended Posts

Tipps zum Generieren von API
Tipps für java.nio.file.Path
Tipps zur guten Verwendung von Canvas mit Xcode
Gradle TIPS-Sammlung (für mich)
[Java] Tipps zum Schreiben der Quelle
Memo zur Herstellung von Niconico API (Materialien)
Tipps zum Umgang mit Pseudoelementen in Selen