Last active
August 29, 2015 14:14
-
-
Save jdigger/2379b590c3f5fb6ed99c to your computer and use it in GitHub Desktop.
Union Class In Java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import javax.annotation.Nonnull; | |
/** | |
* Indicates there was an error. | |
*/ | |
public interface Failure { | |
/** | |
* Returns the error. | |
*/ | |
@Nonnull | |
String errorMessage(); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import javax.annotation.Nullable; | |
/** | |
* Indicates that the call was successful. | |
*/ | |
public interface Success<T> { | |
/** | |
* The value of the invocation. | |
*/ | |
@Nullable | |
T get(); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import javax.annotation.Nonnull; | |
import javax.annotation.Nullable; | |
import java.util.function.Consumer; | |
import java.util.function.Function; | |
/** | |
* An example of creating a "closed union class" in Java. | |
* <p> | |
* This technique is useful for any case where you need to use a constrained possible set of types, | |
* but they don't all inherit from the same hierarchy, so "normal" polymorphism does not apply. | |
* <p> | |
* It's a very typical thing to do in environments with a much more robust type system, but can be | |
* "forced" even in Java, as seen below. See the "main" method for an example of usage. | |
* <p> | |
* Inspired by http://www.slideshare.net/ScottWlaschin/fp-patterns-buildstufflt | |
*/ | |
public class SuccessOrFailure<T> { | |
private final Object val; // the types don't share a hierarchy, so this is the best we can do... | |
/** | |
* We are guaranteeing that this can only be created with a Success or a Failure through the factory methods. | |
*/ | |
// @SuppressWarnings("ConstantConditions") | |
private SuccessOrFailure(@Nonnull Object val) { | |
this.val = val; | |
} | |
/** | |
* Returns a SuccessOrFailure that contains a Success. | |
*/ | |
@Nonnull | |
public static <T> SuccessOrFailure<T> success(@Nullable T val) { | |
// taking advantage of Java 8 lambda support; otherwise can be done | |
// more clumsily and slowly with an inner class | |
return new SuccessOrFailure<>(((Success)() -> val)); | |
} | |
/** | |
* Returns a SuccessOrFailure that contains a Failure with the given message. | |
*/ | |
@Nonnull | |
public static <T> SuccessOrFailure<T> failure(@Nonnull String message) { | |
return new SuccessOrFailure<>((Failure)() -> message); | |
} | |
/** | |
* Returns a SuccessOrFailure that contains a Failure with the given Throwable. | |
*/ | |
@Nonnull | |
public static <T> SuccessOrFailure<T> failure(@Nonnull Throwable throwable) { | |
return new SuccessOrFailure<>(new ThrowableFailure(throwable)); | |
} | |
/** | |
* If this contains a Success, return it. otherwise returns a null | |
*/ | |
@Nullable | |
@SuppressWarnings("unchecked") | |
public Success<T> success() { | |
return (val instanceof Success) ? (Success<T>)val : null; | |
} | |
/** | |
* If this contains a Failure, return it. otherwise returns a null | |
*/ | |
@Nullable | |
@SuppressWarnings("unchecked") | |
public Failure failure() { | |
return (val instanceof Failure) ? (Failure)val : null; | |
} | |
/** | |
* Convert a "normal" function to return a SuccessOrFailure. | |
* The only thing that will generate a Failure is if the function throws an exception. | |
* | |
* @param func the function to translate to return SuccessOrFailure | |
* @param <P> the type of the parameter to the function | |
* @param <R> the type of the return value | |
*/ | |
@Nonnull | |
public static <P, R> Function<P, SuccessOrFailure<R>> bind(Function<P, R> func) { | |
// prior to Java 8 this can be done with a Callable or the like, but... ugh | |
return (x) -> { | |
try { | |
return SuccessOrFailure.success(func.apply(x)); | |
} | |
catch (Throwable exp) { | |
return SuccessOrFailure.failure(exp); | |
} | |
}; | |
} | |
/** | |
* Merge a pair of "normal" Consumers into a single Consumer that can handle a SuccessOrFailure. | |
* | |
* @param successConsumer the Consumer to call if a Success is passed in | |
* @param failureConsumer the Consumer to call if a Failure is passed in | |
* @param <P> the type of the parameter for Success | |
*/ | |
@Nonnull | |
// @SuppressWarnings("ConstantConditions") | |
public static <P> Consumer<SuccessOrFailure<P>> merge(Consumer<P> successConsumer, Consumer<Failure> failureConsumer) { | |
return x -> { | |
if (x.failure() == null) | |
successConsumer.accept(x.success().get()); | |
else | |
failureConsumer.accept(x.failure()); | |
}; | |
} | |
/** | |
* Convert a "normal" function to accept a SuccessOrFailure and return a SuccessOrFailure. | |
* The only thing that will generate a Failure is if the function throws an exception. | |
* If a Failure is passed in, it is simply returned. | |
* | |
* @param func the function to translate to return SuccessOrFailure | |
* @param <P> the type of the parameter to the function | |
* @param <R> the type of the return value | |
*/ | |
@Nonnull | |
@SuppressWarnings({"ConstantConditions", "unchecked"}) | |
public static <P, R> Function<SuccessOrFailure<P>, SuccessOrFailure<R>> map(Function<P, R> func) { | |
return (x) -> { | |
if (x.failure() != null) return (SuccessOrFailure<R>)x; | |
try { | |
return SuccessOrFailure.success(func.apply(x.success().get())); | |
} | |
catch (Throwable exp) { | |
return SuccessOrFailure.failure(exp); | |
} | |
}; | |
} | |
/** | |
* Maps a pair of "normal" Consumers into a Function that can handle a SuccessOrFailure. | |
* Depending on if Success or Failure is passed in, the appropriate Consumer is called and the | |
* same SuccessOrFailure is returned to be propagated along the chain. | |
* | |
* @param successConsumer the Consumer to call if a Success is passed in | |
* @param failureConsumer the Consumer to call if a Failure is passed in | |
* @param <P> the type of the parameter for Success | |
*/ | |
@Nonnull | |
@SuppressWarnings({"ConstantConditions", "unchecked"}) | |
public static <P> Function<SuccessOrFailure<P>, SuccessOrFailure<P>> map(Consumer<P> successConsumer, Consumer<Failure> failureConsumer) { | |
return (x) -> { | |
Failure failure = x.failure(); | |
if (failure != null) | |
failureConsumer.accept(failure); | |
else | |
successConsumer.accept(x.success().get()); | |
return x; | |
}; | |
} | |
@Override | |
public String toString() { | |
return (val instanceof Success) ? "Success(" + ((Success)val).get() + ")" : "Failure(" + ((Failure)val).errorMessage() + ")"; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import javax.annotation.Nonnull; | |
public class ThrowableFailure implements Failure { | |
private Throwable throwable; | |
public ThrowableFailure(@Nonnull Throwable throwable) { | |
this.throwable = throwable; | |
} | |
@Nonnull | |
@Override | |
public String errorMessage() { | |
return this.throwable.toString(); | |
} | |
@SuppressWarnings("UnusedDeclaration") | |
public Throwable getThrowable() { | |
return throwable; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import javax.annotation.Nonnull; | |
import javax.annotation.Nullable; | |
import java.util.Date; | |
import java.util.Random; | |
import java.util.function.Consumer; | |
import java.util.function.Function; | |
import java.util.stream.LongStream; | |
public class TwoTrackTester { | |
public static void main(String[] args) { | |
// | |
// Simple example of making a call that can either succeed or fail | |
// | |
SuccessOrFailure<Date> retVal; | |
// do something successfully | |
retVal = doSomethingWithFailure(true); | |
if (retVal.success() != null) { | |
System.out.println("Success: " + retVal.success().get()); | |
} | |
// do something but fail | |
retVal = doSomethingWithFailure(false); | |
if (retVal.success() == null) { | |
System.out.println("Failed: " + retVal.failure().errorMessage()); | |
} | |
// | |
// This gets even more useful when combined with functional programming... | |
// Using Java 8 lambdas, because this kind of thing pre-Java8 is a pain. | |
// | |
// Create a function that gets the current Date and converts it to a String. | |
// The "bind" converts a "normal" function to return a SuccessOrFailure. | |
// The "map" converts a "normal" function to accept a SuccessOrFailure and | |
// return SuccessOrFailure. | |
Function<Boolean, SuccessOrFailure<String>> funcDateAsString = | |
SuccessOrFailure.bind(TwoTrackTester::getDate). | |
andThen(SuccessOrFailure.map(Date::toString)); | |
Consumer<String> successPrinter = x -> System.out.println("Good: " + x); | |
Consumer<Failure> failurePrinter = x -> System.out.println("Boom: " + x.errorMessage()); | |
// Add Consumers to the end of the function chain and call by applying some arguments | |
funcDateAsString. | |
andThen(SuccessOrFailure.map(successPrinter, failurePrinter)).apply(true); | |
funcDateAsString. | |
andThen(SuccessOrFailure.map(successPrinter, failurePrinter)).apply(false); | |
// | |
// This is where it really comes into its own: | |
// | |
// When the same techniques are applied to a Stream, it allows data to cleanly move from one end of | |
// the Stream to the other. It's trivial to add validators, enrichers, transformers, wiretaps, etc. | |
// | |
// In contrast to an EIP framework (such as Apache Camel or Spring Integration), there's fewer moving parts | |
// (e.g., Messages, Exchanges, etc.) so it's both more efficient and easier to reason about at the cost of | |
// not being quite as powerful. | |
// | |
// In contrast to reactive programming, this provides a very serial path, which is much easier to | |
// reason about (and the call stack is actually useful). | |
// | |
// Of course it also means that any concurrent execution will consume that thread/process until the | |
// Stream is finished because it isn't taking advantage of an event reactor/dispatcher. | |
// | |
// Use whatever tool works best for the problem at hand. | |
// | |
LongStream longStream = new Random().longs(); | |
// only even longs are allowed through (for whatever arbitrary reason :-) | |
Function<Long, SuccessOrFailure<Long>> validator = | |
x -> (x % 2 == 0) ? SuccessOrFailure.success(x) : SuccessOrFailure.failure("Could not get an odd value"); | |
longStream.limit(10).parallel(). | |
mapToObj(validator::apply). // validate the data | |
map(SuccessOrFailure.map((Function<Long, Date>)Date::new)). // transform long to Date | |
map(SuccessOrFailure.map(Date::toString)). // transform Date to String | |
peek(x -> System.out.println("Seeing " + x + " pass by")). // put a "wiretap" in to see traffic through the Stream | |
forEach(SuccessOrFailure.merge(successPrinter, failurePrinter)); // print the results | |
} | |
@Nonnull | |
static Date getDate(boolean shouldSucceed) { | |
if (shouldSucceed) return new Date(); | |
else throw new IllegalArgumentException("Could not get Date"); | |
} | |
@Nonnull | |
static SuccessOrFailure<Date> doSomethingWithFailure(boolean shouldSucceed) { | |
return (shouldSucceed) ? SuccessOrFailure.success(new Date()) : SuccessOrFailure.failure("Got a failure"); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment