Mikatus Advent Calendar 2019 Day 8 article.
If you read Reactive in practice: A complete guide to event-driven systems development in Java, then The algebraic data type pattern (algebraic) Data type pattern) It was interesting to note that something came out. ..
Reactive in practice provides an example of how to represent the types of orders in the stock trading system Stock Trader. As anyone who has traded stocks knows, there are the following types of stock orders:
To represent these orders, a design pattern called the algebraic data type pattern in Reactive in practice is used. Why not an enum (Enum)? Let's unravel the mystery.
I also want to use Lombok, so experiment with the Gradle project instead of JShell. Let's generate a Gradle project with the following command.
$ mkdir stock-trader
$ cd stock-trader
$ gradle init \
--type java-library \
--dsl groovy \
--test-framework junit \
--project-name stock-trader \
--package com.example
First, let's implement only market orders and limit orders as enumerated types. Since limit orders need to hold a limit price, give the enum a limitPrice
field. It also defines a getter getLimitPrice
method and a setter setLimitPrice
method. ʻIsExecutable` An abstract method that determines whether an order is feasible is defined and overridden by each enumeration value.
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);
}
Let's write a test that uses this ʻOrderType` enum.
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));
}
}
As a matter of fact, implementing the order type of stock trading as an enumeration has the following problems.
setLimitPrice
method in the market order.There may be various solutions to these problems, but this time I will try to solve them using algebraic data type patterns.
An article called Maybe in Java is referenced in Reactive in practice as a Sealed Class Pattern. Let's implement only market orders and limit orders as algebraic data types in that shield class pattern.
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);
}
Here are some supplements about the ʻOrderType class. By declaring ʻOrderType
as an abstract class and defining a private constructor, the classes that can inherit ʻOrderType are limited to the inner classes of ʻOrderType
. Also, by declaring the Market
and Limit
classes as final
classes, the Market
and Limit
classes cannot be inherited. I think this is the reason why it is called the shield class pattern.
Let's write a test that uses this ʻOrderType` class.
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));
}
}
The following problems caused by implementing the order type of stock trading by enumeration type could be solved by implementing it by algebraic data type.
setLimitPrice
method in the market order.On the other hand, there is a problem that it is the responsibility of the order type to determine whether or not the order can be executed. This is a problem common to the implementations illustrated for enums and algebraic data types, respectively. Whether or not the order can be executed, there may be times when you want to implement conditional branching outside the ʻOrderType` class as shown below.
@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);
}
Such conditional branching has the following problems in common.
class, you need to find and fix conditional branches for all ʻOrderType
.To solve this problem, let's introduce pattern matching using the Visitor pattern.
We call it pattern matching for convenience, but don't expect pattern matching like Scala as it only applies the Visitor pattern. Also, the ʻisExecutable` method will be removed to make the code easier to understand.
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 declares a
matchabstract method in the abstract class and implements it in each type. The
match method receives an instance of a class that implements the
CaseBlockinterface and calls its
_casemethod. I don't think it's necessary to explain the Visitor pattern again, but the overloading of the
_casemethod here causes the process to branch off depending on each type. It's a slapstick, but I named it
_case because
case` cannot be used as a reserved word.
Let's write a test that uses this ʻOrderType` class. I think it's easier to understand if you look at how to use it.
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);
}
}
This solves the following problems.
class, you need to find and fix conditional branches for all ʻOrderType
.If you add a new type to the ʻOrderType` class, of course you have to modify the code. However, as long as the conditional branch is described by the pattern matching as described above, an error will occur at compile time, so that the correction part can be easily identified and the correction omission can be prevented.
Now, it turns out that algebraic data type patterns seem useful. However, it is cumbersome to define and the code is not well visible. To improve that point a little, let's introduce Lombok here.
Add the following line to the build.gradle
file.
--- 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 {
Writing using Lombok looks like this:
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);
}
Implement the definition of private constructor with NoArgsConstructor
annotation. The transaction type is annotated with Value
as a class that creates a value object. Now both the Market
class and the Limit
class are final
classes. It simplifies the declaration of fields in the Limit
class, but all fields are private final
, and constructors and getters are automatically generated. Note that the ʻEqualsAndHashCode annotation is added because a warning is issued to specify the ʻEqualsAndHashCode
annotation.
From here on, I think it's a matter of taste, but having a static constructor makes it more like that.
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);
}
Prepare static constructors like the ʻOrderType.market method and the ʻOrderType.limit
method. On top of that, the constructors for the Market
and Limit
classes are made into protected constructors with the ʻAllArgsConstructor` annotation.
The test is also slightly modified.
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);
}
}
If you do so far, it will be messed up with annotations, so please feel free to use it.
This is easy to do with Scala, so let's try it with the Scala REPL.
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
It can be written concisely with Scala. Kotlin seems to be concise, but I haven't tried it.
This time, I explained what is called an algebraic data type pattern. Somehow Java is expressive. Also, as Java evolves, it seems that grammars that make it easier to implement algebraic data type patterns will be introduced, so it seems that we will keep an eye on Java in the future. That said, Scala's expressiveness is still fascinating, so I'd love to enjoy both Java and Scala.
Reactive in practice: A complete guide to event-driven systems development in Java
Recommended Posts