[JAVA] Conseils pour généraliser l'API

Aperçu

Les génériques sont pratiques, n'est-ce pas? Mais votre tête n'est-elle pas gâchée avec beaucoup de symboles (arguments de type)? Les étapes recommandées pour généraliser l'API sont:

  1. Tout d'abord, écrivez le processus que vous souhaitez découper et généraliser avec le type spécifique.
  2. Dans le processus, identifiez ce que vous souhaitez modifier le type
  3. Remplacez-les par des arguments de type et rendez-les génériques

Prenons un exemple concret.

Exemple simple

J'ai le code suivant.

        // In: List<SalesLine>
        // Out:Mapper avec le code produit comme clé et le nombre total de ventes comme valeur
        Map<String, Integer> map =
                list.stream().collect(
                        Collectors.groupingBy(SalesLine::getProductCode,
                                Collectors.summingInt(SalesLine::getQuantity)
                        ));

Maintenant, disons que nous utilisons souvent le processus de "regroupement des éléments de liste avec une certaine clé, additionnant quelques valeurs numériques pour chacun et le renvoyant dans une carte", alors faisons-en une méthode utilitaire. Au lieu d'écrire en génériques en une seule fois, écrivez d'abord solidement.

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

Ensuite, dans ce processus, identifiez le type que vous souhaitez modifier. La première chose qui est claire est le type d'élément de liste appelé SalesLine. Remplacez-le par «T». (À ce stade, une erreur de compilation se produira)

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

Ensuite, je veux pouvoir utiliser ʻIntegeret les types propriétaires sans limiter la clé à une chaîne, alors remplacezString par S. La valeur de la carte sera calculée comme une valeur totale, alors laissez-la comme ʻInteger.

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

Débarrassons-nous des T :: getProductCode et T :: getQuantity qui causent l'erreur de compilation. Ceux-ci sont censés extraire des valeurs d'objets de type d'élément de liste (T), alors passez-les comme arguments dans l'interface fonctionnelle. Considérant que la conversion est en «T-> S» et la conversion en «T-> Integer», respectivement, c'est comme suit.

    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)
                ));
    }

C'est tout. Le programme côté utilisateur est le suivant.

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

Un exemple un peu plus difficile

Considérez le programme de sortie de rapport suivant. Il s'agit du processus dit de «coupure de clé» qui est courant dans les applications d'entreprise, où une liste triée des relevés de vente est entrée et une ligne de sous-total est émise lorsque le code produit change.

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)) {
                //Sortie de ligne de sous-total au moment de la pause
                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();
        }
        //Traiter le dernier groupe de sous-total
        sb.append(makeSubtotalLine(currentProductCode, subtotalQty, subtotalAmount)).append("\n");

        return sb.toString();
    }

L'exemple de sortie de ce programme est le suivant.

Détails 2020-04-01 A 2 pièces 200 yens
Détails 2020-04-01 A 3 pièces 300 yens
Détails 2020-04-02 A 1 pièce 100 yens
Détails 2020-04-02 A 1 pièce 100 yens
Sous-total A 7 pièces 700 yens
Détails 2020-04-01 B 1 pièce 150 yens
Détails 2020-04-02 B 2 pièces 300 yens
Détails 2020-04-02 B 2 pièces 300 yens
Sous-total B 5 pièces 750 yens
Détails 2020-04-01 C 2 pièces 400 yens
Sous-total C 2 pièces 400 yens

Supposons maintenant que vous souhaitiez généraliser le processus, car le traitement des sauts de clé se produit fréquemment dans les applications métier. Tout d'abord, écrivez une classe solide pour le traitement des sauts de clé. Seul le flux de traitement doit être partagé et la logique de sortie spécifique doit être passée en argument.

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);
    }

}

Considérez le type que vous souhaitez modifier dans ce processus. Le type SalesLine qui représente la ligne de détail et le type String de la clé de rupture sont ciblés, donc si vous les remplacez par L et K, respectivement, ce sera comme suit.

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);
    }
}

Ceci complète l'API générique. Le côté programme de sortie de rapport est le suivant.

ReportComponent.java


    public String outputReportWithGenerics(List<SalesLine> sales) {
        GeneralKeyBreakProcessor<SalesLine, String> gkbp = new GeneralKeyBreakProcessor<>(sales);
        final StringBuilder sb = new StringBuilder();
        //Génération de clés
        Function<SalesLine, String> keyGenerator = SalesLine::getProductCode;
        //Traiter 1 ligne de détail et sortie
        Consumer<SalesLine> processLine = sl -> sb.append(makeNormalLine(sl)).append("\n");
        //Traiter les éléments de campagne regroupés par clé et sous-total de sortie
        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();
    }

Résumé

Si vous suivez les étapes, implémenter une API à l'aide de Generics n'est pas effrayant!

prime

Il s'agit d'une version générique du programme de sortie de rapport, mais elle reste un peu difficile à lire. La révision de l'API à l'aide de la technique «API fluide» améliorera un peu la lisibilité.

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());
    }

L'explication de cette implémentation est écrite en détail dans Blog.

Recommended Posts

Conseils pour généraliser l'API
astuces pour java.nio.file.Path
Conseils pour bien utiliser Canvas avec Xcode
Collection Gradle TIPS (pour moi)
[Java] Conseils pour l'écriture de la source
Mémo pour la création d'API Niconico (Matériaux)
Conseils pour gérer les pseudo-éléments dans Selenium