Compare commits

...

15 Commits

Author SHA1 Message Date
a8355988b2 #590 add explanation for caching pattern 2021-03-28 12:49:11 +03:00
6d7084f18d remove labelling trigger (#1692) 2021-03-28 10:15:42 +03:00
74f5cfa670 docs: add jinishavora as a contributor (#1688)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
Co-authored-by: Subhrodip Mohanta <subhrodipmohanta@gmail.com>
2021-03-23 14:12:32 +05:30
b525d871b4 docs: Fix mvvm readme (#1689) 2021-03-23 11:29:20 +05:30
c413e0902e docs: #590 add explanation for bytecode pattern (#1687)
Type: docs and refactoring

Co-authored-by: Subhrodip Mohanta <hello@subho.xyz>
2021-03-22 12:19:46 +05:30
7ac468db20 docs: #590 refactor and add explanation for business delegate (#1686)
Type: docs and refactoring

Co-authored-by: Subhrodip Mohanta <hello@subho.xyz>
2021-03-22 12:04:56 +05:30
794795acf5 task: MVVM design pattern using zkoss framework. (#1678)
* MVVM design pattern using zkoss framework.

* MVVM Design pattern updates for issues reported by SonarCloud.

* MVVM Design pattern updates for coverage issues reported by SonarCloud.

* MVVM Design pattern updates for coverage issues (removing lombok @Data)
reported by SonarCloud.

* MVVM Design pattern updates for coverage issues reported by Sonar - TEST
cases added for Equals and ToString

* MVVM Design Pattern - updating missing/todo details.

* MVVM Design Pattern - adding lombok.config

* MVVM Design Pattern - Removing xml, updating pom.xml and README as per
suggested changes in code review

* Update model-view-viewmodel/README.md

* Update model-view-viewmodel/README.md

* Update model-view-viewmodel/README.md

* Update model-view-viewmodel/README.md

* MVVM Design Pattern - Updated pom.xml and Readme based on Suggested
changes

* added type as xml

* MVVM Design Patterm - root pom.xml and module pom.xml updated

* Update pom.xml

Co-authored-by: Subhrodip Mohanta <hello@subho.xyz>
Co-authored-by: Ilkka Seppälä <iluwatar@users.noreply.github.com>
2021-03-22 11:43:34 +05:30
2b7d181ac4 docs: add explanation for balking pattern (#1685)
* #590 explanation for balking pattern

* #590 fix checkstyle error

Co-authored-by: Subhrodip Mohanta <hello@subho.xyz>
2021-03-21 01:25:47 +05:30
b5ed8b2278 labeling PR should trigger builds again for all-contributors bot (#1682) 2021-03-18 22:22:37 +02:00
8fc64a2d38 docs: add qfxl as a contributor (#1681)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2021-03-18 22:13:28 +02:00
7e91322a43 Add explanation to Async Method Invocation pattern (#1680)
* #590 make the example more interesting and add an explanation

* #590 fix checkstyle

Co-authored-by: Subhrodip Mohanta <hello@subho.xyz>
2021-03-18 11:07:34 +05:30
e9106ccfc5 fix spelling errors (#1679)
Co-authored-by: Subhrodip Mohanta <hello@subho.xyz>
2021-03-18 10:49:45 +05:30
77ffae5ecc Format active object README.md to display correctly on the web site 2021-03-14 16:26:24 +02:00
74abc7a0d6 added license headers to active object pattern (#1676) 2021-03-14 16:08:53 +02:00
ccf350b611 docs: add noamgrinch as a contributor (#1675)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2021-03-14 16:07:39 +02:00
53 changed files with 1936 additions and 406 deletions

View File

@ -1422,6 +1422,34 @@
"contributions": [
"code"
]
},
{
"login": "noamgrinch",
"name": "Noam Greenshtain",
"avatar_url": "https://avatars.githubusercontent.com/u/31648669?v=4",
"profile": "https://github.com/noamgrinch",
"contributions": [
"code"
]
},
{
"login": "qfxl",
"name": "yonghong Xu",
"avatar_url": "https://avatars.githubusercontent.com/u/14086462?v=4",
"profile": "https://xuyonghong.cn/",
"contributions": [
"doc"
]
},
{
"login": "jinishavora",
"name": "jinishavora",
"avatar_url": "https://avatars.githubusercontent.com/u/40777762?v=4",
"profile": "https://www.linkedin.com/in/jinisha-vora",
"contributions": [
"review",
"code"
]
}
],
"contributorsPerLine": 4,

View File

@ -10,7 +10,7 @@
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=iluwatar_java-design-patterns&metric=coverage)](https://sonarcloud.io/dashboard?id=iluwatar_java-design-patterns)
[![Join the chat at https://gitter.im/iluwatar/java-design-patterns](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/iluwatar/java-design-patterns?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[![All Contributors](https://img.shields.io/badge/all_contributors-156-orange.svg?style=flat-square)](#contributors-)
[![All Contributors](https://img.shields.io/badge/all_contributors-159-orange.svg?style=flat-square)](#contributors-)
<!-- ALL-CONTRIBUTORS-BADGE:END -->
<br/>
@ -309,6 +309,11 @@ This project is licensed under the terms of the MIT license.
<td align="center"><a href="https://github.com/richardmr36"><img src="https://avatars.githubusercontent.com/u/19147333?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Martel Richard</b></sub></a><br /><a href="https://github.com/iluwatar/java-design-patterns/commits?author=richardmr36" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/va1m"><img src="https://avatars.githubusercontent.com/u/17025445?v=4?s=100" width="100px;" alt=""/><br /><sub><b>va1m</b></sub></a><br /><a href="https://github.com/iluwatar/java-design-patterns/commits?author=va1m" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/noamgrinch"><img src="https://avatars.githubusercontent.com/u/31648669?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Noam Greenshtain</b></sub></a><br /><a href="https://github.com/iluwatar/java-design-patterns/commits?author=noamgrinch" title="Code">💻</a></td>
<td align="center"><a href="https://xuyonghong.cn/"><img src="https://avatars.githubusercontent.com/u/14086462?v=4?s=100" width="100px;" alt=""/><br /><sub><b>yonghong Xu</b></sub></a><br /><a href="https://github.com/iluwatar/java-design-patterns/commits?author=qfxl" title="Documentation">📖</a></td>
<td align="center"><a href="https://www.linkedin.com/in/jinisha-vora"><img src="https://avatars.githubusercontent.com/u/40777762?v=4?s=100" width="100px;" alt=""/><br /><sub><b>jinishavora</b></sub></a><br /><a href="https://github.com/iluwatar/java-design-patterns/pulls?q=is%3Apr+reviewed-by%3Ajinishavora" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/iluwatar/java-design-patterns/commits?author=jinishavora" title="Code">💻</a></td>
</tr>
</table>
<!-- markdownlint-restore -->

View File

@ -79,12 +79,12 @@ public abstract class ActiveCreature{
return this.name;
}
}
```
We can see that any class that will extend the ActiveCreature class will have its own thread of control to execute and invocate methods.
For example, the Orc class:
```java
public class Orc extends ActiveCreature {
@ -96,6 +96,7 @@ public class Orc extends ActiveCreature {
```
Now, we can create multiple creatures such as Orcs, tell them to eat and roam and they will execute it on their own thread of control:
```java
public static void main(String[] args) {
var app = new App();
@ -121,4 +122,4 @@ Now, we can create multiple creatures such as Orcs, tell them to eat and roam an
## Class diagram
![alt text](./etc/active-object.urm.png "Active Object class diagram")
![alt text](./etc/active-object.urm.PNG "Active Object class diagram")

View File

@ -1,3 +1,26 @@
/*
* The MIT License
* Copyright © 2014-2021 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.activeobject;
import java.util.concurrent.BlockingQueue;

View File

@ -1,3 +1,26 @@
/*
* The MIT License
* Copyright © 2014-2021 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.activeobject;
/**

View File

@ -1,3 +1,26 @@
/*
* The MIT License
* Copyright © 2014-2021 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.activeobject;
import static org.junit.jupiter.api.Assertions.assertEquals;

View File

@ -1,3 +1,26 @@
/*
* The MIT License
* Copyright © 2014-2021 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.activeobject;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;

View File

@ -9,21 +9,160 @@ tags:
---
## Intent
Asynchronous method invocation is pattern where the calling thread
Asynchronous method invocation is a pattern where the calling thread
is not blocked while waiting results of tasks. The pattern provides parallel
processing of multiple independent tasks and retrieving the results via
callbacks or waiting until everything is done.
## Explanation
Real world example
> Launching space rockets is an exciting business. The mission command gives an order to launch and
> after some undetermined time, the rocket either launches successfully or fails miserably.
In plain words
> Asynchronous method invocation starts task processing and returns immediately before the task is
> ready. The results of the task processing are returned to the caller later.
Wikipedia says
> In multithreaded computer programming, asynchronous method invocation (AMI), also known as
> asynchronous method calls or the asynchronous pattern is a design pattern in which the call site
> is not blocked while waiting for the called code to finish. Instead, the calling thread is
> notified when the reply arrives. Polling for a reply is an undesired option.
**Programmatic Example**
In this example, we are launching space rockets and deploying lunar rovers.
The application demonstrates the async method invocation pattern. The key parts of the pattern are
`AsyncResult` which is an intermediate container for an asynchronously evaluated value,
`AsyncCallback` which can be provided to be executed on task completion and `AsyncExecutor` that
manages the execution of the async tasks.
```java
public interface AsyncResult<T> {
boolean isCompleted();
T getValue() throws ExecutionException;
void await() throws InterruptedException;
}
```
```java
public interface AsyncCallback<T> {
void onComplete(T value, Optional<Exception> ex);
}
```
```java
public interface AsyncExecutor {
<T> AsyncResult<T> startProcess(Callable<T> task);
<T> AsyncResult<T> startProcess(Callable<T> task, AsyncCallback<T> callback);
<T> T endProcess(AsyncResult<T> asyncResult) throws ExecutionException, InterruptedException;
}
```
`ThreadAsyncExecutor` is an implementation of `AsyncExecutor`. Some of its key parts are highlighted
next.
```java
public class ThreadAsyncExecutor implements AsyncExecutor {
@Override
public <T> AsyncResult<T> startProcess(Callable<T> task) {
return startProcess(task, null);
}
@Override
public <T> AsyncResult<T> startProcess(Callable<T> task, AsyncCallback<T> callback) {
var result = new CompletableResult<>(callback);
new Thread(
() -> {
try {
result.setValue(task.call());
} catch (Exception ex) {
result.setException(ex);
}
},
"executor-" + idx.incrementAndGet())
.start();
return result;
}
@Override
public <T> T endProcess(AsyncResult<T> asyncResult)
throws ExecutionException, InterruptedException {
if (!asyncResult.isCompleted()) {
asyncResult.await();
}
return asyncResult.getValue();
}
}
```
Then we are ready to launch some rockets to see how everything works together.
```java
public static void main(String[] args) throws Exception {
// construct a new executor that will run async tasks
var executor = new ThreadAsyncExecutor();
// start few async tasks with varying processing times, two last with callback handlers
final var asyncResult1 = executor.startProcess(lazyval(10, 500));
final var asyncResult2 = executor.startProcess(lazyval("test", 300));
final var asyncResult3 = executor.startProcess(lazyval(50L, 700));
final var asyncResult4 = executor.startProcess(lazyval(20, 400), callback("Deploying lunar rover"));
final var asyncResult5 =
executor.startProcess(lazyval("callback", 600), callback("Deploying lunar rover"));
// emulate processing in the current thread while async tasks are running in their own threads
Thread.sleep(350); // Oh boy, we are working hard here
log("Mission command is sipping coffee");
// wait for completion of the tasks
final var result1 = executor.endProcess(asyncResult1);
final var result2 = executor.endProcess(asyncResult2);
final var result3 = executor.endProcess(asyncResult3);
asyncResult4.await();
asyncResult5.await();
// log the results of the tasks, callbacks log immediately when complete
log("Space rocket <" + result1 + "> launch complete");
log("Space rocket <" + result2 + "> launch complete");
log("Space rocket <" + result3 + "> launch complete");
}
```
Here's the program console output.
```java
21:47:08.227 [executor-2] INFO com.iluwatar.async.method.invocation.App - Space rocket <test> launched successfully
21:47:08.269 [main] INFO com.iluwatar.async.method.invocation.App - Mission command is sipping coffee
21:47:08.318 [executor-4] INFO com.iluwatar.async.method.invocation.App - Space rocket <20> launched successfully
21:47:08.335 [executor-4] INFO com.iluwatar.async.method.invocation.App - Deploying lunar rover <20>
21:47:08.414 [executor-1] INFO com.iluwatar.async.method.invocation.App - Space rocket <10> launched successfully
21:47:08.519 [executor-5] INFO com.iluwatar.async.method.invocation.App - Space rocket <callback> launched successfully
21:47:08.519 [executor-5] INFO com.iluwatar.async.method.invocation.App - Deploying lunar rover <callback>
21:47:08.616 [executor-3] INFO com.iluwatar.async.method.invocation.App - Space rocket <50> launched successfully
21:47:08.617 [main] INFO com.iluwatar.async.method.invocation.App - Space rocket <10> launch complete
21:47:08.617 [main] INFO com.iluwatar.async.method.invocation.App - Space rocket <test> launch complete
21:47:08.618 [main] INFO com.iluwatar.async.method.invocation.App - Space rocket <50> launch complete
```
# Class diagram
![alt text](./etc/async-method-invocation.png "Async Method Invocation")
## Applicability
Use async method invocation pattern when
Use the async method invocation pattern when
* You have multiple independent tasks that can run in parallel
* You need to improve the performance of a group of sequential tasks
* You have limited amount of processing capacity or long running tasks and the
caller should not wait the tasks to be ready
* You have a limited amount of processing capacity or long-running tasks and the caller should not wait for the tasks to be ready
## Real world examples

View File

@ -27,10 +27,12 @@ import java.util.concurrent.Callable;
import lombok.extern.slf4j.Slf4j;
/**
* This application demonstrates the async method invocation pattern. Key parts of the pattern are
* <code>AsyncResult</code> which is an intermediate container for an asynchronously evaluated
* value, <code>AsyncCallback</code> which can be provided to be executed on task completion and
* <code>AsyncExecutor</code> that manages the execution of the async tasks.
* In this example, we are launching space rockets and deploying lunar rovers.
*
* <p>The application demonstrates the async method invocation pattern. The key parts of the
* pattern are <code>AsyncResult</code> which is an intermediate container for an asynchronously
* evaluated value, <code>AsyncCallback</code> which can be provided to be executed on task
* completion and <code>AsyncExecutor</code> that manages the execution of the async tasks.
*
* <p>The main method shows example flow of async invocations. The main thread starts multiple
* tasks with variable durations and then continues its own work. When the main thread has done it's
@ -68,13 +70,14 @@ public class App {
final var asyncResult1 = executor.startProcess(lazyval(10, 500));
final var asyncResult2 = executor.startProcess(lazyval("test", 300));
final var asyncResult3 = executor.startProcess(lazyval(50L, 700));
final var asyncResult4 = executor.startProcess(lazyval(20, 400), callback("Callback result 4"));
final var asyncResult4 = executor.startProcess(lazyval(20, 400),
callback("Deploying lunar rover"));
final var asyncResult5 =
executor.startProcess(lazyval("callback", 600), callback("Callback result 5"));
executor.startProcess(lazyval("callback", 600), callback("Deploying lunar rover"));
// emulate processing in the current thread while async tasks are running in their own threads
Thread.sleep(350); // Oh boy I'm working hard here
log("Some hard work done");
Thread.sleep(350); // Oh boy, we are working hard here
log("Mission command is sipping coffee");
// wait for completion of the tasks
final var result1 = executor.endProcess(asyncResult1);
@ -84,9 +87,9 @@ public class App {
asyncResult5.await();
// log the results of the tasks, callbacks log immediately when complete
log("Result 1: " + result1);
log("Result 2: " + result2);
log("Result 3: " + result3);
log("Space rocket <" + result1 + "> launch complete");
log("Space rocket <" + result2 + "> launch complete");
log("Space rocket <" + result3 + "> launch complete");
}
/**
@ -99,7 +102,7 @@ public class App {
private static <T> Callable<T> lazyval(T value, long delayMillis) {
return () -> {
Thread.sleep(delayMillis);
log("Task completed with: " + value);
log("Space rocket <" + value + "> launched successfully");
return value;
};
}
@ -115,7 +118,7 @@ public class App {
if (ex.isPresent()) {
log(name + " failed: " + ex.map(Exception::getMessage).orElse(""));
} else {
log(name + ": " + value);
log(name + " <" + value + ">");
}
};
}

View File

@ -9,19 +9,131 @@ tags:
---
## Intent
Balking Pattern is used to prevent an object from executing certain code if it is an
incomplete or inappropriate state
Balking Pattern is used to prevent an object from executing a certain code if it is in an incomplete
or inappropriate state.
## Explanation
Real world example
> There's a start-button in a washing machine to initiate the laundry washing. When the washing
> machine is inactive the button works as expected, but if it's already washing the button does
> nothing.
In plain words
> Using the balking pattern, a certain code executes only if the object is in particular state.
Wikipedia says
> The balking pattern is a software design pattern that only executes an action on an object when
> the object is in a particular state. For example, if an object reads ZIP files and a calling
> method invokes a get method on the object when the ZIP file is not open, the object would "balk"
> at the request.
**Programmatic Example**
In this example implementation, `WashingMachine` is an object that has two states in which it can
be: ENABLED and WASHING. If the machine is ENABLED, the state changes to WASHING using a thread-safe
method. On the other hand, if it already has been washing and any other thread executes `wash()`
it won't do that and returns without doing anything.
Here are the relevant parts of the `WashingMachine` class.
```java
@Slf4j
public class WashingMachine {
private final DelayProvider delayProvider;
private WashingMachineState washingMachineState;
public WashingMachine(DelayProvider delayProvider) {
this.delayProvider = delayProvider;
this.washingMachineState = WashingMachineState.ENABLED;
}
public WashingMachineState getWashingMachineState() {
return washingMachineState;
}
public void wash() {
synchronized (this) {
var machineState = getWashingMachineState();
LOGGER.info("{}: Actual machine state: {}", Thread.currentThread().getName(), machineState);
if (this.washingMachineState == WashingMachineState.WASHING) {
LOGGER.error("Cannot wash if the machine has been already washing!");
return;
}
this.washingMachineState = WashingMachineState.WASHING;
}
LOGGER.info("{}: Doing the washing", Thread.currentThread().getName());
this.delayProvider.executeAfterDelay(50, TimeUnit.MILLISECONDS, this::endOfWashing);
}
public synchronized void endOfWashing() {
washingMachineState = WashingMachineState.ENABLED;
LOGGER.info("{}: Washing completed.", Thread.currentThread().getId());
}
}
```
Here's the simple `DelayProvider` interface used by the `WashingMachine`.
```java
public interface DelayProvider {
void executeAfterDelay(long interval, TimeUnit timeUnit, Runnable task);
}
```
Now we introduce the application using the `WashingMachine`.
```java
public static void main(String... args) {
final var washingMachine = new WashingMachine();
var executorService = Executors.newFixedThreadPool(3);
for (int i = 0; i < 3; i++) {
executorService.execute(washingMachine::wash);
}
executorService.shutdown();
try {
executorService.awaitTermination(10, TimeUnit.SECONDS);
} catch (InterruptedException ie) {
LOGGER.error("ERROR: Waiting on executor service shutdown!");
Thread.currentThread().interrupt();
}
}
```
Here is the console output of the program.
```
14:02:52.268 [pool-1-thread-2] INFO com.iluwatar.balking.WashingMachine - pool-1-thread-2: Actual machine state: ENABLED
14:02:52.272 [pool-1-thread-2] INFO com.iluwatar.balking.WashingMachine - pool-1-thread-2: Doing the washing
14:02:52.272 [pool-1-thread-3] INFO com.iluwatar.balking.WashingMachine - pool-1-thread-3: Actual machine state: WASHING
14:02:52.273 [pool-1-thread-3] ERROR com.iluwatar.balking.WashingMachine - Cannot wash if the machine has been already washing!
14:02:52.273 [pool-1-thread-1] INFO com.iluwatar.balking.WashingMachine - pool-1-thread-1: Actual machine state: WASHING
14:02:52.273 [pool-1-thread-1] ERROR com.iluwatar.balking.WashingMachine - Cannot wash if the machine has been already washing!
14:02:52.324 [pool-1-thread-2] INFO com.iluwatar.balking.WashingMachine - 14: Washing completed.
```
## Class diagram
![alt text](./etc/balking.png "Balking")
## Applicability
Use the Balking pattern when
* you want to invoke an action on an object only when it is in a particular state
* objects are generally only in a state that is prone to balking temporarily
but for an unknown amount of time
* You want to invoke an action on an object only when it is in a particular state
* Objects are generally only in a state that is prone to balking temporarily but for an unknown
amount of time
## Related patterns
* Guarded Suspension Pattern
* Double Checked Locking Pattern
* [Guarded Suspension Pattern](https://java-design-patterns.com/patterns/guarded-suspension/)
* [Double Checked Locking Pattern](https://java-design-patterns.com/patterns/double-checked-locking/)
## Credits
* [Patterns in Java: A Catalog of Reusable Design Patterns Illustrated with UML, 2nd Edition, Volume 1](https://www.amazon.com/gp/product/0471227293/ref=as_li_qf_asin_il_tl?ie=UTF8&tag=javadesignpat-20&creative=9325&linkCode=as2&creativeASIN=0471227293&linkId=0e39a59ffaab93fb476036fecb637b99)

View File

@ -32,11 +32,11 @@ import lombok.extern.slf4j.Slf4j;
* then the method will return without doing anything. Objects that use this pattern are generally
* only in a state that is prone to balking temporarily but for an unknown amount of time
*
* <p>In this example implementation WashingMachine is an object that has two states in which it
* can be: ENABLED and WASHING. If the machine is ENABLED the state is changed into WASHING that any
* other thread can't invoke this action on this and then do the job. On the other hand if it have
* been already washing and any other thread execute wash() it can't do that once again and returns
* doing nothing.
* <p>In this example implementation, {@link WashingMachine} is an object that has two states in
* which it can be: ENABLED and WASHING. If the machine is ENABLED, the state changes to WASHING
* using a thread-safe method. On the other hand, if it already has been washing and any other
* thread executes {@link WashingMachine#wash()} it won't do that and returns without doing
* anything.
*/
@Slf4j
public class App {
@ -54,11 +54,12 @@ public class App {
}
executorService.shutdown();
try {
executorService.awaitTermination(10, TimeUnit.SECONDS);
if (!executorService.awaitTermination(10, TimeUnit.SECONDS)) {
executorService.shutdownNow();
}
} catch (InterruptedException ie) {
LOGGER.error("ERROR: Waiting on executor service shutdown!");
Thread.currentThread().interrupt();
}
}
}

View File

@ -9,21 +9,156 @@ tags:
---
## Intent
The Business Delegate pattern adds an abstraction layer between
presentation and business tiers. By using the pattern we gain loose coupling
between the tiers and encapsulate knowledge about how to locate, connect to,
and interact with the business objects that make up the application.
## Explanation
Real world example
> A mobile phone application promises to stream any movie in existence to your phone. It captures
> the user's search string and passes this on to the business delegate. The business delegate
> selects the most suitable video streaming service and plays the video from there.
In Plain Words
> Business delegate adds an abstraction layer between the presentation and business tiers.
Wikipedia says
> Business delegate is a Java EE design pattern. This pattern is directing to reduce the coupling
> in between business services and the connected presentation tier, and to hide the implementation
> details of services (including lookup and accessibility of EJB architecture). Business delegates
> acts as an adaptor to invoke business objects from the presentation tier.
**Programmatic Example**
First, we have an abstraction for video streaming services and a couple of implementations.
```java
public interface VideoStreamingService {
void doProcessing();
}
@Slf4j
public class NetflixService implements VideoStreamingService {
@Override
public void doProcessing() {
LOGGER.info("NetflixService is now processing");
}
}
@Slf4j
public class YouTubeService implements VideoStreamingService {
@Override
public void doProcessing() {
LOGGER.info("YouTubeService is now processing");
}
}
```
Then we have a lookup service that decides which video streaming service is used.
```java
@Setter
public class BusinessLookup {
private NetflixService netflixService;
private YouTubeService youTubeService;
public VideoStreamingService getBusinessService(String movie) {
if (movie.toLowerCase(Locale.ROOT).contains("die hard")) {
return netflixService;
} else {
return youTubeService;
}
}
}
```
The business delegate uses a business lookup to route movie playback requests to a suitable
video streaming service.
```java
@Setter
public class BusinessDelegate {
private BusinessLookup lookupService;
public void playbackMovie(String movie) {
VideoStreamingService videoStreamingService = lookupService.getBusinessService(movie);
videoStreamingService.doProcessing();
}
}
```
The mobile client utilizes business delegate to call the business tier.
```java
public class MobileClient {
private final BusinessDelegate businessDelegate;
public MobileClient(BusinessDelegate businessDelegate) {
this.businessDelegate = businessDelegate;
}
public void playbackMovie(String movie) {
businessDelegate.playbackMovie(movie);
}
}
```
Finally, we can show the full example in action.
```java
public static void main(String[] args) {
// prepare the objects
var businessDelegate = new BusinessDelegate();
var businessLookup = new BusinessLookup();
businessLookup.setNetflixService(new NetflixService());
businessLookup.setYouTubeService(new YouTubeService());
businessDelegate.setLookupService(businessLookup);
// create the client and use the business delegate
var client = new MobileClient(businessDelegate);
client.playbackMovie("Die Hard 2");
client.playbackMovie("Maradona: The Greatest Ever");
}
```
Here is the console output.
```
21:15:33.790 [main] INFO com.iluwatar.business.delegate.NetflixService - NetflixService is now processing
21:15:33.794 [main] INFO com.iluwatar.business.delegate.YouTubeService - YouTubeService is now processing
```
## Class diagram
![alt text](./etc/business-delegate.png "Business Delegate")
![alt text](./etc/business-delegate.urm.png "Business Delegate")
## Related patterns
* [Service locator pattern](https://java-design-patterns.com/patterns/service-locator/)
## Applicability
Use the Business Delegate pattern when
* you want loose coupling between presentation and business tiers
* you want to orchestrate calls to multiple business services
* you want to encapsulate service lookups and service calls
* You want loose coupling between presentation and business tiers
* You want to orchestrate calls to multiple business services
* You want to encapsulate service lookups and service calls
## Tutorials
* [Business Delegate Pattern at TutorialsPoint](https://www.tutorialspoint.com/design_pattern/business_delegate_pattern.htm)
## Credits
* [J2EE Design Patterns](https://www.amazon.com/gp/product/0596004273/ref=as_li_tl?ie=UTF8&camp=1789&creative=9325&creativeASIN=0596004273&linkCode=as2&tag=javadesignpat-20&linkId=48d37c67fb3d845b802fa9b619ad8f31)
* [Core J2EE Patterns: Best Practices and Design Strategies](https://www.amazon.com/gp/product/0130648841/ref=as_li_qf_asin_il_tl?ie=UTF8&tag=javadesignpat-20&creative=9325&linkCode=as2&creativeASIN=0130648841&linkId=a0100de2b28c71ede8db1757fb2b5947)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

View File

@ -1,136 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<class-diagram version="1.1.8" icons="true" automaticImage="PNG" always-add-relationships="false" generalizations="true"
realizations="true" associations="true" dependencies="false" nesting-relationships="true">
<class id="1" language="java" name="com.iluwatar.business.delegate.BusinessDelegate" project="business-delegate"
file="/business-delegate/src/main/java/com/iluwatar/business/delegate/BusinessDelegate.java" binary="false"
corner="BOTTOM_RIGHT">
<position height="-1" width="-1" x="272" y="219"/>
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
sort-features="false" accessors="true" visibility="true">
<attributes public="true" package="true" protected="true" private="true" static="true"/>
<operations public="true" package="true" protected="true" private="true" static="true"/>
</display>
</class>
<class id="2" language="java" name="com.iluwatar.business.delegate.Client" project="business-delegate"
file="/business-delegate/src/main/java/com/iluwatar/business/delegate/Client.java" binary="false"
corner="BOTTOM_RIGHT">
<position height="-1" width="-1" x="272" y="64"/>
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
sort-features="false" accessors="true" visibility="true">
<attributes public="true" package="true" protected="true" private="true" static="true"/>
<operations public="true" package="true" protected="true" private="true" static="true"/>
</display>
</class>
<enumeration id="3" language="java" name="com.iluwatar.business.delegate.ServiceType" project="business-delegate"
file="/business-delegate/src/main/java/com/iluwatar/business/delegate/ServiceType.java" binary="false"
corner="BOTTOM_RIGHT">
<position height="-1" width="-1" x="89" y="383"/>
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
sort-features="false" accessors="true" visibility="true">
<attributes public="true" package="true" protected="true" private="true" static="true"/>
<operations public="true" package="true" protected="true" private="true" static="true"/>
</display>
</enumeration>
<interface id="4" language="java" name="com.iluwatar.business.delegate.BusinessService" project="business-delegate"
file="/business-delegate/src/main/java/com/iluwatar/business/delegate/BusinessService.java" binary="false"
corner="BOTTOM_RIGHT">
<position height="-1" width="-1" x="630" y="365"/>
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
sort-features="false" accessors="true" visibility="true">
<attributes public="true" package="true" protected="true" private="true" static="true"/>
<operations public="true" package="true" protected="true" private="true" static="true"/>
</display>
</interface>
<class id="5" language="java" name="com.iluwatar.business.delegate.JmsService" project="business-delegate"
file="/business-delegate/src/main/java/com/iluwatar/business/delegate/JmsService.java" binary="false"
corner="BOTTOM_RIGHT">
<position height="-1" width="-1" x="489" y="210"/>
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
sort-features="false" accessors="true" visibility="true">
<attributes public="true" package="true" protected="true" private="true" static="true"/>
<operations public="true" package="true" protected="true" private="true" static="true"/>
</display>
</class>
<class id="6" language="java" name="com.iluwatar.business.delegate.BusinessLookup" project="business-delegate"
file="/business-delegate/src/main/java/com/iluwatar/business/delegate/BusinessLookup.java" binary="false"
corner="BOTTOM_RIGHT">
<position height="-1" width="-1" x="360" y="399"/>
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
sort-features="false" accessors="true" visibility="true">
<attributes public="true" package="true" protected="true" private="true" static="true"/>
<operations public="true" package="true" protected="true" private="true" static="true"/>
</display>
</class>
<class id="7" language="java" name="com.iluwatar.business.delegate.EjbService" project="business-delegate"
file="/business-delegate/src/main/java/com/iluwatar/business/delegate/EjbService.java" binary="false"
corner="BOTTOM_RIGHT">
<position height="-1" width="-1" x="668" y="210"/>
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
sort-features="false" accessors="true" visibility="true">
<attributes public="true" package="true" protected="true" private="true" static="true"/>
<operations public="true" package="true" protected="true" private="true" static="true"/>
</display>
</class>
<realization id="8">
<end type="SOURCE" refId="7"/>
<end type="TARGET" refId="4"/>
</realization>
<association id="9">
<end type="SOURCE" refId="1" navigable="false">
<attribute id="10" name="lookupService">
<position height="0" width="0" x="0" y="0"/>
</attribute>
<multiplicity id="11" minimum="0" maximum="1">
<position height="0" width="0" x="0" y="0"/>
</multiplicity>
</end>
<end type="TARGET" refId="6" navigable="true"/>
<display labels="true" multiplicity="true"/>
</association>
<association id="12">
<end type="SOURCE" refId="1" navigable="false">
<attribute id="13" name="serviceType">
<position height="0" width="0" x="0" y="0"/>
</attribute>
<multiplicity id="14" minimum="0" maximum="1">
<position height="0" width="0" x="0" y="0"/>
</multiplicity>
</end>
<end type="TARGET" refId="3" navigable="true"/>
<display labels="true" multiplicity="true"/>
</association>
<realization id="15">
<end type="SOURCE" refId="5"/>
<end type="TARGET" refId="4"/>
</realization>
<association id="16">
<end type="SOURCE" refId="2" navigable="false">
<attribute id="17" name="businessDelegate">
<position height="0" width="0" x="0" y="0"/>
</attribute>
<multiplicity id="18" minimum="0" maximum="1">
<position height="0" width="0" x="0" y="0"/>
</multiplicity>
</end>
<end type="TARGET" refId="1" navigable="true"/>
<display labels="true" multiplicity="true"/>
</association>
<association id="19">
<end type="SOURCE" refId="1" navigable="false">
<attribute id="20" name="businessService">
<position height="0" width="0" x="0" y="0"/>
</attribute>
<multiplicity id="21" minimum="0" maximum="1">
<position height="0" width="0" x="0" y="0"/>
</multiplicity>
</end>
<end type="TARGET" refId="4" navigable="true"/>
<display labels="true" multiplicity="true"/>
</association>
<classifier-display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
sort-features="false" accessors="true" visibility="true">
<attributes public="true" package="true" protected="true" private="true" static="true"/>
<operations public="true" package="true" protected="true" private="true" static="true"/>
</classifier-display>
<association-display labels="true" multiplicity="true"/>
</class-diagram>

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@ -5,53 +5,42 @@ package com.iluwatar.business.delegate {
+ main(args : String[]) {static}
}
class BusinessDelegate {
- businessService : BusinessService
- lookupService : BusinessLookup
- serviceType : ServiceType
+ BusinessDelegate()
+ doTask()
+ setLookupService(businessLookup : BusinessLookup)
+ setServiceType(serviceType : ServiceType)
+ playbackMovie(movie : String)
+ setLookupService(lookupService : BusinessLookup)
}
class BusinessLookup {
- ejbService : EjbService
- jmsService : JmsService
- netflixService : NetflixService
- youTubeService : YouTubeService
+ BusinessLookup()
+ getBusinessService(serviceType : ServiceType) : BusinessService
+ setEjbService(ejbService : EjbService)
+ setJmsService(jmsService : JmsService)
+ getBusinessService(movie : String) : VideoStreamingService
+ setNetflixService(netflixService : NetflixService)
+ setYouTubeService(youTubeService : YouTubeService)
}
interface BusinessService {
class MobileClient {
- businessDelegate : BusinessDelegate
+ MobileClient(businessDelegate : BusinessDelegate)
+ playbackMovie(movie : String)
}
class NetflixService {
- LOGGER : Logger {static}
+ NetflixService()
+ doProcessing()
}
interface VideoStreamingService {
+ doProcessing() {abstract}
}
class Client {
- businessDelegate : BusinessDelegate
+ Client(businessDelegate : BusinessDelegate)
+ doTask()
}
class EjbService {
class YouTubeService {
- LOGGER : Logger {static}
+ EjbService()
+ YouTubeService()
+ doProcessing()
}
class JmsService {
- LOGGER : Logger {static}
+ JmsService()
+ doProcessing()
}
enum ServiceType {
+ EJB {static}
+ JMS {static}
+ valueOf(name : String) : ServiceType {static}
+ values() : ServiceType[] {static}
}
}
BusinessLookup --> "-ejbService" EjbService
BusinessDelegate --> "-serviceType" ServiceType
Client --> "-businessDelegate" BusinessDelegate
BusinessDelegate --> "-businessService" BusinessService
BusinessLookup --> "-netflixService" NetflixService
BusinessLookup --> "-youTubeService" YouTubeService
MobileClient --> "-businessDelegate" BusinessDelegate
BusinessDelegate --> "-lookupService" BusinessLookup
BusinessLookup --> "-jmsService" JmsService
EjbService ..|> BusinessService
JmsService ..|> BusinessService
NetflixService ..|> VideoStreamingService
YouTubeService ..|> VideoStreamingService
@enduml

View File

@ -33,9 +33,9 @@ package com.iluwatar.business.delegate;
* retrieved through service lookups. The Business Delegate itself may contain business logic too
* potentially tying together multiple service calls, exception handling, retrying etc.
*
* <p>In this example the client ({@link Client}) utilizes a business delegate (
* {@link BusinessDelegate}) to execute a task. The Business Delegate then selects the appropriate
* service and makes the service call.
* <p>In this example the client ({@link MobileClient}) utilizes a business delegate (
* {@link BusinessDelegate}) to search for movies in video streaming services. The Business Delegate
* then selects the appropriate service and makes the service call.
*/
public class App {
@ -46,18 +46,16 @@ public class App {
*/
public static void main(String[] args) {
// prepare the objects
var businessDelegate = new BusinessDelegate();
var businessLookup = new BusinessLookup();
businessLookup.setEjbService(new EjbService());
businessLookup.setJmsService(new JmsService());
businessLookup.setNetflixService(new NetflixService());
businessLookup.setYouTubeService(new YouTubeService());
businessDelegate.setLookupService(businessLookup);
businessDelegate.setServiceType(ServiceType.EJB);
var client = new Client(businessDelegate);
client.doTask();
businessDelegate.setServiceType(ServiceType.JMS);
client.doTask();
// create the client and use the business delegate
var client = new MobileClient(businessDelegate);
client.playbackMovie("Die Hard 2");
client.playbackMovie("Maradona: The Greatest Ever");
}
}

View File

@ -23,24 +23,18 @@
package com.iluwatar.business.delegate;
import lombok.Setter;
/**
* BusinessDelegate separates the presentation and business tiers.
*/
@Setter
public class BusinessDelegate {
private BusinessLookup lookupService;
private ServiceType serviceType;
public void setLookupService(BusinessLookup businessLookup) {
this.lookupService = businessLookup;
}
public void setServiceType(ServiceType serviceType) {
this.serviceType = serviceType;
}
public void doTask() {
BusinessService businessService = lookupService.getBusinessService(serviceType);
businessService.doProcessing();
public void playbackMovie(String movie) {
VideoStreamingService videoStreamingService = lookupService.getBusinessService(movie);
videoStreamingService.doProcessing();
}
}

View File

@ -23,6 +23,7 @@
package com.iluwatar.business.delegate;
import java.util.Locale;
import lombok.Setter;
/**
@ -31,21 +32,21 @@ import lombok.Setter;
@Setter
public class BusinessLookup {
private EjbService ejbService;
private NetflixService netflixService;
private JmsService jmsService;
private YouTubeService youTubeService;
/**
* Gets service instance based on service type.
* Gets service instance based on given movie search string.
*
* @param serviceType Type of service instance to be returned.
* @param movie Search string for the movie.
* @return Service instance.
*/
public BusinessService getBusinessService(ServiceType serviceType) {
if (serviceType.equals(ServiceType.EJB)) {
return ejbService;
public VideoStreamingService getBusinessService(String movie) {
if (movie.toLowerCase(Locale.ROOT).contains("die hard")) {
return netflixService;
} else {
return jmsService;
return youTubeService;
}
}
}

View File

@ -24,17 +24,17 @@
package com.iluwatar.business.delegate;
/**
* Client utilizes BusinessDelegate to call the business tier.
* MobileClient utilizes BusinessDelegate to call the business tier.
*/
public class Client {
public class MobileClient {
private final BusinessDelegate businessDelegate;
public Client(BusinessDelegate businessDelegate) {
public MobileClient(BusinessDelegate businessDelegate) {
this.businessDelegate = businessDelegate;
}
public void doTask() {
businessDelegate.doTask();
public void playbackMovie(String movie) {
businessDelegate.playbackMovie(movie);
}
}

View File

@ -26,13 +26,13 @@ package com.iluwatar.business.delegate;
import lombok.extern.slf4j.Slf4j;
/**
* Service EJB implementation.
* NetflixService implementation.
*/
@Slf4j
public class EjbService implements BusinessService {
public class NetflixService implements VideoStreamingService {
@Override
public void doProcessing() {
LOGGER.info("EjbService is now processing");
LOGGER.info("NetflixService is now processing");
}
}

View File

@ -24,9 +24,9 @@
package com.iluwatar.business.delegate;
/**
* Interface for service implementations.
* Interface for video streaming service implementations.
*/
public interface BusinessService {
public interface VideoStreamingService {
void doProcessing();
}

View File

@ -26,13 +26,13 @@ package com.iluwatar.business.delegate;
import lombok.extern.slf4j.Slf4j;
/**
* Service JMS implementation.
* YouTubeService implementation.
*/
@Slf4j
public class JmsService implements BusinessService {
public class YouTubeService implements VideoStreamingService {
@Override
public void doProcessing() {
LOGGER.info("JmsService is now processing");
LOGGER.info("YouTubeService is now processing");
}
}

View File

@ -26,25 +26,20 @@ package com.iluwatar.business.delegate;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
/**
* The Business Delegate pattern adds an abstraction layer between the presentation and business
* tiers. By using the pattern we gain loose coupling between the tiers. The Business Delegate
* encapsulates knowledge about how to locate, connect to, and interact with the business objects
* that make up the application.
*
* <p>Some of the services the Business Delegate uses are instantiated directly, and some can be
* retrieved through service lookups. The Business Delegate itself may contain business logic too
* potentially tying together multiple service calls, exception handling, retrying etc.
* Tests for the {@link BusinessDelegate}
*/
class BusinessDelegateTest {
private EjbService ejbService;
private NetflixService netflixService;
private JmsService jmsService;
private YouTubeService youTubeService;
private BusinessDelegate businessDelegate;
@ -54,19 +49,19 @@ class BusinessDelegateTest {
*/
@BeforeEach
public void setup() {
ejbService = spy(new EjbService());
jmsService = spy(new JmsService());
netflixService = spy(new NetflixService());
youTubeService = spy(new YouTubeService());
BusinessLookup businessLookup = spy(new BusinessLookup());
businessLookup.setEjbService(ejbService);
businessLookup.setJmsService(jmsService);
businessLookup.setNetflixService(netflixService);
businessLookup.setYouTubeService(youTubeService);
businessDelegate = spy(new BusinessDelegate());
businessDelegate.setLookupService(businessLookup);
}
/**
* In this example the client ({@link Client}) utilizes a business delegate (
* In this example the client ({@link MobileClient}) utilizes a business delegate (
* {@link BusinessDelegate}) to execute a task. The Business Delegate then selects the appropriate
* service and makes the service call.
*/
@ -74,26 +69,20 @@ class BusinessDelegateTest {
void testBusinessDelegate() {
// setup a client object
var client = new Client(businessDelegate);
// set the service type
businessDelegate.setServiceType(ServiceType.EJB);
var client = new MobileClient(businessDelegate);
// action
client.doTask();
client.playbackMovie("Die hard");
// verifying that the businessDelegate was used by client during doTask() method.
verify(businessDelegate).doTask();
verify(ejbService).doProcessing();
// set the service type
businessDelegate.setServiceType(ServiceType.JMS);
// verifying that the businessDelegate was used by client during playbackMovie() method.
verify(businessDelegate).playbackMovie(anyString());
verify(netflixService).doProcessing();
// action
client.doTask();
client.playbackMovie("Maradona");
// verifying that the businessDelegate was used by client during doTask() method.
verify(businessDelegate, times(2)).doTask();
verify(jmsService).doProcessing();
verify(businessDelegate, times(2)).playbackMovie(anyString());
verify(youTubeService).doProcessing();
}
}

View File

@ -9,18 +9,234 @@ tags:
---
## Intent
Allows to encode behaviour as instructions for virtual machine.
Allows encoding behavior as instructions for a virtual machine.
## Explanation
Real world example
> A team is working on a new game where wizards battle against each other. The wizard behavior
> needs to be carefully adjusted and iterated hundreds of times through playtesting. It's not
> optimal to ask the programmer to make changes each time the game designer wants to vary the
> behavior, so the wizard behavior is implemented as a data-driven virtual machine.
In plain words
> Bytecode pattern enables behavior driven by data instead of code.
[Gameprogrammingpatterns.com](https://gameprogrammingpatterns.com/bytecode.html) documentation
states:
> An instruction set defines the low-level operations that can be performed. A series of
> instructions is encoded as a sequence of bytes. A virtual machine executes these instructions one
> at a time, using a stack for intermediate values. By combining instructions, complex high-level
> behavior can be defined.
**Programmatic Example**
One of the most important game objects is the `Wizard` class.
```java
@AllArgsConstructor
@Setter
@Getter
@Slf4j
public class Wizard {
private int health;
private int agility;
private int wisdom;
private int numberOfPlayedSounds;
private int numberOfSpawnedParticles;
public void playSound() {
LOGGER.info("Playing sound");
numberOfPlayedSounds++;
}
public void spawnParticles() {
LOGGER.info("Spawning particles");
numberOfSpawnedParticles++;
}
}
```
Next, we show the available instructions for our virtual machine. Each of the instructions has its
own semantics on how it operates with the stack data. For example, the ADD instruction takes the top
two items from the stack, adds them together and pushes the result to the stack.
```java
@AllArgsConstructor
@Getter
public enum Instruction {
LITERAL(1), // e.g. "LITERAL 0", push 0 to stack
SET_HEALTH(2), // e.g. "SET_HEALTH", pop health and wizard number, call set health
SET_WISDOM(3), // e.g. "SET_WISDOM", pop wisdom and wizard number, call set wisdom
SET_AGILITY(4), // e.g. "SET_AGILITY", pop agility and wizard number, call set agility
PLAY_SOUND(5), // e.g. "PLAY_SOUND", pop value as wizard number, call play sound
SPAWN_PARTICLES(6), // e.g. "SPAWN_PARTICLES", pop value as wizard number, call spawn particles
GET_HEALTH(7), // e.g. "GET_HEALTH", pop value as wizard number, push wizard's health
GET_AGILITY(8), // e.g. "GET_AGILITY", pop value as wizard number, push wizard's agility
GET_WISDOM(9), // e.g. "GET_WISDOM", pop value as wizard number, push wizard's wisdom
ADD(10), // e.g. "ADD", pop 2 values, push their sum
DIVIDE(11); // e.g. "DIVIDE", pop 2 values, push their division
// ...
}
```
At the heart of our example is the `VirtualMachine` class. It takes instructions as input and
executes them to provide the game object behavior.
```java
@Getter
@Slf4j
public class VirtualMachine {
private final Stack<Integer> stack = new Stack<>();
private final Wizard[] wizards = new Wizard[2];
public VirtualMachine() {
wizards[0] = new Wizard(randomInt(3, 32), randomInt(3, 32), randomInt(3, 32),
0, 0);
wizards[1] = new Wizard(randomInt(3, 32), randomInt(3, 32), randomInt(3, 32),
0, 0);
}
public VirtualMachine(Wizard wizard1, Wizard wizard2) {
wizards[0] = wizard1;
wizards[1] = wizard2;
}
public void execute(int[] bytecode) {
for (var i = 0; i < bytecode.length; i++) {
Instruction instruction = Instruction.getInstruction(bytecode[i]);
switch (instruction) {
case LITERAL:
// Read the next byte from the bytecode.
int value = bytecode[++i];
// Push the next value to stack
stack.push(value);
break;
case SET_AGILITY:
var amount = stack.pop();
var wizard = stack.pop();
setAgility(wizard, amount);
break;
case SET_WISDOM:
amount = stack.pop();
wizard = stack.pop();
setWisdom(wizard, amount);
break;
case SET_HEALTH:
amount = stack.pop();
wizard = stack.pop();
setHealth(wizard, amount);
break;
case GET_HEALTH:
wizard = stack.pop();
stack.push(getHealth(wizard));
break;
case GET_AGILITY:
wizard = stack.pop();
stack.push(getAgility(wizard));
break;
case GET_WISDOM:
wizard = stack.pop();
stack.push(getWisdom(wizard));
break;
case ADD:
var a = stack.pop();
var b = stack.pop();
stack.push(a + b);
break;
case DIVIDE:
a = stack.pop();
b = stack.pop();
stack.push(b / a);
break;
case PLAY_SOUND:
wizard = stack.pop();
getWizards()[wizard].playSound();
break;
case SPAWN_PARTICLES:
wizard = stack.pop();
getWizards()[wizard].spawnParticles();
break;
default:
throw new IllegalArgumentException("Invalid instruction value");
}
LOGGER.info("Executed " + instruction.name() + ", Stack contains " + getStack());
}
}
public void setHealth(int wizard, int amount) {
wizards[wizard].setHealth(amount);
}
// other setters ->
// ...
}
```
Now we can show the full example utilizing the virtual machine.
```java
public static void main(String[] args) {
var vm = new VirtualMachine(
new Wizard(45, 7, 11, 0, 0),
new Wizard(36, 18, 8, 0, 0));
vm.execute(InstructionConverterUtil.convertToByteCode("LITERAL 0"));
vm.execute(InstructionConverterUtil.convertToByteCode("LITERAL 0"));
vm.execute(InstructionConverterUtil.convertToByteCode("GET_HEALTH"));
vm.execute(InstructionConverterUtil.convertToByteCode("LITERAL 0"));
vm.execute(InstructionConverterUtil.convertToByteCode("GET_AGILITY"));
vm.execute(InstructionConverterUtil.convertToByteCode("LITERAL 0"));
vm.execute(InstructionConverterUtil.convertToByteCode("GET_WISDOM"));
vm.execute(InstructionConverterUtil.convertToByteCode("ADD"));
vm.execute(InstructionConverterUtil.convertToByteCode("LITERAL 2"));
vm.execute(InstructionConverterUtil.convertToByteCode("DIVIDE"));
vm.execute(InstructionConverterUtil.convertToByteCode("ADD"));
vm.execute(InstructionConverterUtil.convertToByteCode("SET_HEALTH"));
}
```
Here is the console output.
```
16:20:10.193 [main] INFO com.iluwatar.bytecode.VirtualMachine - Executed LITERAL, Stack contains [0]
16:20:10.196 [main] INFO com.iluwatar.bytecode.VirtualMachine - Executed LITERAL, Stack contains [0, 0]
16:20:10.197 [main] INFO com.iluwatar.bytecode.VirtualMachine - Executed GET_HEALTH, Stack contains [0, 45]
16:20:10.197 [main] INFO com.iluwatar.bytecode.VirtualMachine - Executed LITERAL, Stack contains [0, 45, 0]
16:20:10.197 [main] INFO com.iluwatar.bytecode.VirtualMachine - Executed GET_AGILITY, Stack contains [0, 45, 7]
16:20:10.197 [main] INFO com.iluwatar.bytecode.VirtualMachine - Executed LITERAL, Stack contains [0, 45, 7, 0]
16:20:10.197 [main] INFO com.iluwatar.bytecode.VirtualMachine - Executed GET_WISDOM, Stack contains [0, 45, 7, 11]
16:20:10.197 [main] INFO com.iluwatar.bytecode.VirtualMachine - Executed ADD, Stack contains [0, 45, 18]
16:20:10.197 [main] INFO com.iluwatar.bytecode.VirtualMachine - Executed LITERAL, Stack contains [0, 45, 18, 2]
16:20:10.198 [main] INFO com.iluwatar.bytecode.VirtualMachine - Executed DIVIDE, Stack contains [0, 45, 9]
16:20:10.198 [main] INFO com.iluwatar.bytecode.VirtualMachine - Executed ADD, Stack contains [0, 54]
16:20:10.198 [main] INFO com.iluwatar.bytecode.VirtualMachine - Executed SET_HEALTH, Stack contains []
```
## Class diagram
![alt text](./etc/bytecode.urm.png "Bytecode class diagram")
## Applicability
Use the Bytecode pattern when you have a lot of behavior you need to define and your
games implementation language isnt a good fit because:
* its too low-level, making it tedious or error-prone to program in.
* iterating on it takes too long due to slow compile times or other tooling issues.
* it has too much trust. If you want to ensure the behavior being defined cant break the game, you need to sandbox it from the rest of the codebase.
* Its too low-level, making it tedious or error-prone to program in.
* Iterating on it takes too long due to slow compile times or other tooling issues.
* It has too much trust. If you want to ensure the behavior being defined cant break the game, you need to sandbox it from the rest of the codebase.
## Related patterns
* [Interpreter](https://java-design-patterns.com/patterns/interpreter/)
## Credits

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

View File

@ -1,49 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<class-diagram version="1.2.3" icons="true" always-add-relationships="false" generalizations="true" realizations="true"
associations="true" dependencies="false" nesting-relationships="true" router="FAN">
<class id="1" language="java" name="com.iluwatar.bytecode.VirtualMachine" project="bytecode"
file="/bytecode/src/main/java/com/iluwatar/bytecode/VirtualMachine.java" binary="false" corner="BOTTOM_RIGHT">
<position height="-1" width="-1" x="455" y="173"/>
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
sort-features="false" accessors="true" visibility="true">
<attributes public="true" package="true" protected="true" private="true" static="true"/>
<operations public="true" package="true" protected="true" private="true" static="true"/>
</display>
</class>
<class id="2" language="java" name="com.iluwatar.bytecode.App" project="bytecode"
file="/bytecode/src/main/java/com/iluwatar/bytecode/App.java" binary="false" corner="BOTTOM_RIGHT">
<position height="-1" width="-1" x="148" y="110"/>
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
sort-features="false" accessors="true" visibility="true">
<attributes public="true" package="true" protected="true" private="true" static="true"/>
<operations public="true" package="true" protected="true" private="true" static="true"/>
</display>
</class>
<class id="3" language="java" name="com.iluwatar.bytecode.Wizard" project="bytecode"
file="/bytecode/src/main/java/com/iluwatar/bytecode/Wizard.java" binary="false" corner="BOTTOM_RIGHT">
<position height="-1" width="-1" x="148" y="416"/>
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
sort-features="false" accessors="true" visibility="true">
<attributes public="true" package="true" protected="true" private="true" static="true"/>
<operations public="true" package="true" protected="true" private="true" static="true"/>
</display>
</class>
<association id="4">
<end type="SOURCE" refId="1" navigable="false" variant="ASSOCIATION">
<attribute id="5" name="wizards">
<position height="18" width="48" x="296" y="291"/>
</attribute>
<multiplicity id="6" minimum="0" maximum="2147483647">
<position height="0" width="0" x="-327" y="-27"/>
</multiplicity>
</end>
<end type="TARGET" refId="3" navigable="true" variant="ASSOCIATION"/>
<display labels="true" multiplicity="true"/>
</association>
<classifier-display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
sort-features="false" accessors="true" visibility="true">
<attributes public="true" package="true" protected="true" private="true" static="true"/>
<operations public="true" package="true" protected="true" private="true" static="true"/>
</classifier-display>
<association-display labels="true" multiplicity="true"/>
</class-diagram>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 84 KiB

View File

@ -3,7 +3,6 @@ package com.iluwatar.bytecode {
class App {
- LOGGER : Logger {static}
+ App()
- interpretInstruction(instruction : String, vm : VirtualMachine) {static}
+ main(args : String[]) {static}
}
enum Instruction {
@ -18,22 +17,25 @@ package com.iluwatar.bytecode {
+ SET_HEALTH {static}
+ SET_WISDOM {static}
+ SPAWN_PARTICLES {static}
- value : int
- intValue : int
+ getInstruction(value : int) : Instruction {static}
+ getIntValue() : int
+ valueOf(name : String) : Instruction {static}
+ values() : Instruction[] {static}
}
class VirtualMachine {
- LOGGER : Logger {static}
- stack : Stack<Integer>
- wizards : Wizard[]
+ VirtualMachine()
+ VirtualMachine(wizard1 : Wizard, wizard2 : Wizard)
+ execute(bytecode : int[])
+ getAgility(wizard : int) : int
+ getHealth(wizard : int) : int
+ getStack() : Stack<Integer>
+ getWisdom(wizard : int) : int
+ getWizards() : Wizard[]
- randomInt(min : int, max : int) : int
+ setAgility(wizard : int, amount : int)
+ setHealth(wizard : int, amount : int)
+ setWisdom(wizard : int, amount : int)
@ -45,7 +47,7 @@ package com.iluwatar.bytecode {
- numberOfPlayedSounds : int
- numberOfSpawnedParticles : int
- wisdom : int
+ Wizard()
+ Wizard(health : int, agility : int, wisdom : int, numberOfPlayedSounds : int, numberOfSpawnedParticles : int)
+ getAgility() : int
+ getHealth() : int
+ getNumberOfPlayedSounds() : int
@ -54,6 +56,8 @@ package com.iluwatar.bytecode {
+ playSound()
+ setAgility(agility : int)
+ setHealth(health : int)
+ setNumberOfPlayedSounds(numberOfPlayedSounds : int)
+ setNumberOfSpawnedParticles(numberOfSpawnedParticles : int)
+ setWisdom(wisdom : int)
+ spawnParticles()
}

View File

@ -49,33 +49,21 @@ public class App {
*/
public static void main(String[] args) {
var wizard = new Wizard();
wizard.setHealth(45);
wizard.setAgility(7);
wizard.setWisdom(11);
var vm = new VirtualMachine(
new Wizard(45, 7, 11, 0, 0),
new Wizard(36, 18, 8, 0, 0));
var vm = new VirtualMachine();
vm.getWizards()[0] = wizard;
String literal = "LITERAL 0";
interpretInstruction(literal, vm);
interpretInstruction(literal, vm);
interpretInstruction("GET_HEALTH", vm);
interpretInstruction(literal, vm);
interpretInstruction("GET_AGILITY", vm);
interpretInstruction(literal, vm);
interpretInstruction("GET_WISDOM ", vm);
interpretInstruction("ADD", vm);
interpretInstruction("LITERAL 2", vm);
interpretInstruction("DIVIDE", vm);
interpretInstruction("ADD", vm);
interpretInstruction("SET_HEALTH", vm);
}
private static void interpretInstruction(String instruction, VirtualMachine vm) {
vm.execute(InstructionConverterUtil.convertToByteCode(instruction));
var stack = vm.getStack();
LOGGER.info(instruction + String.format("%" + (12 - instruction.length()) + "s", "") + stack);
vm.execute(InstructionConverterUtil.convertToByteCode("LITERAL 0"));
vm.execute(InstructionConverterUtil.convertToByteCode("LITERAL 0"));
vm.execute(InstructionConverterUtil.convertToByteCode("GET_HEALTH"));
vm.execute(InstructionConverterUtil.convertToByteCode("LITERAL 0"));
vm.execute(InstructionConverterUtil.convertToByteCode("GET_AGILITY"));
vm.execute(InstructionConverterUtil.convertToByteCode("LITERAL 0"));
vm.execute(InstructionConverterUtil.convertToByteCode("GET_WISDOM"));
vm.execute(InstructionConverterUtil.convertToByteCode("ADD"));
vm.execute(InstructionConverterUtil.convertToByteCode("LITERAL 2"));
vm.execute(InstructionConverterUtil.convertToByteCode("DIVIDE"));
vm.execute(InstructionConverterUtil.convertToByteCode("ADD"));
vm.execute(InstructionConverterUtil.convertToByteCode("SET_HEALTH"));
}
}

View File

@ -33,17 +33,17 @@ import lombok.Getter;
@Getter
public enum Instruction {
LITERAL(1),
SET_HEALTH(2),
SET_WISDOM(3),
SET_AGILITY(4),
PLAY_SOUND(5),
SPAWN_PARTICLES(6),
GET_HEALTH(7),
GET_AGILITY(8),
GET_WISDOM(9),
ADD(10),
DIVIDE(11);
LITERAL(1), // e.g. "LITERAL 0", push 0 to stack
SET_HEALTH(2), // e.g. "SET_HEALTH", pop health and wizard number, call set health
SET_WISDOM(3), // e.g. "SET_WISDOM", pop wisdom and wizard number, call set wisdom
SET_AGILITY(4), // e.g. "SET_AGILITY", pop agility and wizard number, call set agility
PLAY_SOUND(5), // e.g. "PLAY_SOUND", pop value as wizard number, call play sound
SPAWN_PARTICLES(6), // e.g. "SPAWN_PARTICLES", pop value as wizard number, call spawn particles
GET_HEALTH(7), // e.g. "GET_HEALTH", pop value as wizard number, push wizard's health
GET_AGILITY(8), // e.g. "GET_AGILITY", pop value as wizard number, push wizard's agility
GET_WISDOM(9), // e.g. "GET_WISDOM", pop value as wizard number, push wizard's wisdom
ADD(10), // e.g. "ADD", pop 2 values, push their sum
DIVIDE(11); // e.g. "DIVIDE", pop 2 values, push their division
private final int intValue;

View File

@ -24,12 +24,15 @@
package com.iluwatar.bytecode;
import java.util.Stack;
import java.util.concurrent.ThreadLocalRandom;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
/**
* Implementation of virtual machine.
*/
@Getter
@Slf4j
public class VirtualMachine {
private final Stack<Integer> stack = new Stack<>();
@ -37,12 +40,21 @@ public class VirtualMachine {
private final Wizard[] wizards = new Wizard[2];
/**
* Constructor.
* No-args constructor.
*/
public VirtualMachine() {
for (var i = 0; i < wizards.length; i++) {
wizards[i] = new Wizard();
}
wizards[0] = new Wizard(randomInt(3, 32), randomInt(3, 32), randomInt(3, 32),
0, 0);
wizards[1] = new Wizard(randomInt(3, 32), randomInt(3, 32), randomInt(3, 32),
0, 0);
}
/**
* Constructor taking the wizards as arguments.
*/
public VirtualMachine(Wizard wizard1, Wizard wizard2) {
wizards[0] = wizard1;
wizards[1] = wizard2;
}
/**
@ -57,6 +69,7 @@ public class VirtualMachine {
case LITERAL:
// Read the next byte from the bytecode.
int value = bytecode[++i];
// Push the next value to stack
stack.push(value);
break;
case SET_AGILITY:
@ -107,6 +120,7 @@ public class VirtualMachine {
default:
throw new IllegalArgumentException("Invalid instruction value");
}
LOGGER.info("Executed " + instruction.name() + ", Stack contains " + getStack());
}
}
@ -133,4 +147,8 @@ public class VirtualMachine {
public int getAgility(int wizard) {
return wizards[wizard].getAgility();
}
private int randomInt(int min, int max) {
return ThreadLocalRandom.current().nextInt(min, max + 1);
}
}

View File

@ -23,6 +23,7 @@
package com.iluwatar.bytecode;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
@ -31,16 +32,15 @@ import lombok.extern.slf4j.Slf4j;
* This class represent game objects which properties can be changed by instructions interpreted by
* virtual machine.
*/
@AllArgsConstructor
@Setter
@Getter
@Slf4j
public class Wizard {
private int health;
private int agility;
private int wisdom;
private int numberOfPlayedSounds;
private int numberOfSpawnedParticles;
@ -53,5 +53,4 @@ public class Wizard {
LOGGER.info("Spawning particles");
numberOfSpawnedParticles++;
}
}

View File

@ -73,6 +73,4 @@ public class InstructionConverterUtil {
return false;
}
}
}

View File

@ -10,20 +10,326 @@ tags:
---
## Intent
To avoid expensive re-acquisition of resources by not releasing
the resources immediately after their use. The resources retain their identity, are kept in some
fast-access storage, and are re-used to avoid having to acquire them again.
The caching pattern avoids expensive re-acquisition of resources by not releasing them immediately
after use. The resources retain their identity, are kept in some fast-access storage, and are
re-used to avoid having to acquire them again.
## Explanation
Real world example
> A team is working on a website that provides new homes for abandoned cats. People can post their
> cats on the website after registering, but all the new posts require approval from one of the
> site moderators. The user accounts of the site moderators contain a specific flag and the data
> is stored in a MongoDB database. Checking for the moderator flag each time a post is viewed
> becomes expensive and it's a good idea to utilize caching here.
In plain words
> Caching pattern keeps frequently needed data in fast-access storage to improve performance.
Wikipedia says:
> In computing, a cache is a hardware or software component that stores data so that future
> requests for that data can be served faster; the data stored in a cache might be the result of
> an earlier computation or a copy of data stored elsewhere. A cache hit occurs when the requested
> data can be found in a cache, while a cache miss occurs when it cannot. Cache hits are served by
> reading data from the cache, which is faster than recomputing a result or reading from a slower
> data store; thus, the more requests that can be served from the cache, the faster the system
> performs.
**Programmatic Example**
Let's first look at the data layer of our application. The interesting classes are `UserAccount`
which is a simple Java object containing the user account details, and `DbManager` which handles
reading and writing of these objects to/from MongoDB database.
```java
@Setter
@Getter
@AllArgsConstructor
@ToString
public class UserAccount {
private String userId;
private String userName;
private String additionalInfo;
}
@Slf4j
public final class DbManager {
private static MongoClient mongoClient;
private static MongoDatabase db;
private DbManager() { /*...*/ }
public static void createVirtualDb() { /*...*/ }
public static void connect() throws ParseException { /*...*/ }
public static UserAccount readFromDb(String userId) { /*...*/ }
public static void writeToDb(UserAccount userAccount) { /*...*/ }
public static void updateDb(UserAccount userAccount) { /*...*/ }
public static void upsertDb(UserAccount userAccount) { /*...*/ }
}
```
In the example, we are demonstrating various different caching policies
* Write-through writes data to the cache and DB in a single transaction
* Write-around writes data immediately into the DB instead of the cache
* Write-behind writes data into the cache initially whilst the data is only written into the DB
when the cache is full
* Cache-aside pushes the responsibility of keeping the data synchronized in both data sources to
the application itself
* Read-through strategy is also included in the aforementioned strategies and it returns data from
the cache to the caller if it exists, otherwise queries from DB and stores it into the cache for
future use.
The cache implementation in `LruCache` is a hash table accompanied by a doubly
linked-list. The linked-list helps in capturing and maintaining the LRU data in the cache. When
data is queried (from the cache), added (to the cache), or updated, the data is moved to the front
of the list to depict itself as the most-recently-used data. The LRU data is always at the end of
the list.
```java
@Slf4j
public class LruCache {
static class Node {
String userId;
UserAccount userAccount;
Node previous;
Node next;
public Node(String userId, UserAccount userAccount) {
this.userId = userId;
this.userAccount = userAccount;
}
}
/* ... omitted details ... */
public LruCache(int capacity) {
this.capacity = capacity;
}
public UserAccount get(String userId) {
if (cache.containsKey(userId)) {
var node = cache.get(userId);
remove(node);
setHead(node);
return node.userAccount;
}
return null;
}
public void set(String userId, UserAccount userAccount) {
if (cache.containsKey(userId)) {
var old = cache.get(userId);
old.userAccount = userAccount;
remove(old);
setHead(old);
} else {
var newNode = new Node(userId, userAccount);
if (cache.size() >= capacity) {
LOGGER.info("# Cache is FULL! Removing {} from cache...", end.userId);
cache.remove(end.userId); // remove LRU data from cache.
remove(end);
setHead(newNode);
} else {
setHead(newNode);
}
cache.put(userId, newNode);
}
}
public boolean contains(String userId) {
return cache.containsKey(userId);
}
public void remove(Node node) { /* ... */ }
public void setHead(Node node) { /* ... */ }
public void invalidate(String userId) { /* ... */ }
public boolean isFull() { /* ... */ }
public UserAccount getLruData() { /* ... */ }
public void clear() { /* ... */ }
public List<UserAccount> getCacheDataInListForm() { /* ... */ }
public void setCapacity(int newCapacity) { /* ... */ }
}
```
The next layer we are going to look at is `CacheStore` which implements the different caching
strategies.
```java
@Slf4j
public class CacheStore {
private static LruCache cache;
/* ... details omitted ... */
public static UserAccount readThrough(String userId) {
if (cache.contains(userId)) {
LOGGER.info("# Cache Hit!");
return cache.get(userId);
}
LOGGER.info("# Cache Miss!");
UserAccount userAccount = DbManager.readFromDb(userId);
cache.set(userId, userAccount);
return userAccount;
}
public static void writeThrough(UserAccount userAccount) {
if (cache.contains(userAccount.getUserId())) {
DbManager.updateDb(userAccount);
} else {
DbManager.writeToDb(userAccount);
}
cache.set(userAccount.getUserId(), userAccount);
}
public static void clearCache() {
if (cache != null) {
cache.clear();
}
}
public static void flushCache() {
LOGGER.info("# flushCache...");
Optional.ofNullable(cache)
.map(LruCache::getCacheDataInListForm)
.orElse(List.of())
.forEach(DbManager::updateDb);
}
/* ... omitted the implementation of other caching strategies ... */
}
```
`AppManager` helps to bridge the gap in communication between the main class and the application's
back-end. DB connection is initialized through this class. The chosen caching strategy/policy is
also initialized here. Before the cache can be used, the size of the cache has to be set. Depending
on the chosen caching policy, `AppManager` will call the appropriate function in the `CacheStore`
class.
```java
@Slf4j
public final class AppManager {
private static CachingPolicy cachingPolicy;
private AppManager() {
}
public static void initDb(boolean useMongoDb) { /* ... */ }
public static void initCachingPolicy(CachingPolicy policy) { /* ... */ }
public static void initCacheCapacity(int capacity) { /* ... */ }
public static UserAccount find(String userId) {
if (cachingPolicy == CachingPolicy.THROUGH || cachingPolicy == CachingPolicy.AROUND) {
return CacheStore.readThrough(userId);
} else if (cachingPolicy == CachingPolicy.BEHIND) {
return CacheStore.readThroughWithWriteBackPolicy(userId);
} else if (cachingPolicy == CachingPolicy.ASIDE) {
return findAside(userId);
}
return null;
}
public static void save(UserAccount userAccount) {
if (cachingPolicy == CachingPolicy.THROUGH) {
CacheStore.writeThrough(userAccount);
} else if (cachingPolicy == CachingPolicy.AROUND) {
CacheStore.writeAround(userAccount);
} else if (cachingPolicy == CachingPolicy.BEHIND) {
CacheStore.writeBehind(userAccount);
} else if (cachingPolicy == CachingPolicy.ASIDE) {
saveAside(userAccount);
}
}
public static String printCacheContent() {
return CacheStore.print();
}
/* ... details omitted ... */
}
```
Here is what we do in the main class of the application.
```java
@Slf4j
public class App {
public static void main(String[] args) {
AppManager.initDb(false);
AppManager.initCacheCapacity(3);
var app = new App();
app.useReadAndWriteThroughStrategy();
app.useReadThroughAndWriteAroundStrategy();
app.useReadThroughAndWriteBehindStrategy();
app.useCacheAsideStategy();
}
public void useReadAndWriteThroughStrategy() {
LOGGER.info("# CachingPolicy.THROUGH");
AppManager.initCachingPolicy(CachingPolicy.THROUGH);
var userAccount1 = new UserAccount("001", "John", "He is a boy.");
AppManager.save(userAccount1);
LOGGER.info(AppManager.printCacheContent());
AppManager.find("001");
AppManager.find("001");
}
public void useReadThroughAndWriteAroundStrategy() { /* ... */ }
public void useReadThroughAndWriteBehindStrategy() { /* ... */ }
public void useCacheAsideStategy() { /* ... */ }
}
```
Finally, here is some of the console output from the program.
```
12:32:53.845 [main] INFO com.iluwatar.caching.App - # CachingPolicy.THROUGH
12:32:53.900 [main] INFO com.iluwatar.caching.App -
--CACHE CONTENT--
UserAccount(userId=001, userName=John, additionalInfo=He is a boy.)
----
```
## Class diagram
![alt text](./etc/caching.png "Caching")
## Applicability
Use the Caching pattern(s) when
* Repetitious acquisition, initialization, and release of the same resource causes unnecessary performance overhead.
* Repetitious acquisition, initialization, and release of the same resource cause unnecessary
performance overhead.
## Related patterns
* [Proxy](https://java-design-patterns.com/patterns/proxy/)
## Credits
* [Write-through, write-around, write-back: Cache explained](http://www.computerweekly.com/feature/Write-through-write-around-write-back-Cache-explained)
* [Read-Through, Write-Through, Write-Behind, and Refresh-Ahead Caching](https://docs.oracle.com/cd/E15357_01/coh.360/e15723/cache_rtwtwbra.htm#COHDG5177)
* [Cache-Aside pattern](https://docs.microsoft.com/en-us/azure/architecture/patterns/cache-aside)
* [Java EE 8 High Performance: Master techniques such as memory optimization, caching, concurrency, and multithreading to achieve maximum performance from your enterprise applications](https://www.amazon.com/gp/product/178847306X/ref=as_li_qf_asin_il_tl?ie=UTF8&tag=javadesignpat-20&creative=9325&linkCode=as2&creativeASIN=178847306X&linkId=e948720055599f248cdac47da9125ff4)
* [Java Performance: In-Depth Advice for Tuning and Programming Java 8, 11, and Beyond](https://www.amazon.com/gp/product/1492056111/ref=as_li_qf_asin_il_tl?ie=UTF8&tag=javadesignpat-20&creative=9325&linkCode=as2&creativeASIN=1492056111&linkId=7e553581559b9ec04221259e52004b08)
* [Effective Java](https://www.amazon.com/gp/product/B078H61SCH/ref=as_li_qf_asin_il_tl?ie=UTF8&tag=javadesignpat-20&creative=9325&linkCode=as2&creativeASIN=B078H61SCH&linkId=f06607a0b48c76541ef19c5b8b9e7882)
* [Java Performance: The Definitive Guide: Getting the Most Out of Your Code](https://www.amazon.com/gp/product/1449358454/ref=as_li_qf_asin_il_tl?ie=UTF8&tag=javadesignpat-20&creative=9325&linkCode=as2&creativeASIN=1449358454&linkId=475c18363e350630cc0b39ab681b2687)

View File

@ -25,6 +25,7 @@ package com.iluwatar.caching;
import java.text.ParseException;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
/**
* AppManager helps to bridge the gap in communication between the main class and the application's
@ -33,6 +34,7 @@ import java.util.Optional;
* Depending on the chosen caching policy, AppManager will call the appropriate function in the
* CacheStore class.
*/
@Slf4j
public final class AppManager {
private static CachingPolicy cachingPolicy;
@ -50,7 +52,7 @@ public final class AppManager {
try {
DbManager.connect();
} catch (ParseException e) {
e.printStackTrace();
LOGGER.error("Error connecting to MongoDB", e);
}
} else {
DbManager.createVirtualDb();

View File

@ -30,6 +30,7 @@ import com.mongodb.client.model.UpdateOptions;
import java.text.ParseException;
import java.util.HashMap;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.bson.Document;
/**
@ -41,6 +42,7 @@ import org.bson.Document;
* underlying data storage (connect()) or a simple Java data structure to (temporarily) store the
* data/objects during runtime (createVirtualDB()).</p>
*/
@Slf4j
public final class DbManager {
private static MongoClient mongoClient;
@ -83,7 +85,7 @@ public final class DbManager {
try {
connect();
} catch (ParseException e) {
e.printStackTrace();
LOGGER.error("Error connecting to MongoDB", e);
}
}
var iterable = db
@ -110,7 +112,7 @@ public final class DbManager {
try {
connect();
} catch (ParseException e) {
e.printStackTrace();
LOGGER.error("Error connecting to MongoDB", e);
}
}
db.getCollection(CachingConstants.USER_ACCOUNT).insertOne(
@ -132,7 +134,7 @@ public final class DbManager {
try {
connect();
} catch (ParseException e) {
e.printStackTrace();
LOGGER.error("Error connecting to MongoDB", e);
}
}
db.getCollection(CachingConstants.USER_ACCOUNT).updateOne(
@ -153,7 +155,7 @@ public final class DbManager {
try {
connect();
} catch (ParseException e) {
e.printStackTrace();
LOGGER.error("Error connecting to MongoDB", e);
}
}
db.getCollection(CachingConstants.USER_ACCOUNT).updateOne(

View File

@ -105,7 +105,7 @@ Program output:
```java
This is Ford.
This Ferrari.
This is Ferrari.
```
## Class Diagram

View File

@ -0,0 +1,145 @@
---
layout: pattern
title: Model-View-ViewModel
folder: model-view-viewmodel
permalink: /patterns/model-view-viewmodel/
categories: Architectural
tags:
- Decoupling
---
## Also known as
ModelViewBinder
## Intent
To apply "[Separation of Concerns](https://java-design-patterns.com/principles/#separation-of-concerns)" to separate the logic from the UI components and allow developers to work on UI without affecting the logic and vice versa.
## Explanation
Wikipedia says
> Modelviewviewmodel (MVVM) is a software architectural pattern that facilitates the separation of the development of the graphical user interface (the view) be it via a markup language or GUI code from the development of the business logic or back-end logic (the model) so that the view is not dependent on any specific model platform.
**Programmatic Example**
Zkoss implementation:
> ViewModel will hold the business logic and expose the data from model to View
```java
public class BookViewModel {
@WireVariable
private List<Book> bookList;
private Book selectedBook;
private BookService bookService = new BookServiceImpl();
public Book getSelectedBook() {
return selectedBook;
}
@NotifyChange("selectedBook")
public void setSelectedBook(Book selectedBook) {
this.selectedBook = selectedBook;
}
public List<Book> getBookList() {
return bookService.load();
}
/** Deleting a book.
*/
@Command
@NotifyChange({"selectedBook","bookList"})
public void deleteBook() {
if (selectedBook != null) {
getBookList().remove(selectedBook);
selectedBook = null;
}
}
```
> View will have no logic, only UI elements
```xml
<zk>
<window title="List of Books" border="normal" width="600px" apply="org.zkoss.bind.BindComposer" viewModel="@id('vm') @init('com.iluwatar.model.view.viewmodel.BookViewModel')">
<vbox hflex="true">
<listbox model="@bind(vm.bookList)" selectedItem="@bind(vm.selectedBook)" height="400px" mold="paging">
<listhead>
<listheader label="Book Name"/>
<listheader label="Author"/>
</listhead>
<template name="model" var="book">
<listitem >
<listcell label="@bind(book.name)"/>
<listcell label="@bind(book.author)"/>
</listitem>
</template>
</listbox>
</vbox>
<toolbar>
        <button label="Delete" onClick="@command('deleteBook')" disabled="@load(empty vm.selectedBook)" />
    </toolbar>
<hbox style="margin-top:20px" visible="@bind(not empty vm.selectedBook)">
<vbox>
<hlayout>
Book Name : <label value="@bind(vm.selectedBook.name)" style="font-weight:bold"/>
</hlayout>
<hlayout>
Book Author : <label value="@bind(vm.selectedBook.author)" style="font-weight:bold"/>
</hlayout>
<hlayout>
Book Description : <label value="@bind(vm.selectedBook.description)" style="font-weight:bold"/>
</hlayout>
</vbox>
</hbox>
</window>
</zk>
```
To deploy the example, go to model-view-viewmodel folder and run:
* `mvn clean install`
* `mvn jetty:run -Djetty.http.port=9911`
* Open browser to address: http://localhost:9911/model-view-viewmodel/
## Class diagram
![alt text](./etc/model-view-viewmodel.png "MVVM pattern class diagram")
## Applicability
* When looking for clean architecture, with better reusability, testability and maintainability.
## Tutorials
* [Zkoss Demo](https://www.zkoss.org/zkdemo/getting_started/mvvm)
* [Learn MVVM](https://www.learnmvvm.com/)
* [Android Developer CodeLabs](https://codelabs.developers.google.com/codelabs/android-databinding)
## Typical Use Case
* Android apps
* .NET framework applications
* JavaScript applications
## Real world examples
* ZK Framework [zkoss.org](https://www.zkoss.org/)
* KnockoutJS [knockoutjs.com](https://knockoutjs.com/)
## Consequences
* John Gossman has criticized the MVVM pattern and its application in specific uses, stating that MVVM can be "overkill" when creating simple user interfaces. For larger applications, he believes that generalizing the viewmodel upfront can be difficult, and that large-scale data binding can lead to lower performance - Ref: [MVVM-Wiki](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel)
* Can be hard to design ViewModel for larger applications.
* For complex databinding, debugging can be difficult.
## Credits
* [ZK MVVM](https://www.zkoss.org/wiki/ZK%20Developer's%20Reference/MVVM)
* [GeeksforGeeks MVVM Intro](https://www.geeksforgeeks.org/introduction-to-model-view-view-model-mvvm/)
* [ZK MVVM Book](http://books.zkoss.org/zk-mvvm-book/9.5/)
* [Microsoft MVVM](https://docs.microsoft.com/en-us/archive/msdn-magazine/2009/february/patterns-wpf-apps-with-the-model-view-viewmodel-design-pattern)

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -0,0 +1,43 @@
@startuml
package com.iluwatar.model.view.viewmodel {
class Book {
- author : String
- description : String
- name : String
+ Book(name : String, author : String, description : String)
# canEqual(other : Object) : boolean
+ equals(o : Object) : boolean
+ getAuthor() : String
+ getDescription() : String
+ getName() : String
+ hashCode() : int
+ setAuthor(author : String)
+ setDescription(description : String)
+ setName(name : String)
+ toString() : String
}
interface BookService {
+ load() : List<Book> {abstract}
}
class BookServiceImpl {
- designPatternBooks : List<Book>
+ BookServiceImpl()
+ load() : List<Book>
}
class BookViewModel {
- bookList : List<Book>
- bookService : BookService
- selectedBook : Book
+ BookViewModel()
+ deleteBook()
+ getBookList() : List<Book>
+ getSelectedBook() : Book
+ setSelectedBook(selectedBook : Book)
}
}
BookViewModel --> "-bookService" BookService
BookServiceImpl --> "-designPatternBooks" Book
BookViewModel --> "-bookList" Book
BookViewModel --> "-selectedBook" Book
BookServiceImpl ..|> BookService
@enduml

View File

@ -0,0 +1,2 @@
config.stopBubbling = true
lombok.addLombokGeneratedAnnotation = true

View File

@ -0,0 +1,115 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>java-design-patterns</artifactId>
<groupId>com.iluwatar</groupId>
<version>1.24.0-SNAPSHOT</version>
</parent>
<groupId>com.iluwatar</groupId>
<artifactId>model-view-viewmodel</artifactId>
<version>1.24.0-SNAPSHOT</version>
<properties>
<zk.version>9.0.0</zk.version>
<guava.version>19.0</guava.version>
<jetty-maven-plugin.version>9.4.28.v20200408</jetty-maven-plugin.version>
<maven-war-plugin.version>2.1.1</maven-war-plugin.version>
<maven-assembly-plugin.version>2.2</maven-assembly-plugin.version>
<maven.build.timestamp.format>yyyy-MM-dd</maven.build.timestamp.format>
<packname>-${project.version}-FL-${maven.build.timestamp}</packname>
</properties>
<packaging>war</packaging>
<name>model-view-viewmodel</name>
<description>model-view-viewmodel</description>
<licenses>
<license>
<name>GNU LESSER GENERAL PUBLIC LICENSE, Version 3</name>
<url>https://www.gnu.org/licenses/lgpl.html</url>
<distribution>repo</distribution>
</license>
</licenses>
<repositories>
<repository>
<id>ZK CE</id>
<name>ZK CE Repository</name>
<url>https://mavensync.zkoss.org/maven2</url>
</repository>
<repository>
<id>ZK EVAL</id>
<name>ZK Evaluation Repository</name>
<url>https://mavensync.zkoss.org/eval</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>zkmaven</id>
<name>ZK Maven Plugin Repository</name>
<url>https://mavensync.zkoss.org/maven2/</url>
</pluginRepository>
</pluginRepositories>
<dependencies>
<dependency>
<groupId>org.zkoss.zk</groupId>
<artifactId>zkbind</artifactId>
<version>${zk.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava-testlib</artifactId>
<version>${guava.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<!-- Run with Jetty -->
<plugin>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
<version>${jetty-maven-plugin.version}</version>
<configuration>
<webApp>
<contextPath>/${project.artifactId}</contextPath>
<allowDuplicateFragmentNames>true</allowDuplicateFragmentNames>
</webApp>
<scanIntervalSeconds>5</scanIntervalSeconds>
</configuration>
</plugin>
<!-- Build war -->
<plugin>
<artifactId>maven-war-plugin</artifactId>
<groupId>org.apache.maven.plugins</groupId>
<version>${maven-war-plugin.version}</version>
</plugin>
<!-- Pack zips -->
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>${maven-assembly-plugin.version}</version>
<executions>
<execution>
<id>webapp</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<finalName>model-view-viewmodel${packname}</finalName>
<appendAssemblyId>false</appendAssemblyId>
<descriptors>
<descriptor>src/main/assembly/webapp.xml</descriptor>
</descriptors>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,25 @@
<assembly
xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd">
<id>webapp</id>
<formats>
<format>zip</format>
</formats>
<fileSets>
<fileSet>
<directory>${project.basedir}/src/main/java</directory>
<outputDirectory>/${project.artifactId}/src</outputDirectory>
</fileSet>
<fileSet>
<directory>${project.basedir}/src/main/webapp</directory>
<outputDirectory>/${project.artifactId}/WebContent</outputDirectory>
</fileSet>
</fileSets>
<files>
<file>
<source>${project.build.directory}/${project.artifactId}.war</source>
<outputDirectory>/</outputDirectory>
</file>
</files>
</assembly>

View File

@ -21,13 +21,17 @@
* THE SOFTWARE.
*/
package com.iluwatar.business.delegate;
package com.iluwatar.model.view.viewmodel;
/**
* Enumeration for service types.
*/
public enum ServiceType {
import lombok.AllArgsConstructor;
import lombok.Data;
@AllArgsConstructor
@Data
public class Book {
private String name;
private String author;
private String description;
EJB,
JMS
}

View File

@ -0,0 +1,10 @@
package com.iluwatar.model.view.viewmodel;
import java.util.List;
public interface BookService {
/* List all books
* @return all books
*/
public List<Book> load();
}

View File

@ -0,0 +1,38 @@
package com.iluwatar.model.view.viewmodel;
import java.util.ArrayList;
import java.util.List;
public class BookServiceImpl implements BookService {
private List<Book> designPatternBooks = new ArrayList<>();
/** Initializes Book Data.
* To be used and passed along in load method
* In this case, list design pattern books are initialized to be loaded.
*/
public BookServiceImpl() {
designPatternBooks.add(new Book(
"Head First Design Patterns: A Brain-Friendly Guide",
"Eric Freeman, Bert Bates, Kathy Sierra, Elisabeth Robson",
"Head First Design Patterns Description"));
designPatternBooks.add(new Book(
"Design Patterns: Elements of Reusable Object-Oriented Software",
"Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides",
"Design Patterns Description"));
designPatternBooks.add(new Book(
"Patterns of Enterprise Application Architecture", "Martin Fowler",
"Patterns of Enterprise Application Architecture Description"));
designPatternBooks.add(new Book(
"Design Patterns Explained", "Alan Shalloway, James Trott",
"Design Patterns Explained Description"));
designPatternBooks.add(new Book(
"Applying UML and Patterns: An Introduction to "
+ "Object-Oriented Analysis and Design and Iterative Development",
"Craig Larman", "Applying UML and Patterns Description"));
}
public List<Book> load() {
return designPatternBooks;
}
}

View File

@ -0,0 +1,43 @@
package com.iluwatar.model.view.viewmodel;
import java.util.List;
import org.zkoss.bind.annotation.Command;
import org.zkoss.bind.annotation.NotifyChange;
import org.zkoss.zk.ui.select.annotation.WireVariable;
public class BookViewModel {
@WireVariable
private List<Book> bookList;
private Book selectedBook;
private BookService bookService = new BookServiceImpl();
public Book getSelectedBook() {
return selectedBook;
}
@NotifyChange("selectedBook")
public void setSelectedBook(Book selectedBook) {
this.selectedBook = selectedBook;
}
public List<Book> getBookList() {
return bookService.load();
}
/** Deleting a book.
* When event is triggered on click of Delete button,
* this method will be notified with the selected entry that will be referenced
* and used to delete the selected book from the list of books.
*/
@Command
@NotifyChange({"selectedBook","bookList"})
public void deleteBook() {
if (selectedBook != null) {
getBookList().remove(selectedBook);
selectedBook = null;
}
}
}

View File

@ -0,0 +1,8 @@
Manifest-Version: 1.0
Specification-Title: ZK Application
Specification-Version: 1.0
Specification-Vendor:
Implementation-Title:
Implementation-URL: http://your-website/
Implementation-Version: 1.0
Implementation-Vendor:

View File

@ -0,0 +1,114 @@
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<description><![CDATA[My ZK Application]]></description>
<display-name>model-view-viewmodel</display-name>
<!-- ZK -->
<listener>
<description>ZK listener for session cleanup</description>
<listener-class>org.zkoss.zk.ui.http.HttpSessionListener</listener-class>
</listener>
<servlet>
<description>ZK loader for ZUML pages</description>
<servlet-name>zkLoader</servlet-name>
<servlet-class>org.zkoss.zk.ui.http.DHtmlLayoutServlet</servlet-class>
<!-- Must. Specifies URI of the update engine (DHtmlUpdateServlet).
It must be the same as <url-pattern> for the update engine.
-->
<init-param>
<param-name>update-uri</param-name>
<param-value>/zkau</param-value>
</init-param>
<load-on-startup>1</load-on-startup><!-- Must -->
</servlet>
<servlet-mapping>
<servlet-name>zkLoader</servlet-name>
<url-pattern>*.zul</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>zkLoader</servlet-name>
<url-pattern>*.zhtml</url-pattern>
</servlet-mapping>
<servlet>
<description>The asynchronous update engine for ZK</description>
<servlet-name>auEngine</servlet-name>
<servlet-class>org.zkoss.zk.au.http.DHtmlUpdateServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>auEngine</servlet-name>
<url-pattern>/zkau/*</url-pattern>
</servlet-mapping>
<session-config>
<session-timeout>60</session-timeout>
</session-config>
<!-- [Optional] MIME mapping -->
<mime-mapping>
<extension>doc</extension>
<mime-type>application/vnd.ms-word</mime-type>
</mime-mapping>
<mime-mapping>
<extension>gif</extension>
<mime-type>image/gif</mime-type>
</mime-mapping>
<mime-mapping>
<extension>htm</extension>
<mime-type>text/html</mime-type>
</mime-mapping>
<mime-mapping>
<extension>html</extension>
<mime-type>text/html</mime-type>
</mime-mapping>
<mime-mapping>
<extension>jpeg</extension>
<mime-type>image/jpeg</mime-type>
</mime-mapping>
<mime-mapping>
<extension>jpg</extension>
<mime-type>image/jpeg</mime-type>
</mime-mapping>
<mime-mapping>
<extension>js</extension>
<mime-type>text/javascript</mime-type>
</mime-mapping>
<mime-mapping>
<extension>pdf</extension>
<mime-type>application/pdf</mime-type>
</mime-mapping>
<mime-mapping>
<extension>png</extension>
<mime-type>image/png</mime-type>
</mime-mapping>
<mime-mapping>
<extension>txt</extension>
<mime-type>text/plain</mime-type>
</mime-mapping>
<mime-mapping>
<extension>xls</extension>
<mime-type>application/vnd.ms-excel</mime-type>
</mime-mapping>
<mime-mapping>
<extension>xml</extension>
<mime-type>text/xml</mime-type>
</mime-mapping>
<mime-mapping>
<extension>zhtml</extension>
<mime-type>text/html</mime-type>
</mime-mapping>
<mime-mapping>
<extension>zul</extension>
<mime-type>text/html</mime-type>
</mime-mapping>
<welcome-file-list>
<welcome-file>index.zul</welcome-file>
<welcome-file>index.zhtml</welcome-file>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
</welcome-file-list>
</web-app>

View File

@ -0,0 +1,34 @@
<zk>
<window title="List of Books" border="normal" width="600px" apply="org.zkoss.bind.BindComposer" viewModel="@id('vm') @init('com.iluwatar.model.view.viewmodel.BookViewModel')">
<vbox hflex="true">
<listbox model="@bind(vm.bookList)" selectedItem="@bind(vm.selectedBook)" height="400px" mold="paging">
<listhead>
<listheader label="Book Name"/>
<listheader label="Author"/>
</listhead>
<template name="model" var="book">
<listitem >
<listcell label="@bind(book.name)"/>
<listcell label="@bind(book.author)"/>
</listitem>
</template>
</listbox>
</vbox>
<toolbar>
        <button label="Delete" onClick="@command('deleteBook')" disabled="@load(empty vm.selectedBook)" />
    </toolbar>
<hbox style="margin-top:20px" visible="@bind(not empty vm.selectedBook)">
<vbox>
<hlayout>
Book Name : <label value="@bind(vm.selectedBook.name)" style="font-weight:bold"/>
</hlayout>
<hlayout>
Book Author : <label value="@bind(vm.selectedBook.author)" style="font-weight:bold"/>
</hlayout>
<hlayout>
Book Description : <label value="@bind(vm.selectedBook.description)" style="font-weight:bold"/>
</hlayout>
</vbox>
</hbox>
</window>
</zk>

View File

@ -0,0 +1,108 @@
/*
* The MIT License
* Copyright © 2014-2021 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.model.view.viewmodel;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.junit.Assert.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import com.google.common.testing.EqualsTester;
class BookTest {
BookViewModel bvm;
Book testBook;
List<Book> testBookList;
Book testBookTwo;
Book testBookThree;
@BeforeEach
void setUp() {
bvm = new BookViewModel();
testBook = new Book("Head First Design Patterns: A Brain-Friendly Guide",
"Eric Freeman, Bert Bates, Kathy Sierra, Elisabeth Robson",
"Head First Design Patterns Description");
testBookList = bvm.getBookList();
testBookTwo = new Book("Head First Design Patterns: A Brain-Friendly Guide",
"Eric Freeman, Bert Bates, Kathy Sierra, Elisabeth Robson",
"Head First Design Patterns Description");
testBookThree = new Book("Design Patterns: Elements of Reusable Object-Oriented Software",
"Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides",
"Design Patterns Description");
}
@Test
void testBookModel() {
assertNotNull(testBook);
}
@Test
void testEquals() {
new EqualsTester().addEqualityGroup(testBook, testBookTwo).testEquals();
}
@Test
void testToString() {
assertThat(testBook.toString(), is(testBookTwo.toString()));
assertThat(testBook.toString(), is(not(testBookThree.toString())));
}
@Test
void testHashCode() {
assertTrue(testBook.equals(testBookTwo) && testBookTwo.equals(testBook));
assertEquals(testBook.hashCode(), testBookTwo.hashCode());
}
@Test
void testLoadData() {
assertNotNull(testBookList);
assertTrue(testBookList.get(0).toString().contains("Head First Design Patterns"));
}
@Test
void testSelectedData() {
bvm.setSelectedBook(testBook);
assertNotNull(bvm.getSelectedBook());
assertEquals(testBook.toString(), bvm.getSelectedBook().toString());
assertTrue(true, bvm.getSelectedBook().toString());
}
@Test
void testDeleteData() {
bvm.setSelectedBook(testBook);
assertNotNull(bvm.getSelectedBook());
assertTrue(testBookList.get(0).toString().contains("Head First Design Patterns"));
bvm.deleteBook();
assertNull(bvm.getSelectedBook());
assertFalse(testBookList.get(0).toString().contains("Head First Design Patterns"));
}
}

10
pom.xml
View File

@ -75,6 +75,7 @@
<directory-maven-plugin.version>0.3.1</directory-maven-plugin.version>
<license-maven-plugin.version>3.0</license-maven-plugin.version>
<urm-maven-plugin.version>1.4.8</urm-maven-plugin.version>
<commons-io.version>2.6</commons-io.version>
<!-- SonarCloud -->
<sonar.host.url>https://sonarcloud.io</sonar.host.url>
@ -222,6 +223,7 @@
<module>special-case</module>
<module>parameter-object</module>
<module>active-object</module>
<module>model-view-viewmodel</module>
</modules>
<repositories>
@ -369,9 +371,13 @@
<version>${system-lambda.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${commons-io.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>