Eine Weiterentwicklung von Aufzählungs- und Schalteranweisungen! ?? Versuchen Sie, in Java algebraische Datentypen und Mustervergleiche zu erzielen

Mikatus Adventskalender 2019 Tag 8 Artikel.

Wenn Sie Reaktiv in der Praxis: Eine vollständige Anleitung zur ereignisgesteuerten Systementwicklung in Java lesen, dann Das algebraische Datentypmuster (algebraisches Datentypmuster) Datentypmuster) Es war interessant festzustellen, dass etwas herauskam. ..

Beispiel: Ausdrücken der Auftragsart im Aktienhandelssystem

Reactive in der Praxis bietet ein Beispiel für die Darstellung der Auftragstypen im Aktienhandelssystem Stock Trader. Wie jeder weiß, der Aktien gehandelt hat, gibt es die folgenden Arten von Aktienaufträgen.

Zur Darstellung dieser Ordnungen wird ein Entwurfsmuster verwendet, das in der Praxis in Reactive als algebraisches Datentypmuster bezeichnet wird. Warum nicht ein Aufzählungstyp (Enum)? Lassen Sie uns das Rätsel lösen.

Generieren Sie ein experimentelles Gradle-Projekt

Ich möchte auch Lombok verwenden, also werde ich mit dem Gradle-Projekt anstelle von JShell experimentieren. Erstellen Sie ein Gradle-Projekt mit dem folgenden Befehl.

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

Implementieren Sie die Auftragsart des Aktienhandels als Aufzählungstyp

Lassen Sie uns zunächst nur Marktaufträge implementieren und Aufträge als Aufzählungstypen begrenzen. Da Limit Orders einen Limitpreis enthalten müssen, geben Sie dem Aufzählungstyp ein Feld "limitPrice". Es definiert auch eine Getter-Methode "getLimitPrice" und eine Setter-Methode "setLimitPrice". Wir haben eine abstrakte Methode "isExecutable" definiert, die bestimmt, ob die Reihenfolge durchführbar ist, und sie mit jedem Aufzählungswert überschreibt.

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

Schreiben wir einen Test, der diesen Aufzählungstyp "OrderType" verwendet.

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

Tatsächlich weist die Implementierung des Auftragstyps des Aktienhandels als Aufzählungstyp die folgenden Probleme auf.

Es mag verschiedene Lösungen für diese Probleme geben, aber dieses Mal werde ich versuchen, sie mit einem algebraischen Datentypmuster zu lösen.

Implementieren Sie den Auftragstyp des Aktienhandels als algebraischen Datentyp unter Verwendung des Schildklassenmusters

Ein Artikel mit dem Namen Vielleicht in Java wird in Reactive in der Praxis als versiegeltes Klassenmuster bezeichnet. Lassen Sie uns nur Marktaufträge implementieren und Aufträge als algebraische Datentypen in diesem Schildklassenmuster begrenzen.

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

Hier sind einige Ergänzungen zur OrderType-Klasse. Durch Deklarieren von "OrderType" als abstrakte Klasse und Definieren eines privaten Konstruktors werden die Klassen, die "OrderType" erben können, auf die inneren Klassen von "OrderType" beschränkt. Wenn Sie die Klassen "Market" und "Limit" als "endgültige" Klassen deklarieren, können die Klassen "Market" und "Limit" nicht vererbt werden. Ich denke, dies ist der Grund, warum es das Schildklassenmuster genannt wird.

Schreiben wir einen Test, der diese OrderType-Klasse verwendet.

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

Die folgenden Probleme, die durch die Implementierung des Auftragstyps des Aktienhandels mit dem Aufzählungstyp verursacht werden, können durch Implementierung mit dem algebraischen Datentyp gelöst werden.

Andererseits besteht das Problem, dass es in der Verantwortung des Auftragstyps liegt, zu bestimmen, ob der Auftrag ausgeführt werden kann oder nicht. Dies ist ein Problem, das bei den für Aufzählungs- bzw. algebraische Datentypen dargestellten Implementierungen häufig auftritt. Möglicherweise möchten Sie eine bedingte Verzweigung außerhalb der OrderType-Klasse implementieren, unabhängig davon, ob die Bestellung ausgeführt werden kann oder nicht, wie unten gezeigt.

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

Eine solche bedingte Verzweigung hat die folgenden Probleme gemeinsam.

Um dieses Problem zu lösen, führen wir den Mustervergleich mithilfe des Besuchermusters ein.

Implementieren Sie den Mustervergleich mithilfe des Besuchermusters

Wir nennen es der Einfachheit halber Mustervergleich, erwarten jedoch keinen Mustervergleich wie Scala, da nur das Besuchermuster angewendet wird. Außerdem wird die Methode "isExecutable" entfernt, um das Verständnis des Codes zu erleichtern.

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

Die abstrakte Methode "match" wird in der abstrakten Klasse "OrderType" deklariert und in jedem Typ implementiert. Die Methode "match" empfängt eine Instanz einer Klasse, die die Schnittstelle "CaseBlock" implementiert und ihre Methode "_case" aufruft. Ich denke nicht, dass es notwendig ist, das Besuchermuster noch einmal zu erklären, aber die Überladung der _case -Methode hier führt dazu, dass die Verarbeitung je nach Typ verzweigt. Es ist ein Slapstick, aber ich habe ihn "_case" genannt, weil "case" nicht als reserviertes Wort verwendet werden kann.

