Une évolution de l'énumération et des instructions switch! ?? Essayez d'obtenir des types de données algébriques et des correspondances de modèles en Java

Article du calendrier de l'avent Mikatus 2019.

Si vous lisez Reactive in practice: A complete guide to event-driven systems development in Java, alors The algebraic data type pattern (algebraic data type pattern) Modèle de type de données) Il était intéressant de noter que quelque chose est sorti. ..

Exemple: exprimer le type d'ordre dans le système de négociation d'actions

Réactif dans la pratique fournit un exemple de la façon de représenter les types d'ordres dans le système de négociation d'actions Stock Trader. Comme le sait tous ceux qui ont négocié des actions, il existe les types d'ordres d'actions suivants.

Pour représenter ces ordres, un modèle de conception appelé modèle de type de données algébrique dans Reactive en pratique est utilisé. Pourquoi pas un type d'énumération (Enum)? Découvrons le mystère.

Générer un projet expérimental Gradle

Je veux aussi utiliser Lombok, donc je vais expérimenter avec le projet Gradle au lieu de JShell. Créez un projet Gradle avec la commande suivante.

$ mkdir stock-trader
$ cd stock-trader
$ gradle init \
    --type java-library \
    --dsl groovy \
    --test-framework junit \
    --project-name stock-trader \
    --package com.example

Mettre en œuvre le type d'ordre de négociation d'actions en tant que type d'énumération

Tout d'abord, implémentons uniquement les ordres de marché et les ordres limités en tant que types d'énumération. Puisque les ordres limités doivent contenir un prix limite, donnez au type d'énumération un champ limitPrice. Il définit également une méthode getter getLimitPrice et une méthode setter setLimitPrice. ʻIsExecutable` Définit une méthode abstraite qui détermine si une commande est faisable et la remplace avec chaque valeur d'énumération.

src/main/java/com/example/order/OrderType.java


package com.example.order;

public enum OrderType {
    MARKET {
        @Override
        public boolean isExecutable(int currentPrice) {
            return true;
        }
    },
    LIMIT {
        @Override
        public boolean isExecutable(int currentPrice) {
            return currentPrice <= getLimitPrice();
        }
    };

    private int limitPrice;

    public int getLimitPrice() {
        return limitPrice;
    }

    public void setLimitPrice(int limitPrice) {
        this.limitPrice = limitPrice;
    }

    public abstract boolean isExecutable(int currentPrice);
}

Écrivons un test qui utilise ce type d'énumération ʻOrderType`.

src/test/java/com/example/order/OrderTypeTest.java


package com.example.order;

import org.junit.Test;
import static org.junit.Assert.*;

public class OrderTypeTest {
    @Test
    public void testIsExecutableOnMarketOrder() {
        OrderType orderType = OrderType.MARKET;

        assertTrue(orderType.isExecutable(100));
    }

    @Test
    public void testIsExecutableOnLimitOrder() {
        OrderType orderType = OrderType.LIMIT;
        orderType.setLimitPrice(100);

        assertTrue(orderType.isExecutable(100));
    }
}

En fait, la mise en œuvre du type d'ordre de négociation d'actions en tant que type d'énumération pose les problèmes suivants.

Il peut y avoir différentes solutions à ces problèmes, mais cette fois je vais essayer de les résoudre en utilisant un modèle de type de données algébrique.

Implémentez le type d'ordre de négociation d'actions en tant que type de données algébrique en utilisant le modèle de classe Shield

Un article appelé Peut-être en Java est référencé dans Reactive dans la pratique en tant que modèle de classe scellé. Implémentons uniquement les ordres de marché et les ordres limites en tant que types de données algébriques dans ce modèle de classe de bouclier.

src/main/java/com/example/order/OrderType.java


package com.example.order;

public abstract class OrderType {
    private OrderType() {
    }

    public static final class Market extends OrderType {
        @Override
        public boolean isExecutable(int currentPrice) {
            return true;
        }
    }

    public static final class Limit extends OrderType {
        private int limitPrice;

        public Limit(int limitPrice) {
            this.limitPrice = limitPrice;
        }

        public int getLimitPrice() {
            return limitPrice;
        }

        @Override
        public boolean isExecutable(int currentPrice) {
            return currentPrice <= limitPrice;
        }
    }

    public abstract boolean isExecutable(int currentPrice);
}

