docs: update promise

This commit is contained in:
Ilkka Seppälä committed 2024-05-17 17:33:56 +03:00
1 parent 279d227fd4
commit 6e22be370f
5 files changed
+94 -243

No files matched your search

+87 -234
View File
@@ -3,34 +3,29 @@ title: Promise
category: Concurrency category: Concurrency
language: en language: en
tag: tag:
- Reactive - Asynchronous
- Decoupling
- Messaging
- Synchronization
- Thread management
--- ---
## Also known as ## Also known as
CompletableFuture * Deferred
* Future
## Intent ## Intent
A Promise represents a proxy for a value not necessarily known when the promise is created. It The Promise design pattern is used to handle asynchronous operations by providing a placeholder for a result that is initially unknown but will be resolved in the future.
allows you to associate dependent promises to an asynchronous action's eventual success value or
failure reason. Promises are a way to write async code that still appears as though it is executing
in a synchronous way.
## Explanation ## Explanation
The Promise object is used for asynchronous computations. A Promise represents an operation that Real-world example
hasn't completed yet, but is expected in the future.
Promises provide a few advantages over callback objects: > In an online pizza ordering system, when a customer places an order, the system immediately acknowledges the order and provides a tracking number (the promise). The pizza preparation and delivery process happens asynchronously in the background. The customer can check the status of their order at any time using the tracking number. Once the pizza is prepared and out for delivery, the customer receives a notification (promise resolved) about the delivery status. If there are any issues, such as an unavailable ingredient or delivery delay, the customer is notified about the error (promise rejected).
* Functional composition and error handling. >
* Prevents callback hell and provides callback aggregation. > This analogy illustrates how the Promise design pattern manages asynchronous tasks, decoupling the initial request from the eventual outcome, and handling both results and errors efficiently.
Real world example
> We are developing a software solution that downloads files and calculates the number of lines and
> character frequencies in those files. Promise is an ideal solution to make the code concise and
> easy to understand.
In plain words In plain words
@@ -38,274 +33,132 @@ In plain words
Wikipedia says Wikipedia says
> In computer science, future, promise, delay, and deferred refer to constructs used for > In computer science, future, promise, delay, and deferred refer to constructs used for synchronizing program execution in some concurrent programming languages. They describe an object that acts as a proxy for a result that is initially unknown, usually because the computation of its value is not yet complete.
> synchronizing program execution in some concurrent programming languages. They describe an object
> that acts as a proxy for a result that is initially unknown, usually because the computation of
> its value is not yet complete.
**Programmatic Example** **Programmatic Example**
In the example a file is downloaded and its line count is calculated. The calculated line count is The Promise design pattern is a software design pattern that's often used in concurrent programming to handle asynchronous operations. It represents a proxy for a value not necessarily known when the promise is created. It allows you to associate handlers with an asynchronous action's eventual success value or failure reason.
then consumed and printed on console.
Let's first introduce a support class we need for implementation. Here's `PromiseSupport`. In the provided code, the Promise design pattern is used to handle various asynchronous operations such as downloading a file, counting lines in a file, and calculating the character frequency in a file.
```java ```java
class PromiseSupport<T> implements Future<T> { @Slf4j
public class App {
private static final Logger LOGGER = LoggerFactory.getLogger(PromiseSupport.class); private static final String DEFAULT_URL =
"https://raw.githubusercontent.com/iluwatar/java-design-patterns/master/promise/README.md";
private final ExecutorService executor;
private static final int RUNNING = 1; private App() {
private static final int FAILED = 2; // Create a thread pool with 2 threads
private static final int COMPLETED = 3; executor = Executors.newFixedThreadPool(2);
private final Object lock;
private volatile int state = RUNNING;
private T value;
private Exception exception;
PromiseSupport() {
this.lock = new Object();
} }
void fulfill(T value) { public static void main(String[] args) {
this.value = value; var app = new App();
this.state = COMPLETED; app.promiseUsage();
synchronized (lock) {
lock.notifyAll();
}
} }
void fulfillExceptionally(Exception exception) { private void promiseUsage() {
this.exception = exception; calculateLineCount();
this.state = FAILED; calculateLowestFrequencyChar();
synchronized (lock) {
lock.notifyAll();
}
} }
@Override private void calculateLowestFrequencyChar() {
public boolean cancel(boolean mayInterruptIfRunning) { // Create a promise to calculate the lowest frequency character
return false; lowestFrequencyChar().thenAccept(
} charFrequency -> {
LOGGER.info("Char with lowest frequency is: {}", charFrequency);
@Override
public boolean isCancelled() {
return false;
}
@Override
public boolean isDone() {
return state > RUNNING;
}
@Override
public T get() throws InterruptedException, ExecutionException {
synchronized (lock) {
while (state == RUNNING) {
lock.wait();
}
}
if (state == COMPLETED) {
return value;
}
throw new ExecutionException(exception);
}
@Override
public T get(long timeout, TimeUnit unit) throws ExecutionException {
synchronized (lock) {
while (state == RUNNING) {
try {
lock.wait(unit.toMillis(timeout));
} catch (InterruptedException e) {
LOGGER.warn("Interrupted!", e);
Thread.currentThread().interrupt();
} }
} );
}
if (state == COMPLETED) {
return value;
}
throw new ExecutionException(exception);
}
}
```
With `PromiseSupport` in place we can implement the actual `Promise`.
```java
public class Promise<T> extends PromiseSupport<T> {
private Runnable fulfillmentAction;
private Consumer<? super Throwable> exceptionHandler;
public Promise() {
} }
@Override private void calculateLineCount() {
public void fulfill(T value) { // Create a promise to calculate the line count
super.fulfill(value); countLines().thenAccept(
postFulfillment(); count -> {
LOGGER.info("Line count is: {}", count);
}
);
} }
@Override private Promise<Character> lowestFrequencyChar() {
public void fulfillExceptionally(Exception exception) { // Create a promise to calculate the character frequency and then find the lowest frequency character
super.fulfillExceptionally(exception); return characterFrequency().thenApply(Utility::lowestFrequencyChar);
handleException(exception);
postFulfillment();
} }
private void handleException(Exception exception) { private Promise<Map<Character, Long>> characterFrequency() {
if (exceptionHandler == null) { // Create a promise to download a file and then calculate the character frequency
return; return download(DEFAULT_URL).thenApply(Utility::characterFrequency);
}
exceptionHandler.accept(exception);
} }
private void postFulfillment() {
if (fulfillmentAction == null) {
return;
}
fulfillmentAction.run();
}
public Promise<T> fulfillInAsync(final Callable<T> task, Executor executor) {
executor.execute(() -> {
try {
fulfill(task.call());
} catch (Exception ex) {
fulfillExceptionally(ex);
}
});
return this;
}
public Promise<Void> thenAccept(Consumer<? super T> action) {
var dest = new Promise<Void>();
fulfillmentAction = new ConsumeAction(this, dest, action);
return dest;
}
public Promise<T> onError(Consumer<? super Throwable> exceptionHandler) {
this.exceptionHandler = exceptionHandler;
return this;
}
public <V> Promise<V> thenApply(Function<? super T, V> func) {
Promise<V> dest = new Promise<>();
fulfillmentAction = new TransformAction<>(this, dest, func);
return dest;
}
private class ConsumeAction implements Runnable {
private final Promise<T> src;
private final Promise<Void> dest;
private final Consumer<? super T> action;
private ConsumeAction(Promise<T> src, Promise<Void> dest, Consumer<? super T> action) {
this.src = src;
this.dest = dest;
this.action = action;
}
@Override
public void run() {
try {
action.accept(src.get());
dest.fulfill(null);
} catch (Throwable throwable) {
dest.fulfillExceptionally((Exception) throwable.getCause());
}
}
}
private class TransformAction<V> implements Runnable {
private final Promise<T> src;
private final Promise<V> dest;
private final Function<? super T, V> func;
private TransformAction(Promise<T> src, Promise<V> dest, Function<? super T, V> func) {
this.src = src;
this.dest = dest;
this.func = func;
}
@Override
public void run() {
try {
dest.fulfill(func.apply(src.get()));
} catch (Throwable throwable) {
dest.fulfillExceptionally((Exception) throwable.getCause());
}
}
}
}
```
Now we can show the full example in action. Here's how to download and count the number of lines in
a file using `Promise`.
```java
countLines().thenAccept(
count -> {
LOGGER.info("Line count is: {}", count);
taskCompleted();
}
);
private Promise<Integer> countLines() { private Promise<Integer> countLines() {
// Create a promise to download a file and then count the lines
return download(DEFAULT_URL).thenApply(Utility::countLines); return download(DEFAULT_URL).thenApply(Utility::countLines);
} }
private Promise<String> download(String urlString) { private Promise<String> download(String urlString) {
// Create a promise to download a file
return new Promise<String>() return new Promise<String>()
.fulfillInAsync( .fulfillInAsync(
() -> Utility.downloadFile(urlString), executor) () -> Utility.downloadFile(urlString), executor)
.onError( .onError(
throwable -> { throwable -> {
throwable.printStackTrace(); LOGGER.error("An error occurred: ", throwable);
taskCompleted();
} }
); );
} }
}
``` ```
In this code, the `Promise` class is used to create promises for various operations. The `thenApply` method is used to chain promises, meaning that the result of one promise is used as the input for the next promise. The `thenAccept` method is used to handle the result of a promise. The `fulfillInAsync` method is used to fulfill a promise asynchronously, and the `onError` method is used to handle any errors that occur while fulfilling the promise.
## Class diagram ## Class diagram
![alt text](./etc/promise.png "Promise") ![Promise](./etc/promise.png "Promise")
## Applicability ## Applicability
Promise pattern is applicable in concurrent programming when some work needs to be done * When you need to perform asynchronous tasks and handle their results or errors at a later point.
asynchronously and: * In scenarios where tasks can be executed in parallel and their outcomes need to be handled once they are completed.
* Suitable for improving the readability and maintainability of asynchronous code.
* Code maintainability and readability suffers due to callback hell.
* You need to compose promises and need better error handling for asynchronous tasks.
* You want to use functional style of programming.
## Real world examples
* [java.util.concurrent.CompletableFuture](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/CompletableFuture.html)
* [Guava ListenableFuture](https://github.com/google/guava/wiki/ListenableFutureExplained)
## Related Patterns
* [Async Method Invocation](https://java-design-patterns.com/patterns/async-method-invocation/)
* [Callback](https://java-design-patterns.com/patterns/callback/)
## Tutorials ## Tutorials
* [Guide To CompletableFuture](https://www.baeldung.com/java-completablefuture) * [Guide To CompletableFuture](https://www.baeldung.com/java-completablefuture)
## Known Uses
* Java's CompletableFuture and Future classes.
* JavaScript’s Promise object for managing asynchronous operations.
* Many asynchronous frameworks and libraries such as RxJava and Vert.x.
* [Guava ListenableFuture](https://github.com/google/guava/wiki/ListenableFutureExplained)
## Consequences
Benefits:
* Improved Readability: Simplifies complex asynchronous code, making it easier to understand and maintain.
* Decoupling: Decouples the code that initiates the asynchronous operation from the code that processes the result.
* Error Handling: Provides a unified way to handle both results and errors from asynchronous operations.
Trade-offs:
* Complexity: Can add complexity to the codebase if overused or misused.
* Debugging: Asynchronous code can be harder to debug compared to synchronous code due to the non-linear flow of execution.
## Related Patterns
* [Observer](https://java-design-patterns.com/patterns/observer/): Promises can be used in conjunction with the Observer pattern to notify subscribers about the completion of asynchronous operations.
* [Callback](https://java-design-patterns.com/patterns/callback/): Promises often replace callback mechanisms by providing a more structured and readable way to handle asynchronous results.
* [Async Method Invocation](https://java-design-patterns.com/patterns/async-method-invocation/): Promises are often used to handle the results of asynchronous method invocations, allowing for non-blocking execution and result handling.
## Credits ## Credits
* [You are missing the point to Promises](https://gist.github.com/domenic/3889970) * [You are missing the point to Promises](https://gist.github.com/domenic/3889970)
* [Functional style callbacks using CompletableFuture](https://www.infoq.com/articles/Functional-Style-Callbacks-Using-CompletableFuture) * [Functional style callbacks using CompletableFuture](https://www.infoq.com/articles/Functional-Style-Callbacks-Using-CompletableFuture)
* [Java 8 in Action: Lambdas, Streams, and functional-style programming](https://www.amazon.com/gp/product/1617291994/ref=as_li_qf_asin_il_tl?ie=UTF8&tag=javadesignpat-20&creative=9325&linkCode=as2&creativeASIN=1617291994&linkId=995af46887bb7b65e6c788a23eaf7146) * [Java 8 in Action: Lambdas, Streams, and functional-style programming](https://www.amazon.com/gp/product/1617291994/ref=as_li_qf_asin_il_tl?ie=UTF8&tag=javadesignpat-20&creative=9325&linkCode=as2&creativeASIN=1617291994&linkId=995af46887bb7b65e6c788a23eaf7146)
* [Modern Java in Action: Lambdas, streams, functional and reactive programming](https://www.amazon.com/gp/product/1617293563/ref=as_li_qf_asin_il_tl?ie=UTF8&tag=javadesignpat-20&creative=9325&linkCode=as2&creativeASIN=1617293563&linkId=f70fe0d3e1efaff89554a6479c53759c) * [Modern Java in Action: Lambdas, streams, functional and reactive programming](https://www.amazon.com/gp/product/1617293563/ref=as_li_qf_asin_il_tl?ie=UTF8&tag=javadesignpat-20&creative=9325&linkCode=as2&creativeASIN=1617293563&linkId=f70fe0d3e1efaff89554a6479c53759c)
* [Java Concurrency in Practice](https://amzn.to/4aRMruW)
* [Effective Java](https://amzn.to/4cGk2Jz)
* [Java 8 in Action: Lambdas, Streams, and functional-style programming](https://amzn.to/3QCmGXs)
@@ -78,7 +78,6 @@ public class App {
* *
* @param args arguments * @param args arguments
* @throws InterruptedException if main thread is interrupted. * @throws InterruptedException if main thread is interrupted.
* @throws ExecutionException if an execution error occurs.
*/ */
public static void main(String[] args) throws InterruptedException { public static void main(String[] args) throws InterruptedException {
var app = new App(); var app = new App();
@@ -123,7 +122,7 @@ public class App {
/* /*
* Calculate the character frequency of a file and when that promise is fulfilled, * Calculate the character frequency of a file and when that promise is fulfilled,
* then promise to apply function to calculate lowest character frequency. * then promise to apply function to calculate the lowest character frequency.
*/ */
private Promise<Character> lowestFrequencyChar() { private Promise<Character> lowestFrequencyChar() {
return characterFrequency().thenApply(Utility::lowestFrequencyChar); return characterFrequency().thenApply(Utility::lowestFrequencyChar);
@@ -155,7 +154,7 @@ public class App {
() -> Utility.downloadFile(urlString), executor) () -> Utility.downloadFile(urlString), executor)
.onError( .onError(
throwable -> { throwable -> {
throwable.printStackTrace(); LOGGER.error("An error occurred: ", throwable);
taskCompleted(); taskCompleted();
} }
); );
@@ -45,7 +45,7 @@ public class Promise<T> extends PromiseSupport<T> {
private Consumer<? super Throwable> exceptionHandler; private Consumer<? super Throwable> exceptionHandler;
/** /**
* Creates a promise that will be fulfilled in future. * Creates a promise that will be fulfilled in the future.
*/ */
public Promise() { public Promise() {
// Empty constructor // Empty constructor
@@ -58,7 +58,7 @@ public class Utility {
.mapToObj(x -> (char) x) .mapToObj(x -> (char) x)
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting())); .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
} catch (IOException ex) { } catch (IOException ex) {
ex.printStackTrace(); LOGGER.error("An error occurred: ", ex);
} }
return Collections.emptyMap(); return Collections.emptyMap();
} }
@@ -86,7 +86,7 @@ public class Utility {
try (var bufferedReader = new BufferedReader(new FileReader(fileLocation))) { try (var bufferedReader = new BufferedReader(new FileReader(fileLocation))) {
return (int) bufferedReader.lines().count(); return (int) bufferedReader.lines().count();
} catch (IOException ex) { } catch (IOException ex) {
ex.printStackTrace(); LOGGER.error("An error occurred: ", ex);
} }
return 0; return 0;
} }
@@ -24,11 +24,10 @@
*/ */
package com.iluwatar.promise; package com.iluwatar.promise;
import java.util.concurrent.ExecutionException;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import org.junit.jupiter.api.Test;
/** /**
* Application test. * Application test.
*/ */