From 6e22be370fce0478dc1a1e1c9c13586eb753afd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ilkka=20Sepp=C3=A4l=C3=A4?= Date: Fri, 17 May 2024 17:33:56 +0300 Subject: [PATCH] docs: update promise --- promise/README.md | 321 +++++------------- .../main/java/com/iluwatar/promise/App.java | 5 +- .../java/com/iluwatar/promise/Promise.java | 2 +- .../java/com/iluwatar/promise/Utility.java | 4 +- .../java/com/iluwatar/promise/AppTest.java | 5 +- 5 files changed, 94 insertions(+), 243 deletions(-) diff --git a/promise/README.md b/promise/README.md index 82442dc6c..5b8f149a2 100644 --- a/promise/README.md +++ b/promise/README.md @@ -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 implements Future { +@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 extends PromiseSupport { - - private Runnable fulfillmentAction; - private Consumer 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 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> 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 fulfillInAsync(final Callable task, Executor executor) { - executor.execute(() -> { - try { - fulfill(task.call()); - } catch (Exception ex) { - fulfillExceptionally(ex); - } - }); - return this; - } - - public Promise thenAccept(Consumer action) { - var dest = new Promise(); - fulfillmentAction = new ConsumeAction(this, dest, action); - return dest; - } - - public Promise onError(Consumer exceptionHandler) { - this.exceptionHandler = exceptionHandler; - return this; - } - - public Promise thenApply(Function func) { - Promise dest = new Promise<>(); - fulfillmentAction = new TransformAction<>(this, dest, func); - return dest; - } - - private class ConsumeAction implements Runnable { - - private final Promise src; - private final Promise dest; - private final Consumer action; - - private ConsumeAction(Promise src, Promise dest, Consumer 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 implements Runnable { - - private final Promise src; - private final Promise dest; - private final Function func; - - private TransformAction(Promise src, Promise dest, Function 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 countLines() { + // Create a promise to download a file and then count the lines return download(DEFAULT_URL).thenApply(Utility::countLines); } private Promise download(String urlString) { + // Create a promise to download a file return new Promise() .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. +* 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 * [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) diff --git a/promise/src/main/java/com/iluwatar/promise/App.java b/promise/src/main/java/com/iluwatar/promise/App.java index 3ac7b8363..1f66ae0da 100644 --- a/promise/src/main/java/com/iluwatar/promise/App.java +++ b/promise/src/main/java/com/iluwatar/promise/App.java @@ -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 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(); } ); diff --git a/promise/src/main/java/com/iluwatar/promise/Promise.java b/promise/src/main/java/com/iluwatar/promise/Promise.java index 27ce5899f..595814783 100644 --- a/promise/src/main/java/com/iluwatar/promise/Promise.java +++ b/promise/src/main/java/com/iluwatar/promise/Promise.java @@ -45,7 +45,7 @@ public class Promise extends PromiseSupport { private Consumer exceptionHandler; /** - * Creates a promise that will be fulfilled in future. + * Creates a promise that will be fulfilled in the future. */ public Promise() { // Empty constructor diff --git a/promise/src/main/java/com/iluwatar/promise/Utility.java b/promise/src/main/java/com/iluwatar/promise/Utility.java index 0976c8c75..e3a2e0ad8 100644 --- a/promise/src/main/java/com/iluwatar/promise/Utility.java +++ b/promise/src/main/java/com/iluwatar/promise/Utility.java @@ -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; } diff --git a/promise/src/test/java/com/iluwatar/promise/AppTest.java b/promise/src/test/java/com/iluwatar/promise/AppTest.java index a3eb8718f..502968466 100644 --- a/promise/src/test/java/com/iluwatar/promise/AppTest.java +++ b/promise/src/test/java/com/iluwatar/promise/AppTest.java @@ -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. */