2
\$\begingroup\$

Intro

This class emulates a switch statement, in order to

  • include non-switchable types (POJOs)
  • have the same switch statement executable for different values
  • be able to extend a previously defined switch
  • TODO: Refactor into ConsumerSwitch.Builder and FunctionSwitch.Builder that represents SwitchCase<Predicate<T>, Consumer<T>> and SwitchCase<Predicate<T>, Function<T, U>>

Implementation

/* Switch.java
 * Copyright (C) 2018 Zymus
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
package net.zephyrion.util;

import java.util.LinkedList;
import java.util.function.Consumer;
import java.util.function.Predicate;

public final class Switch<T> {

    public static final class Builder<T> {

        private final LinkedList<SwitchCase<T>> cases = new LinkedList<>();
        private Consumer<T> defaultCase;

        public Builder() {
        }

        public Builder(final Switch<T> existingSwitch) {
            this.cases.addAll(existingSwitch.cases);
            this.defaultCase = existingSwitch.defaultCase;
        }

        public Builder<T> when(final Predicate<T> predicate, final Consumer<T> consumer) {
            cases.add(new SwitchCase<>(predicate, consumer, false));

            return this;
        }

        public Builder<T> breakWhen(final Predicate<T> predicate, final Consumer<T> consumer) {
            cases.add(new SwitchCase<>(predicate, consumer, true));

            return this;
        }

        public Builder<T> defaultCase(final Consumer<T> consumer) {
            this.defaultCase = consumer;
            return this;
        }

        public Switch<T> build() {
            return new Switch<>(cases, defaultCase);
        }
    }

    private static class SwitchCase<T> {

        private final Predicate<T> predicate;
        private final Consumer<T> consumer;
        private final boolean shouldBreak;

        private SwitchCase(final Predicate<T> predicate, final Consumer<T> consumer, final boolean shouldBreak) {
            this.predicate = predicate;
            this.consumer = consumer;
            this.shouldBreak = shouldBreak;
        }

        private boolean evaluate(final T value) {
            if (predicate.test(value)) {
                consumer.accept(value);
                return true;
            }
            return false;
        }
    }

    private LinkedList<SwitchCase<T>> cases;
    private Consumer<T> defaultCase;

    private Switch(LinkedList<SwitchCase<T>> cases, Consumer<T> defaultCase) {
        this.cases = cases;
        this.defaultCase = defaultCase;
    }

    public void evaluate(final T value) {
        boolean caseExecuted = false;

        for (final SwitchCase<T> switchCase : cases) {
            caseExecuted = switchCase.evaluate(value);
            if (switchCase.shouldBreak) {
                break;
            }
        }

        if (!caseExecuted) {
            if (defaultCase != null) {
                defaultCase.accept(value);
            }
        }
    }
}

Tests

Tests use String as type, even though String is switchable, but this allows non-constant value tests.

/* SwitchTests.java
 * Copyright (C) 2018 Zymus
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
package net.zephyrion.util;

import org.junit.Test;

public class SwitchTests {

    @Test
    public void testSwitches() {
        final String mod = "Mod";
        final String notMod = "Admin";

        final Switch<String> nonBreakingNameSwitch = createNonBreakingTestSwitch();
        final Switch<String> breakingSwitch = createBreakBeforeLastTestSwitch();

        nonBreakingNameSwitch.evaluate(mod);
        System.out.println();

        breakingSwitch.evaluate(mod);
        System.out.println();

        nonBreakingNameSwitch.evaluate(notMod);
        System.out.println();

        final Switch<String> extendedSwitch = createNonBreakingExtendedTestSwitch(nonBreakingNameSwitch);

        System.out.println("===== Extended Switch =====");
        extendedSwitch.evaluate(mod);
        System.out.println();

        extendedSwitch.evaluate(notMod);
        System.out.println();

        final Switch<String> extendedSwitchWithoutDefault = createNonBreakingExtendedTestSwitchWithoutDefault(nonBreakingNameSwitch);

        System.out.println("===== Extended Switch Without Default =====");
        extendedSwitchWithoutDefault.evaluate(mod);
        System.out.println();

        extendedSwitchWithoutDefault.evaluate(notMod);
        System.out.println();
    }

    private Switch<String> createNonBreakingTestSwitch() {
        final Switch.Builder<String> nameSwitchBuilder = new Switch.Builder<>();

        return nameSwitchBuilder.defaultCase(this::printUnhandledName)
                                .when(this::nameContainsUppercaseLetter, this::printUppercaseName)
                                .when(this::nameIsMod, this::printNameIsMod)
                                .when(this::nameContainsM, this::printNameContainsM)
                                .build();
    }

    private Switch<String> createBreakBeforeLastTestSwitch() {
        final Switch.Builder<String> nameSwitchBuilder = new Switch.Builder<>();

        return nameSwitchBuilder.defaultCase(this::printUnhandledName)
                                .when(this::nameContainsUppercaseLetter, this::printUppercaseName)
                                .breakWhen(this::nameIsMod, this::printNameIsMod)
                                .when(this::nameContainsM, this::printNameContainsM)
                                .build();
    }

    private Switch<String> createNonBreakingExtendedTestSwitch(final Switch<String> existingSwitch) {
        final Switch.Builder<String> nameSwitchBuilder = new Switch.Builder<>(existingSwitch);

        return nameSwitchBuilder.defaultCase(this::differentDefaultCase)
                                .when(this::nameContainsO, this::printNameContainsO)
                                .build();
    }

    private Switch<String> createNonBreakingExtendedTestSwitchWithoutDefault(final Switch<String> existingSwitch) {
        final Switch.Builder<String> nameSwitchBuilder = new Switch.Builder<>(existingSwitch);

        return nameSwitchBuilder.when(this::nameContainsO, this::printNameContainsO)
                                .build();
    }

    private void printUnhandledName(final String name) {
        System.out.println("Unhandled name: " + name);
    }

    // region Uppercase Case

    private boolean nameContainsUppercaseLetter(final String name) {
        return name.chars().anyMatch(Character::isUpperCase);
    }

    private void printUppercaseName(final String name) {
        System.out.println(name + " contains uppercase letter");
    }

    // endregion

    //region Mod case

    private boolean nameIsMod(final String name) {
        return name.equals("Mod");
    }

    private void printNameIsMod(final String name) {
        System.out.println("name is Mod");
    }

    // endregion

    //region M case

    private boolean nameContainsM(final String name) {
        return name.contains("M");
    }

    private void printNameContainsM(final String name) {
        System.out.println("name contains M");
    }

    // endregion

    // region o case

    private boolean nameContainsO(final String name) {
        return name.contains("o");
    }

    private void printNameContainsO(final String name) {
        System.out.println("name contains o");
    }

    private void differentDefaultCase(final String name) {
        System.out.println(name + " triggered the different default case");
    }

    // endregion
}

Output

Mod contains uppercase letter
name is Mod
name contains M

Mod contains uppercase letter
name is Mod

Admin contains uppercase letter
Unhandled name: Admin

===== Extended Switch =====
Mod contains uppercase letter
name is Mod
name contains M
name contains o

Admin contains uppercase letter
Admin triggered the different default case

===== Extended Switch Without Default =====
Mod contains uppercase letter
name is Mod
name contains M
name contains o

Admin contains uppercase letter
Unhandled name: Admin
\$\endgroup\$
3
\$\begingroup\$

Bugs

Unfortunately this doesn't work as expected

Assuming that breakWhen works like break in a regular switch, then I expect this:

    new Switch.Builder<>() //
            .breakWhen(x -> x.equals("A"), x -> System.out.println("Is A")) //
            .breakWhen(x -> x.equals("B"), x -> System.out.println("Is B")) //
            .build().evaluate("B");

to print "Is B", but it doesn't print anything.

Also I would expect this:

    new Switch.Builder<>() //
            .when(x -> x.equals("A"), x -> System.out.println("Is A")) //
            .when(x -> x.equals("B"), x -> System.out.println("Is B")) //
            .defaultCase(x -> System.out.println("DEFAULT"))
            .build().evaluate("A");

to just print "Is A", but it prints both "Is A" and "DEFAULT".

Code style

I like the code style (of the implementation). It is almost perfect in my opinion, there are only two mostly minor points:

LinkedList

The choice of a LinkedList for the list of cases is a good one, since you are only adding and iterating the list for which LinkedList is optimized.

However you should consider using just the List interface as the variable/field types everywhere else (e.g. use List everywhere you are currently using LinkedList, except for the instantiation new LinkedList<>()).

One could even consider using just Iterable for the cases field inside the actual Switch class. (EDIT: I've reconsidered. This isn't a good idea, because Iterable doesn't imply an order, which is important here.)

Access modifiers

(This is a topic I just learned about recently, so take this with a grain of salt. I'd be be grateful for some input from others.)

From a Java language point of view using the private access modifier on the fields of the inner classes are correct. However from the JVM view classes can't access private fields fields in their inner classes (and vice versa inner classes can't access private fields of their parents). The Java complier works around this by creating "invisible" accessor methods, which results in slightly larger class files and minute performance differences. This is not really a problem so you can ignore it, but you may consider removing the private access modifier on the fields to make them package-private (or explicit create package-private accessor methods yourself).

If you are using Eclipse you can see warnings about this by setting the settings option

Java > Complier > Errors/Warnings > Code style > Access to a non-accessible member of an enclosing type

to something else than "Ignore". Unfortunately I don't know the settings for any other IDEs or the Java compiler.

Usability

Personally I would make the API a bit more concise, possibly sacrificing reuse-ability and extendability (for which I don't really see a practical use case). Also since breaking is the more common use case, I'd make when break and have something like whenContinue for the non breaking case. Maybe something like this:

Switch
  .when(x -> x.equals("A"), x -> System.out.println("Is A")) //
  .when(x -> x.equals("B"), x -> System.out.println("Is B")) //
  .defaultCase(x -> System.out.println("DEFAULT"))
  .evaluate("A");

I may would even try and move the .evaluate() to the start in order to mirror the switch statement.

Tests

The tests are a big problem with this code. For one they didn't catch the bugs.

But more importantly they are also too complex and difficult to read. Try using simpler Predicates, like the examples I use up top to demonstrate the bug.

Also find a way to make them proper tests, that assert the results. Several testing libraries (such as Mockito) allow you to create mock objects on which you can assert that a specific method has been called.

Or just do something like this:

@Test
public void test1() {
    List<String> calls = new LinkedList<>();
    new Switch.Builder<>() //
            .breakWhen(x -> x.equals("A"), x -> calls.add("A")) //
            .breakWhen(x -> x.equals("B"), x -> calls.add("B")) //
            .build().evaluate("B");
    assertEquals(Arrays.asList("B"), calls);
}
\$\endgroup\$
  • \$\begingroup\$ Yeah, I didn't spend a whole lot of time on the tests. They were primarily used as a means of getting the intent across, and experimenting with the syntax. Thanks for the feedback! \$\endgroup\$ – Zymus Oct 31 '18 at 17:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.