Schreiben wir einen Test, der diese OrderType-Klasse verwendet. Ich denke, es ist einfacher zu verstehen, wenn man sich ansieht, wie man es benutzt.

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

Dies löst die folgenden Probleme.

Wenn Sie der OrderType-Klasse einen neuen Typ hinzufügen, müssen Sie natürlich den Code ändern. Solange jedoch der bedingte Zweig durch die oben beschriebene Musterübereinstimmung beschrieben wird, tritt zum Zeitpunkt der Kompilierung ein Fehler auf, so dass der Korrekturteil leicht identifiziert und das Auslassen der Korrektur verhindert werden kann.

Verfeinern Sie algebraische Datentypen mit Lombok

Nun stellt sich heraus, dass algebraische Datentypmuster nützlich erscheinen. Die Definition ist jedoch umständlich und der Code ist nicht gut sichtbar. Um diesen Punkt ein wenig zu verbessern, stellen wir hier Lombok vor.

Fügen Sie der Datei "build.gradle" die folgende Zeile hinzu.

--- 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 {

Das Schreiben mit Lombok sieht folgendermaßen aus:

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

Implementieren Sie die Definition eines privaten Konstruktors mit der Annotation "NoArgsConstructor". Fügen Sie für den Transaktionstyp die Annotation "Wert" als Klasse hinzu, die ein Wertobjekt erstellt. Dies macht sowohl die "Market" -Klasse als auch die "Limit" -Klasse zur "Final" -Klasse. Es vereinfacht die Deklaration von Feldern in der Klasse "Limit", aber alle Felder sind "private final" und Konstruktoren und Getter werden automatisch generiert. Die Annotation "EqualsAndHashCode" wird hinzugefügt, da eine Warnung ausgegeben wird, um die Annotation "EqualsAndHashCode" anzugeben.

Bereiten Sie einen statischen Konstruktor vor

Von hier an denke ich, dass es Geschmackssache ist, aber ein statischer Konstruktor macht es eher so.

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

Bereiten Sie statische Konstruktoren wie die OrderType.market-Methode und die OrderType.limit-Methode vor. Darüber hinaus werden die Konstruktoren für die Klassen "Market" und "Limit" mit der Annotation "AllArgsConstructor" zu geschützten Konstruktoren.

Der Test ist ebenfalls leicht modifiziert.

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

Wenn Sie dies bisher tun, werden die Anmerkungen durcheinander gebracht. Sie können es also gerne verwenden.

Implementieren Sie den Auftragstyp des Aktienhandels als algebraischen Datentyp in Scala

Dies kann mit Scala leicht erreicht werden. Versuchen wir es also mit Scalas 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

Es kann kurz mit Scala geschrieben werden. Kotlin scheint prägnant zu sein, aber ich habe es nicht versucht.


Dieses Mal erklärte ich, was als algebraisches Datentypmuster bezeichnet wird. Irgendwie ist Java sehr ausdrucksstark. Während sich Java weiterentwickelt, werden anscheinend Grammatiken eingeführt, die die Implementierung algebraischer Datentypmuster erleichtern, sodass wir Java in Zukunft im Auge behalten werden. Die Ausdruckskraft von Scala ist jedoch immer noch faszinierend, daher würde ich gerne sowohl Java als auch Scala genießen.

Verweise

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

Recommended Posts

Eine Weiterentwicklung von Aufzählungs- und Schalteranweisungen! ?? Versuchen Sie, in Java algebraische Datentypen und Mustervergleiche zu erzielen
Versuchen Sie, Scala-ähnliche Optionstyp-Musterübereinstimmung und Map FlatMap in Java zu realisieren
Diskriminierung von Enum in Java 7 und höher
[Java] Arten von Kommentaren und wie man sie schreibt
Bedingte Java-Verzweigung: Erstellen und Studieren von switch-Anweisungen
[Java Silver] Hinweise zu switch-Anweisungen
Java if- und switch-Anweisungen
Lesen Sie die Daten der Shizuoka Prefecture Point Cloud DB mit Java und versuchen Sie, die Baumhöhe zu ermitteln.
[Einführung in Java] Informationen zu Variablen und Typen (Variablendeklaration, Initialisierung, Datentyp)
Grundlagen der Java-Entwicklung ~ Schreiben von Programmen (Variablen und Typen) ~
So löschen Sie große Datenmengen in Rails und Bedenken
Grundlegende Datentypen und Referenztypen (Java)
Versuchen Sie, Yuma in Java zu implementieren
Java-Basisdatentypen und Referenztypen
Liste der in Java 9 hinzugefügten Typen
Beispielcode zum Serialisieren und Deserialisieren von Java Enum-Enumerationen und JSON in Jackson
Die Geschichte, zu vergessen, eine Datei in Java zu schließen und zu scheitern
So ändern Sie die maximale und maximale Anzahl von POST-Daten in Spark
Versuchen Sie, Project Euler in Java zu lösen
Versuchen Sie, n-ary Addition in Java zu implementieren
[Einführung in Java] Informationen zu Variablendeklarationen und -typen
Von Java nach C und von C nach Java in Android Studio
Ich habe die Daten der Reise (Tagebuchanwendung) in Java erhalten und versucht, sie # 001 zu visualisieren
Beispielcode zum Abrufen der wichtigsten SQL-Typwerte in Java + MySQL 8.0