Voici quelques compléments concernant la classe ʻOrderType. En déclarant ʻOrderType comme classe abstraite et en définissant un constructeur privé, les classes qui peuvent hériter de ʻOrderType sont limitées aux classes internes de ʻOrderType. De plus, en déclarant les classes "Market" et "Limit" comme classes "finales", les classes "Market" et "Limit" ne peuvent pas être héritées. Je pense que c'est la raison pour laquelle on l'appelle le modèle de classe de bouclier.

Écrivons un test qui utilise cette classe ʻOrderType`.

src/test/java/com/example/order/OrderTypeTest.java


package com.example.order;

import org.junit.Test;
import static org.junit.Assert.*;

public class OrderTypeTest {
    @Test
    public void testIsExecutableOnMarketOrder() {
        OrderType orderType = new OrderType.Market();

        assertTrue(orderType.isExecutable(100));
    }

    @Test
    public void testIsExecutableOnLimitOrder() {
        OrderType orderType = new OrderType.Limit(100);

        assertTrue(orderType.isExecutable(100));
    }
}

Les problèmes suivants causés par l'implémentation du type d'ordre de négociation d'actions avec le type d'énumération pourraient être résolus en l'implémentant avec le type de données algébrique.

D'autre part, il existe un problème en ce qu'il est de la responsabilité du type d'ordre de déterminer si l'ordre peut être exécuté ou non. Il s'agit d'un problème commun aux implémentations illustrées pour les types de données énumération et algébrique, respectivement. Que l'ordre puisse être exécuté ou non, il peut arriver que vous souhaitiez implémenter un branchement conditionnel en dehors de la classe ʻOrderType` comme indiqué ci-dessous.

    @Test
    public void testSwitchOnLimitOrder() {
        OrderType orderType = OrderType.LIMIT;
        orderType.setLimitPrice(100);
        int currentPrice = 100;

        boolean result = false;
        switch (orderType) {
            case MARKET:
                result = true;
                break;
            case LIMIT:
                result = currentPrice <= orderType.getLimitPrice();
                break;
            default:
                throw new UnsupportedOperationException("Unsupported order type");
        }

        assertTrue(result);
    }
    @Test
    public void testIfOnLimitOrder() {
        OrderType orderType = new OrderType.Limit(100);
        int currentPrice = 100;

        boolean result = false;
        if (orderType instanceof OrderType.Market) {
            result = true;
        } else if (orderType instanceof OrderType.Limit) {
            result = currentPrice <= orderType.getLimitPrice();
        } else {
            throw new UnsupportedOperationException("Unsupported order type");
        }

        assertTrue(result);
    }

Un tel branchement conditionnel a les problèmes suivants en commun.

Pour résoudre ce problème, introduisons la correspondance de modèles à l'aide du modèle Visiteur.

Implémenter la correspondance de modèles à l'aide du modèle de visiteur

Nous appelons cela la correspondance de modèle pour plus de commodité, mais ne vous attendez pas à une correspondance de modèle comme Scala car elle n'applique que le modèle de visiteur. De plus, la méthode ʻisExecutable` sera supprimée pour rendre le code plus facile à comprendre.

src/main/java/com/example/order/OrderTypeTest.java


package com.example.order;

public abstract class OrderType {
    private OrderType() {
    }

    public static final class Market extends OrderType {
        @Override
        public <T> T match(CaseBlock<T> caseBlock) {
            return caseBlock._case(this);
        }
    }

    public static final class Limit extends OrderType {
        private int limitPrice;

        public Limit(int limitPrice) {
            this.limitPrice = limitPrice;
        }

        public int getLimitPrice() {
            return limitPrice;
        }

        @Override
        public <T> T match(CaseBlock<T> caseBlock) {
            return caseBlock._case(this);
        }
    }

    public interface CaseBlock<T> {
        T _case(Market market);
        T _case(Limit limit);
    }

    public abstract <T> T match(CaseBlock<T> caseBlock);
}

ʻOrderType Déclarez la méthode abstraite matchdans la classe abstraite et implémentez-la dans chaque type. La méthodematch reçoit une instance d'une classe qui implémente l'interface CaseBlock et appelle sa méthode _case. Je ne pense pas qu'il soit nécessaire d'expliquer à nouveau le modèle de visiteur, mais la surcharge de la méthode _caseici provoque le branchement du traitement en fonction de chaque type. C'est un slapstick, mais je l'ai nommé_case parce que case` ne peut pas être utilisé comme mot réservé.

Écrivons un test qui utilise cette classe ʻOrderType`. Je pense que c'est plus facile à comprendre si vous regardez comment l'utiliser.

