Bedingungen abstrahieren

looparda

Top Contributor
Man hat eine Bedingung, deren Einhaltung man prüfen muss und damit verbunden hat man immer das gleiche Muster:
Java:
int x = userInput();
if( x <= 5 ) {
    String msg = "x muss größer als 5 sein, ist aber '" + x + "'";
    log.warn(msg)
    throw new IllegalStateException(msg);
} else {
    return x + 9;
}
Was mich stört ist die Bedingung sowohl im if als auch in der Nachricht (wahlweise als Exception, Anzweige auf der View oder Logging). Warum nicht in etwas wie dieses Konstrukt verpacken, wobei sich die Condition intern um das Logging kümmert (Bedingung und eigentlicher Wert).

Java:
int x = userInput();
Condition c = Condition.greaterThanOrEqual(5, "%d muss größer als $1 sein");
return c.testOrThrow(x).andThen(() -> return x + 9);

Conditions sollten so gestaltet sein, dass man Ausdrücke bilden kann. Ich kenne das bereits von JavaFX Bindings.
Java:
int x = userInput();
Condition c = Condition.and(
                Condition.greaterThanOrEqual(5, "%d muss größer als $1 sein")
                otherCondition);
return c.testOrThrow(x).andThen(() -> return x + 9);

Vor allem, wenn man Prädikate einsetzt geht die Information verloren, warum ein Element gefiltert wurde. Das erschwert das Debugging meiner Meinung nach.
Java:
Predicate<Integer> greaterThanOrEqual = i -> i >= 5;
List<Integer> ints = IntStream.iterate(0, i -> i+1)
                              .limit(10)
                              .boxed()
                              .collect(toList());
Schöner wäre so ein Konstrukt. Wo auch das Logging intern wieder erfolgt und test()-Aufrufe eines Prädikats, die false zurückliefern, loggt mit Bedingung und eigentlichem Wert.
Java:
Condition c = Condition.greaterThanOrEqual(5, "%d muss größer als $1 sein");
List<Integer> filteres = ints.stream()
                                 .filter(c)
                                 .collect(toList());
return c.testOrThrow(x).andThen(() -> return x + 9);

Kennt jemand einen bestehenden Ansatz/Lib dafür? Ich lande bei Recherchen immer bei Constraint Programming, was jedoch die falsche Richtung ist.
 

looparda

Top Contributor
Ist die Idee zu bescheuert oder war die Erklärung nicht nachvollziehbar? Im vorletzten Beispiel fehlt natürlich der filter(greaterThanOrEqual)-Aufruf, habe ich bemerkt.
 

mihe7

Top Contributor
Das Problem an der Sache sind IMO Verknüpfungen, die den Sinn der Meldung verändern können wie z. B. negate() und or(). Ansonsten dürfte das relativ einfach umzusetzen sein.
 

looparda

Top Contributor
Ich hatte tatsächlich schon angefangen was eigenes zu stricken, aber schnell gemerkt, wie umfangreich es wird. Ich kann es mir auf jeden Fall nicht aus dem Ärmel schütteln sondern müsste noch viel dazulernen, bevor ich es hinbekomme. Problem ist vor allem es generisch für alle möglichen Datentypen zu machen. Außerdem sehe ich Parallelen zu libs wie AssertJ.
 

White_Fox

Top Contributor
Ich finde das Problem interessant, verstehe es aber nicht so richtig.

Das, was du beschreibst, würde ich ganz naiv so erschlagen (aber darauf wäre doch sicher jeder selbst gekommen?):
Java:
abstract class ConditionProcessor{
    public boolean process();
}

public class UserinputProcessor extends ConditionProcessor{
    final boolean userinputLessThanFive;
    
    public UserinputProcessor(String input){
        userinputLessThanFive = input.toDouble < 5; //Hab auf die Schnelle vergessen wie man einen String zu int konvertiert
    }
    
    @Override
    public boolean process(){return userinputLessThanFive;}
}

Wahlweise könnte man noch die process()-Methode mit einem Parameter ausstatten, um mehrere Bedingungen auszuwerten.

Außerdem sehe ich Parallelen zu libs wie AssertJ.
Ich weiß nicht was AssertJ macht, aber ich würde eine Bibliothek nicht für Zwecke mißbrauchen, für die sie nicht geschaffen ist. Auch wenn es logisch gehen würde, ist der Quellcode dann gar gräßlich zu lesen. Und wehe, man muß da irgendwann mal wieder ran.
 

looparda

Top Contributor
Das, was du beschreibst, würde ich ganz naiv so erschlagen (aber darauf wäre doch sicher jeder selbst gekommen?):
Ja, das deckt den einfachsten Fall ab, die Evaluation einer einfachen Bedingung zu abstrahieren. Jedoch ist das Problem:
Problem ist vor allem es generisch für alle möglichen Datentypen zu machen.

