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. ..
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.
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
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.
setLimitPrice
dans l'ordre du marché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.
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.
setLimitPrice
dans l'ordre du marché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.
, vous devez trouver et corriger les branches conditionnelles pour tous les ʻOrderType
.Pour résoudre ce problème, introduisons la correspondance de modèles à l'aide du modèle 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éthode
match 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.
, vous devez trouver et corriger les branches conditionnelles pour tous les ʻOrderType
.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.
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
.
À 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.
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.
Reactive in practice: A complete guide to event-driven systems development in Java
Recommended Posts