src/test/java/com/example/order/OrderTypeTest.java


package com.example.order;

import org.junit.Test;
import static org.junit.Assert.*;

public class OrderTypeTest {
    @Test
    public void testPatternMatchingOnLimitOrder() {
        OrderType orderType = new OrderType.Limit(100);
        int currentPrice = 100;

        boolean result = orderType.match(new OrderType.CaseBlock<>() {
            @Override
            public Boolean _case(OrderType.Market market) {
                return true;
            }

            @Override
            public Boolean _case(OrderType.Limit limit) {
                return currentPrice <= limit.getLimitPrice();
            }
        });

        assertTrue(result);
    }
}

Cela résout les problèmes suivants.

Si vous ajoutez un nouveau type à la classe ʻOrderType`, vous devez bien sûr modifier le code. Cependant, tant que la branche conditionnelle est décrite par la correspondance de motif comme décrit ci-dessus, une erreur se produira au moment de la compilation, de sorte que la partie de correction peut être facilement identifiée et que l'omission de correction peut être évitée.

Affiner les types de données algébriques avec Lombok

Maintenant, il s'avère que les modèles de type de données algébriques semblent utiles. Cependant, il est lourd à définir et le code n'est pas bien visible. Pour améliorer un peu ce point, introduisons Lombok ici.

Ajoutez la ligne suivante au fichier build.gradle.

--- a/build.gradle
+++ b/build.gradle
@@ -9,6 +9,7 @@
 plugins {
     // Apply the java-library plugin to add support for Java Library
     id 'java-library'
+    id "io.freefair.lombok" version "4.1.5"
 }
 
 repositories {

L'écriture à l'aide de Lombok ressemble à ceci:

src/main/java/com/example/order/OrderTypeTest.java


package com.example.order;

import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.Value;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public abstract class OrderType {
    @Value
    @EqualsAndHashCode(callSuper = false)
    public static class Market extends OrderType {
        @Override
        public <T> T match(CaseBlock<T> caseBlock) {
            return caseBlock._case(this);
        }
    }

    @Value
    @EqualsAndHashCode(callSuper = false)
    public static class Limit extends OrderType {
        int limitPrice;

        @Override
        public <T> T match(CaseBlock<T> caseBlock) {
            return caseBlock._case(this);
        }
    }

    public interface CaseBlock<T> {
        T _case(Market market);
        T _case(Limit limit);
    }

    public abstract <T> T match(CaseBlock<T> caseBlock);
}

Implémentez la définition d'un constructeur privé avec l'annotation NoArgsConstructor. Pour le type de transaction, ajoutez l'annotation «Value» en tant que classe qui crée un objet de valeur. Cela fait de la classe Market et de la classe Limit la classe finale. Cela simplifie la déclaration des champs dans la classe «Limit», mais tous les champs sont «private final», et les constructeurs et les getters sont automatiquement générés. L'annotation ʻEqualsAndHashCode est ajoutée car un avertissement est émis pour spécifier l'annotation ʻEqualsAndHashCode.

Préparer un constructeur statique

À partir de maintenant, je pense que c'est une question de goût, mais avoir un constructeur statique le rend plus comme ça.

src/main/java/com/example/order/OrderTypeTest.java


package com.example.order;

import lombok.*;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public abstract class OrderType {
    @Value
    @EqualsAndHashCode(callSuper = false)
    @AllArgsConstructor(access = AccessLevel.PROTECTED)
    public static class Market extends OrderType {
        @Override
        public <T> T match(CaseBlock<T> caseBlock) {
            return caseBlock._case(this);
        }
    }

    public static OrderType market() {
        return new Market();
    }

    @Value
    @EqualsAndHashCode(callSuper = false)
    @AllArgsConstructor(access = AccessLevel.PROTECTED)
    public static class Limit extends OrderType {
        int limitPrice;

        @Override
        public <T> T match(CaseBlock<T> caseBlock) {
            return caseBlock._case(this);
        }
    }

    public static OrderType limit(int limitPrice) {
        return new Limit(limitPrice);
    }

    public interface CaseBlock<T> {
        T _case(Market market);
        T _case(Limit limit);
    }

    public abstract <T> T match(CaseBlock<T> caseBlock);
}

Préparez des constructeurs statiques comme la méthode ʻOrderType.market et la méthode ʻOrderType.limit. De plus, les constructeurs des classes «Market» et «Limit» sont des constructeurs protégés avec l'annotation «AllArgsConstructor».

Le test est également légèrement modifié.

src/test/java/com/example/order/OrderTypeTest.java


package com.example.order;

import org.junit.Test;
import static org.junit.Assert.*;

public class OrderTypeTest {
    @Test
    public void testPatternMatchingOnLimitOrder() {
        OrderType orderType = OrderType.limit(100);
        int currentPrice = 100;

        boolean result = orderType.match(new OrderType.CaseBlock<>() {
            @Override
            public Boolean _case(OrderType.Market market) {
                return true;
            }

            @Override
            public Boolean _case(OrderType.Limit limit) {
                return currentPrice <= limit.getLimitPrice();
            }
        });

        assertTrue(result);
    }
}

Si vous le faites jusqu'à présent, il y aura des annotations, alors n'hésitez pas à l'utiliser.

Implémenter le type d'ordre de négociation d'actions en tant que type de données algébrique dans Scala

Cela peut être facilement réalisé avec Scala, alors essayons-le avec le REPL de Scala.

scala> :paste 
// Entering paste mode (ctrl-D to finish)

sealed trait OrderType
case object Market extends OrderType
case class Limit(limitPrice: Int) extends OrderType

// Exiting paste mode, now interpreting.

defined trait OrderType
defined object Market
defined class Limit

scala> :paste 
// Entering paste mode (ctrl-D to finish)

val orderType: OrderType = Limit(100)
val currentPrice = 100

orderType match {
  case Market => true
  case Limit(limitPrice) => currentPrice <= limitPrice
}

// Exiting paste mode, now interpreting.

orderType: OrderType = Limit(100)
currentPrice: Int = 100
res3: Boolean = true

Il peut être écrit de manière concise avec Scala. Kotlin semble être concis, mais je ne l'ai pas essayé.


Cette fois, j'ai expliqué ce qu'on appelle un modèle de type de données algébrique. D'une certaine manière, Java est très expressif. De plus, à mesure que Java évolue, il semble que des grammaires qui facilitent la mise en œuvre de modèles de type de données algébriques seront introduites, il semble donc que nous garderons un œil sur Java à l'avenir. Cependant, l'expressivité de Scala est toujours fascinante, j'espère donc profiter à la fois de Java et de Scala.

Les références

Reactive in practice: A complete guide to event-driven systems development in Java

Recommended Posts

Une évolution de l'énumération et des instructions switch! ?? Essayez d'obtenir des types de données algébriques et des correspondances de modèles en Java
Essayez de réaliser une correspondance de modèle de type Option de type Scala et une carte, flatMap en Java
Discrimination d'énum dans Java 7 et supérieur
[Java] Types de commentaires et comment les rédiger
Branchement conditionnel Java: comment créer et étudier des instructions de commutation
[Java Silver] Ce qu'il faut savoir concernant les instructions switch
Instructions Java if et switch
Lisez les données de Shizuoka Prefecture Point Cloud DB avec Java et essayez de détecter la hauteur de l'arbre.
[Introduction à Java] À propos des variables et des types (déclaration de variable, initialisation, type de données)
Bases du développement Java ~ Comment écrire des programmes (variables et types) ~
Comment supprimer de grandes quantités de données dans Rails et problèmes
Types de données de base et types de référence (Java)
Essayez d'implémenter Yuma en Java
Types de données de base et types de référence Java
Liste des types ajoutés dans Java 9
Exemple de code pour la sérialisation et la désérialisation des énumérations Java Enum et JSON dans Jackson
L'histoire de l'oubli de fermer un fichier en Java et de l'échec
Comment modifier le nombre maximum et maximum de données POST dans Spark
Essayez de résoudre Project Euler en Java
Essayez d'implémenter l'ajout n-aire en Java
[Introduction à Java] À propos des déclarations et des types de variables
De Java à C et de C à Java dans Android Studio
J'ai reçu les données du voyage (application agenda) en Java et j'ai essayé de les visualiser # 001
Exemple de code pour obtenir les valeurs de type SQL clés dans Java + MySQL 8.0