Ich weiß nicht was AssertJ macht, aber ich würde eine Bibliothek nicht für Zwecke mißbrauchen
Ich habe nur gesagt, dass ich parallelen zu AssertJ sehe, da diese Library im Grunde eben (sinnvolle) Assertions für alle möglichen Datentypen anbietet. Der Aufbau wird sehr ähnlich sein.
 

mrBrown

Super-Moderator
Mitarbeiter
Ja, das deckt den einfachsten Fall ab, die Evaluation einer einfachen Bedingung zu abstrahieren. Jedoch ist das Problem:
Problem ist vor allem es generisch für alle möglichen Datentypen zu machen.

Müsste man einfach mal versuchen, grundsätzlich sieht das schon machbar aus. Für ein MVP kann man es ja erstmal auf einen konkreten Typen beschränken, zB Integer. Dann sieht man schon mal die ersten Schwierigkeiten, die es gibt und kann nachher immer noch abstrahieren.

Ich habe nur gesagt, dass ich parallelen zu AssertJ sehe, da diese Library im Grunde eben (sinnvolle) Assertions für alle möglichen Datentypen anbietet. Der Aufbau wird sehr ähnlich sein.
Jein, der Aufbau dort kennt keinerlei Verknüpfungen, jede Assertion steht für sich, das funktioniert schon grundsätzlich anders.
 

looparda

Top Contributor
Müsste man einfach mal versuchen, grundsätzlich sieht das schon machbar aus.
Ich habe es versucht, kam aber nicht weiter und hatte bisher keine Zeit weiter zu probieren. Ich hoffe, dass ich bald weiterkomme und konkrete Fragen/Probleme dazu formulieren kann.

Jein, der Aufbau dort kennt keinerlei Verknüpfungen, jede Assertion steht für sich, das funktioniert schon grundsätzlich anders.
Ich meinte, dass der Aufbau der Lib, für alle möglichen Datentypen passende Assertions bereitzustellen dort ebenfalls angegangen wurde. Ebenso werden die Assertions in hilfreiche Ausgaben Umgewandelt. In der Hinsicht kann man sich da vermutlich etwas abschauen. Ja, Verknüpfungen sind dort nicht vorhanden.
 
Zuletzt bearbeitet:

looparda

Top Contributor
Die Idee liegt schon eine Weile zurück und ich hatte nicht so viel Zeit daran zu arbeiten. Ich hab es immer wieder mal angeschaut und wieder zur Seite gelegt, weil ich dachte mir fällt schon noch etwas besseres ein. Aber leider ist mir nichts besseres eingefallen.

Java:
/**
* @param <T> Type of the input
* @param <R> Type of the result
*/
public abstract class Condition<T, R> {

    protected abstract boolean doTest(T actual);
    protected abstract String getExplanation(T actual);
    protected abstract String getMessage();
    protected abstract R getExpected();

    private static final Logger log = Logger.getLogger( Condition.class.getName() );

    public static Condition<Integer, Integer> greaterThanOrEqual(int expected) {
        Predicate<? super Integer> p = integer -> integer >= expected;
        return new IntegerCondition<>(p, expected, "%d muss >= $1 sein");
    }

    public static Condition<Integer, Integer> greaterThan(int expected) {
        Predicate<? super Integer> p = integer -> integer > expected;
        return new IntegerCondition<>(p, expected, "%d muss > $1 sein");
    }

    public static <T> Condition<T, Boolean> and(Condition<T, T> a, Condition<T, T> b) {
        return new AndCondition<>(a, b);
    }

    public static Condition<String, String> greaterThanOrEqual(String expected) {
        Predicate<? super String> p = string -> string.compareTo(expected) >= 0;
        return new StringCondition<>(p, expected, "%s muss > $1 sein");
    }

    public static <T, Attribute> Condition<T, Attribute> fromPredicate(Predicate<T> p, Function<T, Attribute> extractor, Attribute expected) {
        return new PredicateCondition<>(p, extractor, expected, "%s muss $1 sein");
    }

    public boolean test(T actual) {
        final boolean result = doTest(actual);
        if (!result) {
            log.info("Condition failed because: " + getExplanation(actual));
        }
        return result;
    }

    public ConditionEvaluationStage<T, R> testOrThrow(T actual) {
        if( !test(actual) ) {
            final String explanation = getExplanation(actual);
            throw new IllegalStateException(explanation);
        }
        return new ConditionEvaluationStage<>(this, actual);
    }

}

Java:
public class AndCondition<T> extends Condition<T, Boolean> {

    private final Condition<T, T> a;
    private final Condition<T, T> b;

    public AndCondition(Condition<T, T> a, Condition<T, T> b) {
        this.a = a;
        this.b = b;
    }

