docs: update promise

This commit is contained in:
Ilkka Seppälä
2024-05-17 17:33:56 +03:00
parent 279d227fd4
commit 6e22be370f
5 changed files with 94 additions and 243 deletions
+87 -234
View File
@@ -3,34 +3,29 @@ title: Promise
category: Concurrency
language: en
tag:
- Reactive
- Asynchronous
- Decoupling
- Messaging
- Synchronization
- Thread management
---
## Also known as
CompletableFuture
* Deferred
* Future
## Intent
A Promise represents a proxy for a value not necessarily known when the promise is created. It
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.
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.
## Explanation
The Promise object is used for asynchronous computations. A Promise represents an operation that
hasn't completed yet, but is expected in the future.
Real-world example
Promises provide a few advantages over callback objects:
* Functional composition and error handling.
* Prevents callback hell and provides callback aggregation.
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 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).
>
> 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.
In plain words
@@ -38,274 +33,132 @@ In plain words
Wikipedia says
> 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.
> 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.
**Programmatic Example**
In the example a file is downloaded and its line count is calculated. The calculated line count is
then consumed and printed on console.
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.
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
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 static final int FAILED = 2;
private static final int COMPLETED = 3;
private final Object lock;
private volatile int state = RUNNING;
private T value;
private Exception exception;
PromiseSupport() {
this.lock = new Object();
private App() {
// Create a thread pool with 2 threads
executor = Executors.newFixedThreadPool(2);
}
void fulfill(T value) {
this.value = value;
this.state = COMPLETED;
synchronized (lock) {
lock.notifyAll();
}
public static void main(String[] args) {
var app = new App();
app.promiseUsage();
}
void fulfillExceptionally(Exception exception) {
this.exception = exception;
this.state = FAILED;
synchronized (lock) {
lock.notifyAll();
}
private void promiseUsage() {
calculateLineCount();
calculateLowestFrequencyChar();
}
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
return false;
}
@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();
private void calculateLowestFrequencyChar() {
// Create a promise to calculate the lowest frequency character
lowestFrequencyChar().thenAccept(
charFrequency -> {
LOGGER.info("Char with lowest frequency is: {}", charFrequency);
}
}
}
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
public void fulfill(T value) {
super.fulfill(value);
postFulfillment();
private void calculateLineCount() {
// Create a promise to calculate the line count
countLines().thenAccept(
count -> {
LOGGER.info("Line count is: {}", count);
}
);
}
@Override
public void fulfillExceptionally(Exception exception) {
super.fulfillExceptionally(exception);
handleException(exception);
postFulfillment();
private Promise<Character> lowestFrequencyChar() {
// Create a promise to calculate the character frequency and then find the lowest frequency character
return characterFrequency().thenApply(Utility::lowestFrequencyChar);
}
private void handleException(Exception exception) {
if (exceptionHandler == null) {
return;
}
exceptionHandler.accept(exception);
private Promise<Map<Character, Long>> characterFrequency() {
// Create a promise to download a file and then calculate the character frequency
return download(DEFAULT_URL).thenApply(Utility::characterFrequency);
}
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() {
// Create a promise to download a file and then count the lines
return download(DEFAULT_URL).thenApply(Utility::countLines);
}
private Promise<String> download(String urlString) {
// Create a promise to download a file
return new Promise<String>()
.fulfillInAsync(
() -> Utility.downloadFile(urlString), executor)
.onError(
throwable -> {
throwable.printStackTrace();
taskCompleted();
LOGGER.error("An error occurred: ", throwable);
}
);
}
}
```
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
![alt text](./etc/promise.png "Promise")
![Promise](./etc/promise.png "Promise")
## Applicability
Promise pattern is applicable in concurrent programming when some work needs to be done
asynchronously and:
* 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/)
* When you need to perform asynchronous tasks and handle their results or errors at a later point.
* 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.
## Tutorials
* [Guide To CompletableFuture](https://www.baeldung.com/java-completablefuture)
## Known Uses
* Java's CompletableFuture and Future classes.
* JavaScripts 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
* [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)
* [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)
* [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
* @throws InterruptedException if main thread is interrupted.
* @throws ExecutionException if an execution error occurs.
*/
public static void main(String[] args) throws InterruptedException {
var app = new App();
@@ -123,7 +122,7 @@ public class App {
/*
* 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() {
return characterFrequency().thenApply(Utility::lowestFrequencyChar);
@@ -155,7 +154,7 @@ public class App {
() -> Utility.downloadFile(urlString), executor)
.onError(
throwable -> {
throwable.printStackTrace();
LOGGER.error("An error occurred: ", throwable);
taskCompleted();
}
);
@@ -45,7 +45,7 @@ public class Promise<T> extends PromiseSupport<T> {
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() {
// Empty constructor
@@ -58,7 +58,7 @@ public class Utility {
.mapToObj(x -> (char) x)
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
} catch (IOException ex) {
ex.printStackTrace();
LOGGER.error("An error occurred: ", ex);
}
return Collections.emptyMap();
}
@@ -86,7 +86,7 @@ public class Utility {
try (var bufferedReader = new BufferedReader(new FileReader(fileLocation))) {
return (int) bufferedReader.lines().count();
} catch (IOException ex) {
ex.printStackTrace();
LOGGER.error("An error occurred: ", ex);
}
return 0;
}
@@ -24,11 +24,10 @@
*/
package com.iluwatar.promise;
import java.util.concurrent.ExecutionException;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import org.junit.jupiter.api.Test;
/**
* Application test.
*/