    @Override
    public boolean doTest(T actual) {
        return a.test(actual) && b.test(actual) == getExpected();
    }

    @Override
    protected String getExplanation(T actual) {
        return a.getExplanation(actual) + " AND " + b.getExplanation(actual);
    }

    @Override
    protected String getMessage() {
        return String.format("%s AND %s", a.getMessage(), b.getMessage());
    }

    @Override
    protected Boolean getExpected() {
        return Boolean.TRUE;
    }
}

Java:
public class IntegerCondition<T extends Integer> extends Condition<T, T> {

    private final Predicate<? super T> predicate;
    private final T expected;
    private final String message;

    public IntegerCondition(Predicate<? super T> predicate, T expected, String message) {
        this.predicate = predicate;
        this.expected = expected;
        this.message = message;
    }

    @Override
    public boolean doTest(T actual) {
        return predicate.test(actual);
    }

    @Override
    protected String getExplanation(T actual) {
        return String.format(getMessage(), actual).replaceAll("\\$1", "" + getExpected());
    }

    @Override
    protected String getMessage() {
        return message;
    }

    @Override
    protected T getExpected() {
        return expected;
    }

}

Java:
public class PredicateCondition<T, X> extends Condition<T, X> {

    private final Predicate<? super T> predicate;
    private final Function<T, X> extractor;
    private final X expected;
    private final String message;

    public PredicateCondition(Predicate<? super T> predicate, Function<T, X> extractor, X expected, String message) {
        this.predicate = predicate;
        this.extractor = extractor;
        this.expected = expected;
        this.message = message;
    }

    @Override
    public boolean doTest(T actual) {
        return predicate.test(actual);
    }

    @Override
    protected String getExplanation(T actual) {
        return String.format(getMessage(), extractor.apply(actual)).replaceAll("\\$1", "" + getExpected());
    }

    @Override
    protected String getMessage() {
        return message;
    }

    @Override
    protected X getExpected() {
        return expected;
    }

}

Java:
public class StringCondition<T extends String> extends Condition<T, T> {

    private final Predicate<? super T> predicate;
    private final T expected;
    private final String message;

    public StringCondition(Predicate<? super T> predicate, T expected, String message) {
        this.predicate = predicate;
        this.expected = expected;
        this.message = message;
    }

    @Override
    public boolean doTest(T actual) {
        return predicate.test(actual);
    }

    @Override
    protected String getExplanation(T actual) {
        return String.format(getMessage(), actual).replaceAll("\\$1", "" + getExpected());
    }

    @Override
    protected String getMessage() {
        return message;
    }

    @Override
    protected T getExpected() {
        return expected;
    }

}

Java:
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;

@DisplayName("Conditions")
class ConditionTest {

    @Nested
    @DisplayName("greaterThanOrEqual")
    class GreaterThanOrEqual {

        private Condition<Integer, Integer> c;

        @BeforeEach
        public void beforeEach() {
            this.c = Condition.greaterThanOrEqual(5);
        }

        @DisplayName("throws exception")
        @Test
        void throwsException() {
            assertThatThrownBy(() -> c.testOrThrow(1)).isInstanceOf(IllegalStateException.class).hasMessage("1 muss >= 5 sein");
        }

        @DisplayName("throws no exception")
        @Test
        void throwsNoException() {
            assertThat(c.testOrThrow(6).andThenApply((x) -> x + 9)).isEqualTo(15);
        }

    }

    @Nested
    @DisplayName("greaterThan")
    class GreaterThan {
        private Condition<Integer, Integer> c;

        @BeforeEach
        public void beforeEach() {
            this.c = Condition.greaterThan(5);
        }

        @DisplayName("throws exception")
        @Test
        void throwsException() {
            assertThatThrownBy(() -> c.testOrThrow(1)).isInstanceOf(IllegalStateException.class).hasMessage("1 muss > 5 sein");
        }

        @DisplayName("throws no exception")
        @Test
        void throwsNoException() {
            assertThat(c.testOrThrow(6).andThenApply((x) -> x + 9)).isEqualTo(15);
        }

    }

    @Nested
    @DisplayName("And")
    class And {

        private Condition<Integer, Integer> a;
        private Condition<Integer, Integer> b;
        private Condition<Integer, Boolean> c;

        @BeforeEach
        public void beforeEach() {
            this.a = Condition.greaterThan(5);
            this.b = Condition.greaterThanOrEqual(6);
            this.c = Condition.and(a, b);
        }

        @DisplayName("throws exception")
        @Test
        void throwsException() {
            assertThatThrownBy(() -> c.testOrThrow(1)).isInstanceOf(IllegalStateException.class).hasMessage("1 muss > 5 sein AND 1 muss >= 6 sein");
            assertThatThrownBy(() -> c.testOrThrow(5)).isInstanceOf(IllegalStateException.class).hasMessage("5 muss > 5 sein AND 5 muss >= 6 sein");
        }

        @DisplayName("throws no exception")
        @Test
        void throwsNoException() {
            assertThat(c.testOrThrow(6).andThenApply((x) -> x + 9)).isEqualTo(15);
        }

    }

}

Code:
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

@DisplayName("Conditions with real world examples")
class ConditionIntegrationTest {

    @Nested
    @DisplayName("integer lists filtering")
    class IntegerListFiltering {

        private List<Integer> list;

        @BeforeEach
        public void beforeEach() {
            this.list = IntStream.iterate(1, i -> i + 1)
                                 .limit(10)
                                 .boxed()
                                 .collect(Collectors.toList());
        }

       @DisplayName("List should contain filtered elements only")
       @Test
       public void ListShouldContainFilteredElementsOnly() {
           Condition<Integer, Integer> c = Condition.greaterThanOrEqual(5);
           List<Integer> filtered = list.stream().filter(c::test).collect(Collectors.toList());
           assertThat(filtered).containsExactly(5, 6, 7, 8, 9, 10);
       }
    }

    @Nested
    @DisplayName("integer lists filtering")
    class StringListFiltering {

        private List<String> list = new ArrayList<>();

        @BeforeEach
        public void beforeEach() {
            list.addAll(Arrays.asList("a", "b", "x", "y", "z"));
        }

        @DisplayName("List should contain filtered elements only")
        @Test
        public void ListShouldContainFilteredElementsOnly() {
            Condition<String, String> c = Condition.greaterThanOrEqual("x");
            List<String> filtered = list.stream().filter(c::test).collect(Collectors.toList());
            assertThat(filtered).containsExactly("x", "y", "z");
        }
    }


    @Nested
    @DisplayName("integer lists filtering")
    static class EnumListFiltering {
        enum Gender {
            F,M
        }
        class Person {
            Gender gender;

            public Person(Gender gender) {
                this.gender = gender;
            }

            public Gender getGender() {
                return gender;
            }
        }
        private List<Person> list = new ArrayList<>();

        @BeforeEach
        public void beforeEach() {
            list.addAll(Arrays.asList(new Person(Gender.F), new Person(Gender.M), new Person(Gender.F)));
        }

        @DisplayName("List should contain filtered elements only")
        @Test
        public void ListShouldContainFilteredElementsOnly() {
            Function<Person, Gender> extractor = Person::getGender;
            Predicate<Person> isMale = (p) -> extractor.apply(p) == Gender.M;
            Condition<Person, Gender> c = Condition.fromPredicate(isMale, extractor, Gender.M);
            List<Person> filtered = list.stream().filter(c::test).collect(Collectors.toList());
            assertThat(filtered).hasSize(1);
        }
    }
}
[CODE=java]

Probleme:
1. Was mich am meisten stört ist, dass der Client die beiden Typparameter für Type der Eingabe und Typ der Ausgabe angeben muss
Condition<Integer, Integer> c = Condition.greaterThanOrEqual(5);

2. Die Fehlermeldungen ein Problem: "M muss W" sein für das Beispiel mit dem Gender ist halt blöd zu lesen. Geschweige denn das angesprochene Problem mit Negationen.

Vielleicht hat jemand Anregungen. Jedenfalls wollte ich einfach mal den Stand teilen.
 

mrBrown

Super-Moderator
Mitarbeiter
1. Was mich am meisten stört ist, dass der Client die beiden Typparameter für Type der Eingabe und Typ der Ausgabe angeben muss
Condition<Integer, Integer> c = Condition.greaterThanOrEqual(5);
var c = Condition.greaterThanOrEqual(5);:cool:

Da wird man nicht drum rum kommen, wobei man es mit Interfaces etwas einschränken könnte, hast du ja bei Integer und StringCondition auch schon getan. Wenn man das alles konsequent als Interfaces umsetzt, dürfte das noch etwas besser klappen

2. Die Fehlermeldungen ein Problem: "M muss W" sein für das Beispiel mit dem Gender ist halt blöd zu lesen. Geschweige denn das angesprochene Problem mit Negationen.
Soll die Fehlermeldung Menschen oder Entwickler-Lesbar sein?
 

looparda

Top Contributor
Wenn man das alles konsequent als Interfaces umsetzt, dürfte das noch etwas besser klappen
Muss ich nochmal ausprobieren. Hab es noch nicht ganz vor Augen.

Soll die Fehlermeldung Menschen oder Entwickler-Lesbar sein?
Im Optimalfall natürlich auch lesbar für den Nutzer. Da ich jedoch der einzige bin, der den Kram liest reicht auch für Entwickler. Ich vermute worauf du hinaus willst - bei Negation den Ausdruck einfach bei der Ausgabe in !() hüllen.
 

Ähnliche Java Themen

Neue Themen


Oben