Compare commits
46 Commits
all-contri
...
all-contri
Author | SHA1 | Date | |
---|---|---|---|
|
4b18e223cd | ||
|
13c6de036f | ||
|
6f979d0cb2 | ||
|
f084f8bf41 | ||
|
6c95868b8d | ||
|
4c7f1b7822 | ||
|
0c44b53909 | ||
|
9ead3adf73 | ||
|
97e3a3debc | ||
|
43ed09015d | ||
|
7eee546208 | ||
|
dc31960710 | ||
|
7c0fdad5a2 | ||
|
5a8933ea17 | ||
|
d02233f0b7 | ||
|
d42bcab9fc | ||
|
77b2ff2150 | ||
|
cff072d1ca | ||
|
80ba0407db | ||
|
6a09b909f2 | ||
|
53ccc0e7e6 | ||
|
a7a8e23b01 | ||
|
2783251d00 | ||
|
ac432968ae | ||
|
f006782805 | ||
|
2332520d67 | ||
|
af1b611136 | ||
|
4ff196ce35 | ||
|
242ae6a412 | ||
|
b689fe0a26 | ||
|
7aea765dd1 | ||
|
ea49cbfe94 | ||
|
1f4a412e70 | ||
|
36d0a3718c | ||
|
195a735814 | ||
|
633d45aa67 | ||
|
adc267e48e | ||
|
cc8d209c22 | ||
|
a3753807ae | ||
|
64266d63fd | ||
|
4f62070eb2 | ||
|
b29bd66369 | ||
|
ad435dd2fd | ||
|
dac8e659ce | ||
|
a1515b3b52 | ||
|
93aa1046aa |
@@ -1222,6 +1222,51 @@
|
|||||||
"contributions": [
|
"contributions": [
|
||||||
"doc"
|
"doc"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "Ascenio",
|
||||||
|
"name": "Ascênio",
|
||||||
|
"avatar_url": "https://avatars1.githubusercontent.com/u/7662016?v=4",
|
||||||
|
"profile": "https://github.com/Ascenio",
|
||||||
|
"contributions": [
|
||||||
|
"review"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "dsibilio",
|
||||||
|
"name": "Domenico Sibilio",
|
||||||
|
"avatar_url": "https://avatars2.githubusercontent.com/u/24280982?v=4",
|
||||||
|
"profile": "https://www.linkedin.com/in/domenico-sibilio/",
|
||||||
|
"contributions": [
|
||||||
|
"doc"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "akashchandwani",
|
||||||
|
"name": "Akash Chandwani",
|
||||||
|
"avatar_url": "https://avatars2.githubusercontent.com/u/3483277?v=4",
|
||||||
|
"profile": "https://github.com/akashchandwani",
|
||||||
|
"contributions": [
|
||||||
|
"review"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "manannikov",
|
||||||
|
"name": "Pavlo Manannikov",
|
||||||
|
"avatar_url": "https://avatars2.githubusercontent.com/u/7019769?v=4",
|
||||||
|
"profile": "http://www.linkedin.com/in/manannikov",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "eimanip",
|
||||||
|
"name": "Eiman",
|
||||||
|
"avatar_url": "https://avatars0.githubusercontent.com/u/20307301?v=4",
|
||||||
|
"profile": "https://github.com/eimanip",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"contributorsPerLine": 4,
|
"contributorsPerLine": 4,
|
||||||
|
14
.github/workflows/maven-ci.yml
vendored
@@ -30,29 +30,43 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches: [ master ]
|
branches: [ master ]
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
build:
|
build:
|
||||||
|
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
# Disabling shallow clone for improving relevancy of SonarQube reporting
|
# Disabling shallow clone for improving relevancy of SonarQube reporting
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up JDK 11
|
- name: Set up JDK 11
|
||||||
uses: actions/setup-java@v1
|
uses: actions/setup-java@v1
|
||||||
with:
|
with:
|
||||||
java-version: 11
|
java-version: 11
|
||||||
|
|
||||||
|
- name: Cache SonarCloud packages
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: ~/.sonar/cache
|
||||||
|
key: ${{ runner.os }}-sonar
|
||||||
|
restore-keys: ${{ runner.os }}-sonar
|
||||||
|
|
||||||
- uses: actions/cache@v2
|
- uses: actions/cache@v2
|
||||||
with:
|
with:
|
||||||
path: ~/.m2/repository
|
path: ~/.m2/repository
|
||||||
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
|
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-maven-
|
${{ runner.os }}-maven-
|
||||||
|
|
||||||
# Some tests need screen access
|
# Some tests need screen access
|
||||||
- name: Install xvfb
|
- name: Install xvfb
|
||||||
run: sudo apt-get install -y xvfb
|
run: sudo apt-get install -y xvfb
|
||||||
|
|
||||||
# The SonarQube analysis is only for the master branch of the main repository.
|
# The SonarQube analysis is only for the master branch of the main repository.
|
||||||
# SonarQube scan does not work for forked repositories try changing it to xvfb-run mvn clean verify
|
# SonarQube scan does not work for forked repositories try changing it to xvfb-run mvn clean verify
|
||||||
# See https://jira.sonarsource.com/browse/MMF-1371
|
# See https://jira.sonarsource.com/browse/MMF-1371
|
||||||
|
11
README.md
@@ -10,7 +10,7 @@
|
|||||||
[](https://sonarcloud.io/dashboard?id=iluwatar_java-design-patterns)
|
[](https://sonarcloud.io/dashboard?id=iluwatar_java-design-patterns)
|
||||||
[](https://gitter.im/iluwatar/java-design-patterns?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
[](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-BADGE:START - Do not remove or modify this section -->
|
||||||
[](#contributors-)
|
[](#contributors-)
|
||||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||||
|
|
||||||
# Introduction
|
# Introduction
|
||||||
@@ -31,7 +31,7 @@ This site showcases Java Design Patterns. The solutions have been developed by
|
|||||||
experienced programmers and architects from the open source community. The
|
experienced programmers and architects from the open source community. The
|
||||||
patterns can be browsed by their high level descriptions or by looking at their
|
patterns can be browsed by their high level descriptions or by looking at their
|
||||||
source code. The source code examples are well commented and can be thought as
|
source code. The source code examples are well commented and can be thought as
|
||||||
programming tutorials how to implement a specific pattern. We use the most
|
programming tutorials on how to implement a specific pattern. We use the most
|
||||||
popular battle-proven open source Java technologies.
|
popular battle-proven open source Java technologies.
|
||||||
|
|
||||||
Before you dive into the material, you should be familiar with various
|
Before you dive into the material, you should be familiar with various
|
||||||
@@ -270,6 +270,13 @@ This project is licensed under the terms of the MIT license.
|
|||||||
<tr>
|
<tr>
|
||||||
<td align="center"><a href="https://github.com/swarajsaaj"><img src="https://avatars2.githubusercontent.com/u/6285049?v=4" width="100px;" alt=""/><br /><sub><b>Swaraj</b></sub></a><br /><a href="https://github.com/iluwatar/java-design-patterns/commits?author=swarajsaaj" title="Code">💻</a></td>
|
<td align="center"><a href="https://github.com/swarajsaaj"><img src="https://avatars2.githubusercontent.com/u/6285049?v=4" width="100px;" alt=""/><br /><sub><b>Swaraj</b></sub></a><br /><a href="https://github.com/iluwatar/java-design-patterns/commits?author=swarajsaaj" title="Code">💻</a></td>
|
||||||
<td align="center"><a href="http://christophflick.de"><img src="https://avatars0.githubusercontent.com/u/4465376?v=4" width="100px;" alt=""/><br /><sub><b>Christoph Flick</b></sub></a><br /><a href="https://github.com/iluwatar/java-design-patterns/commits?author=ChFlick" title="Documentation">📖</a></td>
|
<td align="center"><a href="http://christophflick.de"><img src="https://avatars0.githubusercontent.com/u/4465376?v=4" width="100px;" alt=""/><br /><sub><b>Christoph Flick</b></sub></a><br /><a href="https://github.com/iluwatar/java-design-patterns/commits?author=ChFlick" title="Documentation">📖</a></td>
|
||||||
|
<td align="center"><a href="https://github.com/Ascenio"><img src="https://avatars1.githubusercontent.com/u/7662016?v=4" width="100px;" alt=""/><br /><sub><b>Ascênio</b></sub></a><br /><a href="https://github.com/iluwatar/java-design-patterns/pulls?q=is%3Apr+reviewed-by%3AAscenio" title="Reviewed Pull Requests">👀</a></td>
|
||||||
|
<td align="center"><a href="https://www.linkedin.com/in/domenico-sibilio/"><img src="https://avatars2.githubusercontent.com/u/24280982?v=4" width="100px;" alt=""/><br /><sub><b>Domenico Sibilio</b></sub></a><br /><a href="https://github.com/iluwatar/java-design-patterns/commits?author=dsibilio" title="Documentation">📖</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center"><a href="https://github.com/akashchandwani"><img src="https://avatars2.githubusercontent.com/u/3483277?v=4" width="100px;" alt=""/><br /><sub><b>Akash Chandwani</b></sub></a><br /><a href="https://github.com/iluwatar/java-design-patterns/pulls?q=is%3Apr+reviewed-by%3Aakashchandwani" title="Reviewed Pull Requests">👀</a></td>
|
||||||
|
<td align="center"><a href="http://www.linkedin.com/in/manannikov"><img src="https://avatars2.githubusercontent.com/u/7019769?v=4" width="100px;" alt=""/><br /><sub><b>Pavlo Manannikov</b></sub></a><br /><a href="https://github.com/iluwatar/java-design-patterns/commits?author=manannikov" title="Code">💻</a></td>
|
||||||
|
<td align="center"><a href="https://github.com/eimanip"><img src="https://avatars0.githubusercontent.com/u/20307301?v=4" width="100px;" alt=""/><br /><sub><b>Eiman</b></sub></a><br /><a href="https://github.com/iluwatar/java-design-patterns/commits?author=eimanip" title="Code">💻</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
@@ -19,12 +19,15 @@ cannot bring the whole application down, and we can reconnect to the service as
|
|||||||
|
|
||||||
Real world example
|
Real world example
|
||||||
|
|
||||||
> Imagine a web application that has both local files/images and remote database entries to serve.
|
> Imagine a web application that has both local files/images and remote services that are used for
|
||||||
> The database might not be responding due to a variety of reasons, so if the application keeps
|
> fetching data. These remote services may be either healthy and responsive at times, or may become
|
||||||
> trying to read from the database using multiple threads/processes, soon all of them will hang
|
> slow and unresponsive at some point of time due to variety of reasons. So if one of the remote
|
||||||
> causing our entire web application will crash. We should be able to detect this situation and show
|
> services is slow or not responding successfully, our application will try to fetch response from
|
||||||
> the user an appropriate message so that he/she can explore other parts of the app unaffected by
|
> the remote service using multiple threads/processes, soon all of them will hang (also called
|
||||||
> the database failure.
|
> [thread starvation](https://en.wikipedia.org/wiki/Starvation_(computer_science))) causing our entire web application to crash. We should be able to detect
|
||||||
|
> this situation and show the user an appropriate message so that he/she can explore other parts of
|
||||||
|
> the app unaffected by the remote service failure. Meanwhile, the other services that are working
|
||||||
|
> normally, should keep functioning unaffected by this failure.
|
||||||
|
|
||||||
In plain words
|
In plain words
|
||||||
|
|
||||||
@@ -52,40 +55,104 @@ In terms of code, the end user application is:
|
|||||||
|
|
||||||
```java
|
```java
|
||||||
public class App {
|
public class App {
|
||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(App.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(App.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Program entry point.
|
||||||
|
*
|
||||||
|
* @param args command line args
|
||||||
|
*/
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
var obj = new MonitoringService();
|
|
||||||
var circuitBreaker = new CircuitBreaker(3000, 1, 2000 * 1000 * 1000);
|
|
||||||
var serverStartTime = System.nanoTime();
|
var serverStartTime = System.nanoTime();
|
||||||
while (true) {
|
|
||||||
LOGGER.info(obj.localResourceResponse());
|
var delayedService = new DelayedRemoteService(serverStartTime, 5);
|
||||||
LOGGER.info(obj.remoteResourceResponse(circuitBreaker, serverStartTime));
|
var delayedServiceCircuitBreaker = new DefaultCircuitBreaker(delayedService, 3000, 2,
|
||||||
LOGGER.info(circuitBreaker.getState());
|
2000 * 1000 * 1000);
|
||||||
try {
|
|
||||||
Thread.sleep(5 * 1000);
|
var quickService = new QuickRemoteService();
|
||||||
} catch (InterruptedException e) {
|
var quickServiceCircuitBreaker = new DefaultCircuitBreaker(quickService, 3000, 2,
|
||||||
LOGGER.error(e.getMessage());
|
2000 * 1000 * 1000);
|
||||||
}
|
|
||||||
|
//Create an object of monitoring service which makes both local and remote calls
|
||||||
|
var monitoringService = new MonitoringService(delayedServiceCircuitBreaker,
|
||||||
|
quickServiceCircuitBreaker);
|
||||||
|
|
||||||
|
//Fetch response from local resource
|
||||||
|
LOGGER.info(monitoringService.localResourceResponse());
|
||||||
|
|
||||||
|
//Fetch response from delayed service 2 times, to meet the failure threshold
|
||||||
|
LOGGER.info(monitoringService.delayedServiceResponse());
|
||||||
|
LOGGER.info(monitoringService.delayedServiceResponse());
|
||||||
|
|
||||||
|
//Fetch current state of delayed service circuit breaker after crossing failure threshold limit
|
||||||
|
//which is OPEN now
|
||||||
|
LOGGER.info(delayedServiceCircuitBreaker.getState());
|
||||||
|
|
||||||
|
//Meanwhile, the delayed service is down, fetch response from the healthy quick service
|
||||||
|
LOGGER.info(monitoringService.quickServiceResponse());
|
||||||
|
LOGGER.info(quickServiceCircuitBreaker.getState());
|
||||||
|
|
||||||
|
//Wait for the delayed service to become responsive
|
||||||
|
try {
|
||||||
|
LOGGER.info("Waiting for delayed service to become responsive");
|
||||||
|
Thread.sleep(5000);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
//Check the state of delayed circuit breaker, should be HALF_OPEN
|
||||||
|
LOGGER.info(delayedServiceCircuitBreaker.getState());
|
||||||
|
|
||||||
|
//Fetch response from delayed service, which should be healthy by now
|
||||||
|
LOGGER.info(monitoringService.delayedServiceResponse());
|
||||||
|
//As successful response is fetched, it should be CLOSED again.
|
||||||
|
LOGGER.info(delayedServiceCircuitBreaker.getState());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The monitoring service:
|
The monitoring service:
|
||||||
|
|
||||||
``` java
|
```java
|
||||||
public class MonitoringService {
|
public class MonitoringService {
|
||||||
|
|
||||||
|
private final CircuitBreaker delayedService;
|
||||||
|
|
||||||
|
private final CircuitBreaker quickService;
|
||||||
|
|
||||||
|
public MonitoringService(CircuitBreaker delayedService, CircuitBreaker quickService) {
|
||||||
|
this.delayedService = delayedService;
|
||||||
|
this.quickService = quickService;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Assumption: Local service won't fail, no need to wrap it in a circuit breaker logic
|
||||||
public String localResourceResponse() {
|
public String localResourceResponse() {
|
||||||
return "Local Service is working";
|
return "Local Service is working";
|
||||||
}
|
}
|
||||||
|
|
||||||
public String remoteResourceResponse(CircuitBreaker circuitBreaker, long serverStartTime) {
|
/**
|
||||||
|
* Fetch response from the delayed service (with some simulated startup time).
|
||||||
|
*
|
||||||
|
* @return response string
|
||||||
|
*/
|
||||||
|
public String delayedServiceResponse() {
|
||||||
try {
|
try {
|
||||||
return circuitBreaker.call("delayedService", serverStartTime);
|
return this.delayedService.attemptRequest();
|
||||||
} catch (Exception e) {
|
} catch (RemoteServiceException e) {
|
||||||
|
return e.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches response from a healthy service without any failure.
|
||||||
|
*
|
||||||
|
* @return response string
|
||||||
|
*/
|
||||||
|
public String quickServiceResponse() {
|
||||||
|
try {
|
||||||
|
return this.quickService.attemptRequest();
|
||||||
|
} catch (RemoteServiceException e) {
|
||||||
return e.getMessage();
|
return e.getMessage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,72 +162,127 @@ As it can be seen, it does the call to get local resources directly, but it wrap
|
|||||||
remote (costly) service in a circuit breaker object, which prevents faults as follows:
|
remote (costly) service in a circuit breaker object, which prevents faults as follows:
|
||||||
|
|
||||||
```java
|
```java
|
||||||
public class CircuitBreaker {
|
public class DefaultCircuitBreaker implements CircuitBreaker {
|
||||||
|
|
||||||
private final long timeout;
|
private final long timeout;
|
||||||
private final long retryTimePeriod;
|
private final long retryTimePeriod;
|
||||||
|
private final RemoteService service;
|
||||||
long lastFailureTime;
|
long lastFailureTime;
|
||||||
|
private String lastFailureResponse;
|
||||||
int failureCount;
|
int failureCount;
|
||||||
private final int failureThreshold;
|
private final int failureThreshold;
|
||||||
private State state;
|
private State state;
|
||||||
private final long futureTime = 1000 * 1000 * 1000 * 1000;
|
private final long futureTime = 1000 * 1000 * 1000 * 1000;
|
||||||
|
|
||||||
CircuitBreaker(long timeout, int failureThreshold, long retryTimePeriod) {
|
/**
|
||||||
|
* Constructor to create an instance of Circuit Breaker.
|
||||||
|
*
|
||||||
|
* @param timeout Timeout for the API request. Not necessary for this simple example
|
||||||
|
* @param failureThreshold Number of failures we receive from the depended service before changing
|
||||||
|
* state to 'OPEN'
|
||||||
|
* @param retryTimePeriod Time period after which a new request is made to remote service for
|
||||||
|
* status check.
|
||||||
|
*/
|
||||||
|
DefaultCircuitBreaker(RemoteService serviceToCall, long timeout, int failureThreshold,
|
||||||
|
long retryTimePeriod) {
|
||||||
|
this.service = serviceToCall;
|
||||||
|
// We start in a closed state hoping that everything is fine
|
||||||
this.state = State.CLOSED;
|
this.state = State.CLOSED;
|
||||||
this.failureThreshold = failureThreshold;
|
this.failureThreshold = failureThreshold;
|
||||||
|
// Timeout for the API request.
|
||||||
|
// Used to break the calls made to remote resource if it exceeds the limit
|
||||||
this.timeout = timeout;
|
this.timeout = timeout;
|
||||||
this.retryTimePeriod = retryTimePeriod;
|
this.retryTimePeriod = retryTimePeriod;
|
||||||
|
//An absurd amount of time in future which basically indicates the last failure never happened
|
||||||
this.lastFailureTime = System.nanoTime() + futureTime;
|
this.lastFailureTime = System.nanoTime() + futureTime;
|
||||||
this.failureCount = 0;
|
this.failureCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void reset() {
|
// Reset everything to defaults
|
||||||
|
@Override
|
||||||
|
public void recordSuccess() {
|
||||||
this.failureCount = 0;
|
this.failureCount = 0;
|
||||||
this.lastFailureTime = System.nanoTime() + futureTime;
|
this.lastFailureTime = System.nanoTime() + futureTime;
|
||||||
this.state = State.CLOSED;
|
this.state = State.CLOSED;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void recordFailure() {
|
@Override
|
||||||
|
public void recordFailure(String response) {
|
||||||
failureCount = failureCount + 1;
|
failureCount = failureCount + 1;
|
||||||
this.lastFailureTime = System.nanoTime();
|
this.lastFailureTime = System.nanoTime();
|
||||||
|
// Cache the failure response for returning on open state
|
||||||
|
this.lastFailureResponse = response;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void setState() {
|
// Evaluate the current state based on failureThreshold, failureCount and lastFailureTime.
|
||||||
if (failureCount > failureThreshold) {
|
protected void evaluateState() {
|
||||||
|
if (failureCount >= failureThreshold) { //Then something is wrong with remote service
|
||||||
if ((System.nanoTime() - lastFailureTime) > retryTimePeriod) {
|
if ((System.nanoTime() - lastFailureTime) > retryTimePeriod) {
|
||||||
|
//We have waited long enough and should try checking if service is up
|
||||||
state = State.HALF_OPEN;
|
state = State.HALF_OPEN;
|
||||||
} else {
|
} else {
|
||||||
|
//Service would still probably be down
|
||||||
state = State.OPEN;
|
state = State.OPEN;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
//Everything is working fine
|
||||||
state = State.CLOSED;
|
state = State.CLOSED;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public String getState() {
|
public String getState() {
|
||||||
|
evaluateState();
|
||||||
return state.name();
|
return state.name();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setStateForBypass(State state) {
|
/**
|
||||||
|
* Break the circuit beforehand if it is known service is down Or connect the circuit manually if
|
||||||
|
* service comes online before expected.
|
||||||
|
*
|
||||||
|
* @param state State at which circuit is in
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void setState(State state) {
|
||||||
this.state = state;
|
this.state = state;
|
||||||
|
switch (state) {
|
||||||
|
case OPEN:
|
||||||
|
this.failureCount = failureThreshold;
|
||||||
|
this.lastFailureTime = System.nanoTime();
|
||||||
|
break;
|
||||||
|
case HALF_OPEN:
|
||||||
|
this.failureCount = failureThreshold;
|
||||||
|
this.lastFailureTime = System.nanoTime() - retryTimePeriod;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.failureCount = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public String call(String serviceToCall, long serverStartTime) throws Exception {
|
/**
|
||||||
setState();
|
* Executes service call.
|
||||||
|
*
|
||||||
|
* @return Value from the remote resource, stale response or a custom exception
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String attemptRequest() throws RemoteServiceException {
|
||||||
|
evaluateState();
|
||||||
if (state == State.OPEN) {
|
if (state == State.OPEN) {
|
||||||
return "This is stale response from API";
|
// return cached response if the circuit is in OPEN state
|
||||||
|
return this.lastFailureResponse;
|
||||||
} else {
|
} else {
|
||||||
if (serviceToCall.equals("delayedService")) {
|
// Make the API request if the circuit is not OPEN
|
||||||
var delayedService = new DelayedService(20);
|
try {
|
||||||
var response = delayedService.response(serverStartTime);
|
//In a real application, this would be run in a thread and the timeout
|
||||||
if (response.split(" ")[3].equals("working")) {
|
//parameter of the circuit breaker would be utilized to know if service
|
||||||
reset();
|
//is working. Here, we simulate that based on server response itself
|
||||||
return response;
|
var response = service.call();
|
||||||
} else {
|
// Yay!! the API responded fine. Let's reset everything.
|
||||||
recordFailure();
|
recordSuccess();
|
||||||
throw new Exception("Remote service not responding");
|
return response;
|
||||||
}
|
} catch (RemoteServiceException ex) {
|
||||||
} else {
|
recordFailure(ex.getMessage());
|
||||||
throw new Exception("Unknown Service Name");
|
throw ex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 79 KiB |
@@ -5,32 +5,52 @@ package com.iluwatar.circuitbreaker {
|
|||||||
+ App()
|
+ App()
|
||||||
+ main(args : String[]) {static}
|
+ main(args : String[]) {static}
|
||||||
}
|
}
|
||||||
class CircuitBreaker {
|
interface CircuitBreaker {
|
||||||
|
+ attemptRequest() : String {abstract}
|
||||||
|
+ getState() : String {abstract}
|
||||||
|
+ recordFailure(String) {abstract}
|
||||||
|
+ recordSuccess() {abstract}
|
||||||
|
+ setState(State) {abstract}
|
||||||
|
}
|
||||||
|
class DefaultCircuitBreaker {
|
||||||
~ failureCount : int
|
~ failureCount : int
|
||||||
- failureThreshold : int
|
- failureThreshold : int
|
||||||
- futureTime : long
|
- futureTime : long
|
||||||
|
- lastFailureResponse : String
|
||||||
~ lastFailureTime : long
|
~ lastFailureTime : long
|
||||||
- retryTimePeriod : long
|
- retryTimePeriod : long
|
||||||
|
- service : RemoteService
|
||||||
- state : State
|
- state : State
|
||||||
- timeout : long
|
- timeout : long
|
||||||
~ CircuitBreaker(timeout : long, failureThreshold : int, retryTimePeriod : long)
|
~ DefaultCircuitBreaker(serviceToCall : RemoteService, timeout : long, failureThreshold : int, retryTimePeriod : long)
|
||||||
+ call(serviceToCall : String, serverStartTime : long) : String
|
+ attemptRequest() : String
|
||||||
|
# evaluateState()
|
||||||
+ getState() : String
|
+ getState() : String
|
||||||
- recordFailure()
|
+ recordFailure(response : String)
|
||||||
- reset()
|
+ recordSuccess()
|
||||||
# setState()
|
+ setState(state : State)
|
||||||
+ setStateForBypass(state : State)
|
|
||||||
}
|
}
|
||||||
class DelayedService {
|
class DelayedRemoteService {
|
||||||
- delay : int
|
- delay : int
|
||||||
+ DelayedService()
|
- serverStartTime : long
|
||||||
+ DelayedService(delay : int)
|
+ DelayedRemoteService()
|
||||||
+ response(serverStartTime : long) : String
|
+ DelayedRemoteService(serverStartTime : long, delay : int)
|
||||||
|
+ call() : String
|
||||||
}
|
}
|
||||||
class MonitoringService {
|
class MonitoringService {
|
||||||
+ MonitoringService()
|
- delayedService : CircuitBreaker
|
||||||
|
- quickService : CircuitBreaker
|
||||||
|
+ MonitoringService(delayedService : CircuitBreaker, quickService : CircuitBreaker)
|
||||||
|
+ delayedServiceResponse() : String
|
||||||
+ localResourceResponse() : String
|
+ localResourceResponse() : String
|
||||||
+ remoteResourceResponse(circuitBreaker : CircuitBreaker, serverStartTime : long) : String
|
+ quickServiceResponse() : String
|
||||||
|
}
|
||||||
|
class QuickRemoteService {
|
||||||
|
+ QuickRemoteService()
|
||||||
|
+ call() : String
|
||||||
|
}
|
||||||
|
interface RemoteService {
|
||||||
|
+ call() : String {abstract}
|
||||||
}
|
}
|
||||||
enum State {
|
enum State {
|
||||||
+ CLOSED {static}
|
+ CLOSED {static}
|
||||||
@@ -40,5 +60,10 @@ package com.iluwatar.circuitbreaker {
|
|||||||
+ values() : State[] {static}
|
+ values() : State[] {static}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CircuitBreaker --> "-state" State
|
DefaultCircuitBreaker --> "-state" State
|
||||||
|
MonitoringService --> "-delayedService" CircuitBreaker
|
||||||
|
DefaultCircuitBreaker --> "-service" RemoteService
|
||||||
|
DefaultCircuitBreaker ..|> CircuitBreaker
|
||||||
|
DelayedRemoteService ..|> RemoteService
|
||||||
|
QuickRemoteService ..|> RemoteService
|
||||||
@enduml
|
@enduml
|
@@ -36,17 +36,18 @@ import org.slf4j.LoggerFactory;
|
|||||||
* operational again, so that we can use it
|
* operational again, so that we can use it
|
||||||
* </p>
|
* </p>
|
||||||
* <p>
|
* <p>
|
||||||
* In this example, the circuit breaker pattern is demonstrated by using two services: {@link
|
* In this example, the circuit breaker pattern is demonstrated by using three services: {@link
|
||||||
* MonitoringService} and {@link DelayedService}. The monitoring service is responsible for calling
|
* DelayedRemoteService}, {@link QuickRemoteService} and {@link MonitoringService}. The monitoring
|
||||||
* two services: a local service and a remote service {@link DelayedService} , and by using the
|
* service is responsible for calling three services: a local service, a quick remove service
|
||||||
* circuit breaker construction we ensure that if the call to remote service is going to fail, we
|
* {@link QuickRemoteService} and a delayed remote service {@link DelayedRemoteService} , and by
|
||||||
* are going to save our resources and not make the function call at all, by wrapping our call to
|
* using the circuit breaker construction we ensure that if the call to remote service is going to
|
||||||
* the remote service in the circuit breaker object.
|
* fail, we are going to save our resources and not make the function call at all, by wrapping our
|
||||||
|
* call to the remote services in the {@link DefaultCircuitBreaker} implementation object.
|
||||||
* </p>
|
* </p>
|
||||||
* <p>
|
* <p>
|
||||||
* This works as follows: The {@link CircuitBreaker} object can be in one of three states:
|
* This works as follows: The {@link DefaultCircuitBreaker} object can be in one of three states:
|
||||||
* <b>Open</b>, <b>Closed</b> and <b>Half-Open</b>, which represents the real world circuits. If the
|
* <b>Open</b>, <b>Closed</b> and <b>Half-Open</b>, which represents the real world circuits. If
|
||||||
* state is closed (initial), we assume everything is alright and perform the function call.
|
* the state is closed (initial), we assume everything is alright and perform the function call.
|
||||||
* However, every time the call fails, we note it and once it crosses a threshold, we set the state
|
* However, every time the call fails, we note it and once it crosses a threshold, we set the state
|
||||||
* to Open, preventing any further calls to the remote server. Then, after a certain retry period
|
* to Open, preventing any further calls to the remote server. Then, after a certain retry period
|
||||||
* (during which we expect thee service to recover), we make another call to the remote server and
|
* (during which we expect thee service to recover), we make another call to the remote server and
|
||||||
@@ -63,22 +64,50 @@ public class App {
|
|||||||
*
|
*
|
||||||
* @param args command line args
|
* @param args command line args
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("squid:S2189")
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
//Create an object of monitoring service which makes both local and remote calls
|
|
||||||
var obj = new MonitoringService();
|
|
||||||
//Set the circuit Breaker parameters
|
|
||||||
var circuitBreaker = new CircuitBreaker(3000, 1, 2000 * 1000 * 1000);
|
|
||||||
var serverStartTime = System.nanoTime();
|
var serverStartTime = System.nanoTime();
|
||||||
while (true) {
|
|
||||||
LOGGER.info(obj.localResourceResponse());
|
var delayedService = new DelayedRemoteService(serverStartTime, 5);
|
||||||
LOGGER.info(obj.remoteResourceResponse(circuitBreaker, serverStartTime));
|
var delayedServiceCircuitBreaker = new DefaultCircuitBreaker(delayedService, 3000, 2,
|
||||||
LOGGER.info(circuitBreaker.getState());
|
2000 * 1000 * 1000);
|
||||||
try {
|
|
||||||
Thread.sleep(5 * 1000);
|
var quickService = new QuickRemoteService();
|
||||||
} catch (InterruptedException e) {
|
var quickServiceCircuitBreaker = new DefaultCircuitBreaker(quickService, 3000, 2,
|
||||||
LOGGER.error(e.getMessage());
|
2000 * 1000 * 1000);
|
||||||
}
|
|
||||||
|
//Create an object of monitoring service which makes both local and remote calls
|
||||||
|
var monitoringService = new MonitoringService(delayedServiceCircuitBreaker,
|
||||||
|
quickServiceCircuitBreaker);
|
||||||
|
|
||||||
|
//Fetch response from local resource
|
||||||
|
LOGGER.info(monitoringService.localResourceResponse());
|
||||||
|
|
||||||
|
//Fetch response from delayed service 2 times, to meet the failure threshold
|
||||||
|
LOGGER.info(monitoringService.delayedServiceResponse());
|
||||||
|
LOGGER.info(monitoringService.delayedServiceResponse());
|
||||||
|
|
||||||
|
//Fetch current state of delayed service circuit breaker after crossing failure threshold limit
|
||||||
|
//which is OPEN now
|
||||||
|
LOGGER.info(delayedServiceCircuitBreaker.getState());
|
||||||
|
|
||||||
|
//Meanwhile, the delayed service is down, fetch response from the healthy quick service
|
||||||
|
LOGGER.info(monitoringService.quickServiceResponse());
|
||||||
|
LOGGER.info(quickServiceCircuitBreaker.getState());
|
||||||
|
|
||||||
|
//Wait for the delayed service to become responsive
|
||||||
|
try {
|
||||||
|
LOGGER.info("Waiting for delayed service to become responsive");
|
||||||
|
Thread.sleep(5000);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
//Check the state of delayed circuit breaker, should be HALF_OPEN
|
||||||
|
LOGGER.info(delayedServiceCircuitBreaker.getState());
|
||||||
|
|
||||||
|
//Fetch response from delayed service, which should be healthy by now
|
||||||
|
LOGGER.info(monitoringService.delayedServiceResponse());
|
||||||
|
//As successful response is fetched, it should be CLOSED again.
|
||||||
|
LOGGER.info(delayedServiceCircuitBreaker.getState());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -24,114 +24,22 @@
|
|||||||
package com.iluwatar.circuitbreaker;
|
package com.iluwatar.circuitbreaker;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The circuit breaker class with all configurations.
|
* The Circuit breaker interface.
|
||||||
*/
|
*/
|
||||||
public class CircuitBreaker {
|
public interface CircuitBreaker {
|
||||||
private final long timeout;
|
|
||||||
private final long retryTimePeriod;
|
|
||||||
long lastFailureTime;
|
|
||||||
int failureCount;
|
|
||||||
private final int failureThreshold;
|
|
||||||
private State state;
|
|
||||||
private final long futureTime = 1000 * 1000 * 1000 * 1000;
|
|
||||||
|
|
||||||
/**
|
// Success response. Reset everything to defaults
|
||||||
* Constructor to create an instance of Circuit Breaker.
|
void recordSuccess();
|
||||||
*
|
|
||||||
* @param timeout Timeout for the API request. Not necessary for this simple example
|
|
||||||
* @param failureThreshold Number of failures we receive from the depended service before changing
|
|
||||||
* state to 'OPEN'
|
|
||||||
* @param retryTimePeriod Time period after which a new request is made to remote service for
|
|
||||||
* status check.
|
|
||||||
*/
|
|
||||||
CircuitBreaker(long timeout, int failureThreshold, long retryTimePeriod) {
|
|
||||||
// We start in a closed state hoping that everything is fine
|
|
||||||
this.state = State.CLOSED;
|
|
||||||
this.failureThreshold = failureThreshold;
|
|
||||||
// Timeout for the API request.
|
|
||||||
// Used to break the calls made to remote resource if it exceeds the limit
|
|
||||||
this.timeout = timeout;
|
|
||||||
this.retryTimePeriod = retryTimePeriod;
|
|
||||||
//An absurd amount of time in future which basically indicates the last failure never happened
|
|
||||||
this.lastFailureTime = System.nanoTime() + futureTime;
|
|
||||||
this.failureCount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Reset everything to defaults
|
// Failure response. Handle accordingly with response and change state if required.
|
||||||
private void reset() {
|
void recordFailure(String response);
|
||||||
this.failureCount = 0;
|
|
||||||
this.lastFailureTime = System.nanoTime() + futureTime;
|
|
||||||
this.state = State.CLOSED;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void recordFailure() {
|
// Get the current state of circuit breaker
|
||||||
failureCount = failureCount + 1;
|
String getState();
|
||||||
this.lastFailureTime = System.nanoTime();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void setState() {
|
// Set the specific state manually.
|
||||||
if (failureCount > failureThreshold) { //Then something is wrong with remote service
|
void setState(State state);
|
||||||
if ((System.nanoTime() - lastFailureTime) > retryTimePeriod) {
|
|
||||||
//We have waited long enough and should try checking if service is up
|
|
||||||
state = State.HALF_OPEN;
|
|
||||||
} else {
|
|
||||||
//Service would still probably be down
|
|
||||||
state = State.OPEN;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
//Everything is working fine
|
|
||||||
state = State.CLOSED;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getState() {
|
// Attempt to fetch response from the remote service.
|
||||||
return state.name();
|
String attemptRequest() throws RemoteServiceException;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Break the circuit beforehand if it is known service is down Or connect the circuit manually if
|
|
||||||
* service comes online before expected.
|
|
||||||
*
|
|
||||||
* @param state State at which circuit is in
|
|
||||||
*/
|
|
||||||
public void setStateForBypass(State state) {
|
|
||||||
this.state = state;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Executes service call.
|
|
||||||
*
|
|
||||||
* @param serviceToCall The name of the service in String. Can be changed to data URLs in case
|
|
||||||
* of web applications
|
|
||||||
* @param serverStartTime Time at which actual server was started which makes calls to this
|
|
||||||
* service
|
|
||||||
* @return Value from the remote resource, stale response or a custom exception
|
|
||||||
*/
|
|
||||||
public String call(String serviceToCall, long serverStartTime) throws Exception {
|
|
||||||
setState();
|
|
||||||
if (state == State.OPEN) {
|
|
||||||
// return cached response if no the circuit is in OPEN state
|
|
||||||
return "This is stale response from API";
|
|
||||||
} else {
|
|
||||||
// Make the API request if the circuit is not OPEN
|
|
||||||
if (serviceToCall.equals("delayedService")) {
|
|
||||||
var delayedService = new DelayedService(20);
|
|
||||||
var response = delayedService.response(serverStartTime);
|
|
||||||
//In a real application, this would be run in a thread and the timeout
|
|
||||||
//parameter of the circuit breaker would be utilized to know if service
|
|
||||||
//is working. Here, we simulate that based on server response itself
|
|
||||||
if (response.split(" ")[3].equals("working")) {
|
|
||||||
// Yay!! the API responded fine. Let's reset everything.
|
|
||||||
reset();
|
|
||||||
return response;
|
|
||||||
} else {
|
|
||||||
// Uh-oh!! the call still failed. Let's update that in our records.
|
|
||||||
recordFailure();
|
|
||||||
throw new Exception("Remote service not responding");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Exception("Unknown Service Name");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@@ -0,0 +1,155 @@
|
|||||||
|
/*
|
||||||
|
* The MIT License
|
||||||
|
* Copyright © 2014-2019 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.circuitbreaker;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The delay based Circuit breaker implementation that works in a
|
||||||
|
* CLOSED->OPEN-(retry_time_period)->HALF_OPEN->CLOSED flow with some retry time period for failed
|
||||||
|
* services and a failure threshold for service to open circuit.
|
||||||
|
*/
|
||||||
|
public class DefaultCircuitBreaker implements CircuitBreaker {
|
||||||
|
|
||||||
|
private final long timeout;
|
||||||
|
private final long retryTimePeriod;
|
||||||
|
private final RemoteService service;
|
||||||
|
long lastFailureTime;
|
||||||
|
private String lastFailureResponse;
|
||||||
|
int failureCount;
|
||||||
|
private final int failureThreshold;
|
||||||
|
private State state;
|
||||||
|
private final long futureTime = 1000 * 1000 * 1000 * 1000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor to create an instance of Circuit Breaker.
|
||||||
|
*
|
||||||
|
* @param timeout Timeout for the API request. Not necessary for this simple example
|
||||||
|
* @param failureThreshold Number of failures we receive from the depended service before changing
|
||||||
|
* state to 'OPEN'
|
||||||
|
* @param retryTimePeriod Time period after which a new request is made to remote service for
|
||||||
|
* status check.
|
||||||
|
*/
|
||||||
|
DefaultCircuitBreaker(RemoteService serviceToCall, long timeout, int failureThreshold,
|
||||||
|
long retryTimePeriod) {
|
||||||
|
this.service = serviceToCall;
|
||||||
|
// We start in a closed state hoping that everything is fine
|
||||||
|
this.state = State.CLOSED;
|
||||||
|
this.failureThreshold = failureThreshold;
|
||||||
|
// Timeout for the API request.
|
||||||
|
// Used to break the calls made to remote resource if it exceeds the limit
|
||||||
|
this.timeout = timeout;
|
||||||
|
this.retryTimePeriod = retryTimePeriod;
|
||||||
|
//An absurd amount of time in future which basically indicates the last failure never happened
|
||||||
|
this.lastFailureTime = System.nanoTime() + futureTime;
|
||||||
|
this.failureCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset everything to defaults
|
||||||
|
@Override
|
||||||
|
public void recordSuccess() {
|
||||||
|
this.failureCount = 0;
|
||||||
|
this.lastFailureTime = System.nanoTime() + futureTime;
|
||||||
|
this.state = State.CLOSED;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void recordFailure(String response) {
|
||||||
|
failureCount = failureCount + 1;
|
||||||
|
this.lastFailureTime = System.nanoTime();
|
||||||
|
// Cache the failure response for returning on open state
|
||||||
|
this.lastFailureResponse = response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evaluate the current state based on failureThreshold, failureCount and lastFailureTime.
|
||||||
|
protected void evaluateState() {
|
||||||
|
if (failureCount >= failureThreshold) { //Then something is wrong with remote service
|
||||||
|
if ((System.nanoTime() - lastFailureTime) > retryTimePeriod) {
|
||||||
|
//We have waited long enough and should try checking if service is up
|
||||||
|
state = State.HALF_OPEN;
|
||||||
|
} else {
|
||||||
|
//Service would still probably be down
|
||||||
|
state = State.OPEN;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//Everything is working fine
|
||||||
|
state = State.CLOSED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getState() {
|
||||||
|
evaluateState();
|
||||||
|
return state.name();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Break the circuit beforehand if it is known service is down Or connect the circuit manually if
|
||||||
|
* service comes online before expected.
|
||||||
|
*
|
||||||
|
* @param state State at which circuit is in
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void setState(State state) {
|
||||||
|
this.state = state;
|
||||||
|
switch (state) {
|
||||||
|
case OPEN:
|
||||||
|
this.failureCount = failureThreshold;
|
||||||
|
this.lastFailureTime = System.nanoTime();
|
||||||
|
break;
|
||||||
|
case HALF_OPEN:
|
||||||
|
this.failureCount = failureThreshold;
|
||||||
|
this.lastFailureTime = System.nanoTime() - retryTimePeriod;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.failureCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes service call.
|
||||||
|
*
|
||||||
|
* @return Value from the remote resource, stale response or a custom exception
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String attemptRequest() throws RemoteServiceException {
|
||||||
|
evaluateState();
|
||||||
|
if (state == State.OPEN) {
|
||||||
|
// return cached response if the circuit is in OPEN state
|
||||||
|
return this.lastFailureResponse;
|
||||||
|
} else {
|
||||||
|
// Make the API request if the circuit is not OPEN
|
||||||
|
try {
|
||||||
|
//In a real application, this would be run in a thread and the timeout
|
||||||
|
//parameter of the circuit breaker would be utilized to know if service
|
||||||
|
//is working. Here, we simulate that based on server response itself
|
||||||
|
var response = service.call();
|
||||||
|
// Yay!! the API responded fine. Let's reset everything.
|
||||||
|
recordSuccess();
|
||||||
|
return response;
|
||||||
|
} catch (RemoteServiceException ex) {
|
||||||
|
recordFailure(ex.getMessage());
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -27,7 +27,9 @@ package com.iluwatar.circuitbreaker;
|
|||||||
* This simulates the remote service It responds only after a certain timeout period (default set to
|
* This simulates the remote service It responds only after a certain timeout period (default set to
|
||||||
* 20 seconds).
|
* 20 seconds).
|
||||||
*/
|
*/
|
||||||
public class DelayedService {
|
public class DelayedRemoteService implements RemoteService {
|
||||||
|
|
||||||
|
private final long serverStartTime;
|
||||||
private final int delay;
|
private final int delay;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,22 +37,23 @@ public class DelayedService {
|
|||||||
*
|
*
|
||||||
* @param delay the delay after which service would behave properly, in seconds
|
* @param delay the delay after which service would behave properly, in seconds
|
||||||
*/
|
*/
|
||||||
public DelayedService(int delay) {
|
public DelayedRemoteService(long serverStartTime, int delay) {
|
||||||
|
this.serverStartTime = serverStartTime;
|
||||||
this.delay = delay;
|
this.delay = delay;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DelayedService() {
|
public DelayedRemoteService() {
|
||||||
this.delay = 60;
|
this.serverStartTime = System.nanoTime();
|
||||||
|
this.delay = 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Responds based on delay, current time and server start time if the service is down / working.
|
* Responds based on delay, current time and server start time if the service is down / working.
|
||||||
*
|
*
|
||||||
* @param serverStartTime Time at which actual server was started which makes calls to this
|
|
||||||
* service
|
|
||||||
* @return The state of the service
|
* @return The state of the service
|
||||||
*/
|
*/
|
||||||
public String response(long serverStartTime) {
|
@Override
|
||||||
|
public String call() throws RemoteServiceException {
|
||||||
var currentTime = System.nanoTime();
|
var currentTime = System.nanoTime();
|
||||||
//Since currentTime and serverStartTime are both in nanoseconds, we convert it to
|
//Since currentTime and serverStartTime are both in nanoseconds, we convert it to
|
||||||
//seconds by diving by 10e9 and ensure floating point division by multiplying it
|
//seconds by diving by 10e9 and ensure floating point division by multiplying it
|
||||||
@@ -58,9 +61,8 @@ public class DelayedService {
|
|||||||
//send the reply
|
//send the reply
|
||||||
if ((currentTime - serverStartTime) * 1.0 / (1000 * 1000 * 1000) < delay) {
|
if ((currentTime - serverStartTime) * 1.0 / (1000 * 1000 * 1000) < delay) {
|
||||||
//Can use Thread.sleep() here to block and simulate a hung server
|
//Can use Thread.sleep() here to block and simulate a hung server
|
||||||
return "Delayed service is down";
|
throw new RemoteServiceException("Delayed service is down");
|
||||||
} else {
|
|
||||||
return "Delayed service is working";
|
|
||||||
}
|
}
|
||||||
|
return "Delayed service is working";
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -24,28 +24,47 @@
|
|||||||
package com.iluwatar.circuitbreaker;
|
package com.iluwatar.circuitbreaker;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The service class which makes local and remote calls Uses {@link CircuitBreaker} object to ensure
|
* The service class which makes local and remote calls Uses {@link DefaultCircuitBreaker} object to
|
||||||
* remote calls don't use up resources.
|
* ensure remote calls don't use up resources.
|
||||||
*/
|
*/
|
||||||
public class MonitoringService {
|
public class MonitoringService {
|
||||||
|
|
||||||
|
private final CircuitBreaker delayedService;
|
||||||
|
|
||||||
|
private final CircuitBreaker quickService;
|
||||||
|
|
||||||
|
public MonitoringService(CircuitBreaker delayedService, CircuitBreaker quickService) {
|
||||||
|
this.delayedService = delayedService;
|
||||||
|
this.quickService = quickService;
|
||||||
|
}
|
||||||
|
|
||||||
//Assumption: Local service won't fail, no need to wrap it in a circuit breaker logic
|
//Assumption: Local service won't fail, no need to wrap it in a circuit breaker logic
|
||||||
public String localResourceResponse() {
|
public String localResourceResponse() {
|
||||||
return "Local Service is working";
|
return "Local Service is working";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Try to get result from remote server.
|
* Fetch response from the delayed service (with some simulated startup time).
|
||||||
*
|
*
|
||||||
* @param circuitBreaker The circuitBreaker object with all parameters
|
* @return response string
|
||||||
* @param serverStartTime Time at which actual server was started which makes calls to this
|
|
||||||
* service
|
|
||||||
* @return result from the remote response or exception raised by it.
|
|
||||||
*/
|
*/
|
||||||
public String remoteResourceResponse(CircuitBreaker circuitBreaker, long serverStartTime) {
|
public String delayedServiceResponse() {
|
||||||
try {
|
try {
|
||||||
return circuitBreaker.call("delayedService", serverStartTime);
|
return this.delayedService.attemptRequest();
|
||||||
} catch (Exception e) {
|
} catch (RemoteServiceException e) {
|
||||||
|
return e.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches response from a healthy service without any failure.
|
||||||
|
*
|
||||||
|
* @return response string
|
||||||
|
*/
|
||||||
|
public String quickServiceResponse() {
|
||||||
|
try {
|
||||||
|
return this.quickService.attemptRequest();
|
||||||
|
} catch (RemoteServiceException e) {
|
||||||
return e.getMessage();
|
return e.getMessage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -23,19 +23,13 @@
|
|||||||
|
|
||||||
package com.iluwatar.circuitbreaker;
|
package com.iluwatar.circuitbreaker;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Monitoring Service test
|
* A quick response remote service, that responds healthy without any delay or failure.
|
||||||
*/
|
*/
|
||||||
public class DelayedServiceTest {
|
public class QuickRemoteService implements RemoteService {
|
||||||
|
|
||||||
//Improves code coverage
|
@Override
|
||||||
@Test
|
public String call() throws RemoteServiceException {
|
||||||
public void testDefaultConstructor() {
|
return "Quick Service is working";
|
||||||
var obj = new DelayedService();
|
|
||||||
assertEquals(obj.response(System.nanoTime()), "Delayed service is down");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* The MIT License
|
||||||
|
* Copyright © 2014-2019 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.circuitbreaker;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Remote service interface, used by {@link CircuitBreaker} for fetching response from remote
|
||||||
|
* services.
|
||||||
|
*/
|
||||||
|
public interface RemoteService {
|
||||||
|
|
||||||
|
//Fetch response from remote service.
|
||||||
|
String call() throws RemoteServiceException;
|
||||||
|
}
|
@@ -21,39 +21,14 @@
|
|||||||
* THE SOFTWARE.
|
* THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.iluwatar.command;
|
package com.iluwatar.circuitbreaker;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ShrinkSpell is a concrete command.
|
* Exception thrown when {@link RemoteService} does not respond successfully.
|
||||||
*/
|
*/
|
||||||
public class ShrinkSpell implements Command {
|
public class RemoteServiceException extends Exception {
|
||||||
|
|
||||||
private Size oldSize;
|
public RemoteServiceException(String message) {
|
||||||
private Target target;
|
super(message);
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(Target target) {
|
|
||||||
oldSize = target.getSize();
|
|
||||||
target.setSize(Size.SMALL);
|
|
||||||
this.target = target;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void undo() {
|
|
||||||
if (oldSize != null && target != null) {
|
|
||||||
var temp = target.getSize();
|
|
||||||
target.setSize(oldSize);
|
|
||||||
oldSize = temp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void redo() {
|
|
||||||
undo();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "Shrink spell";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -0,0 +1,135 @@
|
|||||||
|
/*
|
||||||
|
* The MIT License
|
||||||
|
* Copyright © 2014-2019 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.circuitbreaker;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* App Test showing usage of circuit breaker.
|
||||||
|
*/
|
||||||
|
public class AppTest {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(AppTest.class);
|
||||||
|
|
||||||
|
//Startup delay for delayed service (in seconds)
|
||||||
|
private static final int STARTUP_DELAY = 4;
|
||||||
|
|
||||||
|
//Number of failed requests for circuit breaker to open
|
||||||
|
private static final int FAILURE_THRESHOLD = 1;
|
||||||
|
|
||||||
|
//Time period in seconds for circuit breaker to retry service
|
||||||
|
private static final int RETRY_PERIOD = 2;
|
||||||
|
|
||||||
|
private MonitoringService monitoringService;
|
||||||
|
|
||||||
|
private CircuitBreaker delayedServiceCircuitBreaker;
|
||||||
|
|
||||||
|
private CircuitBreaker quickServiceCircuitBreaker;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup the circuit breakers and services, where {@link DelayedRemoteService} will be start with
|
||||||
|
* a delay of 4 seconds and a {@link QuickRemoteService} responding healthy. Both services are
|
||||||
|
* wrapped in a {@link DefaultCircuitBreaker} implementation with failure threshold of 1 failure
|
||||||
|
* and retry time period of 2 seconds.
|
||||||
|
*/
|
||||||
|
@BeforeEach
|
||||||
|
public void setupCircuitBreakers() {
|
||||||
|
var delayedService = new DelayedRemoteService(System.nanoTime(), STARTUP_DELAY);
|
||||||
|
//Set the circuit Breaker parameters
|
||||||
|
delayedServiceCircuitBreaker = new DefaultCircuitBreaker(delayedService, 3000,
|
||||||
|
FAILURE_THRESHOLD,
|
||||||
|
RETRY_PERIOD * 1000 * 1000 * 1000);
|
||||||
|
|
||||||
|
var quickService = new QuickRemoteService();
|
||||||
|
//Set the circuit Breaker parameters
|
||||||
|
quickServiceCircuitBreaker = new DefaultCircuitBreaker(quickService, 3000, FAILURE_THRESHOLD,
|
||||||
|
RETRY_PERIOD * 1000 * 1000 * 1000);
|
||||||
|
|
||||||
|
monitoringService = new MonitoringService(delayedServiceCircuitBreaker,
|
||||||
|
quickServiceCircuitBreaker);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFailure_OpenStateTransition() {
|
||||||
|
//Calling delayed service, which will be unhealthy till 4 seconds
|
||||||
|
assertEquals("Delayed service is down", monitoringService.delayedServiceResponse());
|
||||||
|
//As failure threshold is "1", the circuit breaker is changed to OPEN
|
||||||
|
assertEquals("OPEN", delayedServiceCircuitBreaker.getState());
|
||||||
|
//As circuit state is OPEN, we expect a quick fallback response from circuit breaker.
|
||||||
|
assertEquals("Delayed service is down", monitoringService.delayedServiceResponse());
|
||||||
|
|
||||||
|
//Meanwhile, the quick service is responding and the circuit state is CLOSED
|
||||||
|
assertEquals("Quick Service is working", monitoringService.quickServiceResponse());
|
||||||
|
assertEquals("CLOSED", quickServiceCircuitBreaker.getState());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFailure_HalfOpenStateTransition() {
|
||||||
|
//Calling delayed service, which will be unhealthy till 4 seconds
|
||||||
|
assertEquals("Delayed service is down", monitoringService.delayedServiceResponse());
|
||||||
|
//As failure threshold is "1", the circuit breaker is changed to OPEN
|
||||||
|
assertEquals("OPEN", delayedServiceCircuitBreaker.getState());
|
||||||
|
|
||||||
|
//Waiting for recovery period of 2 seconds for circuit breaker to retry service.
|
||||||
|
try {
|
||||||
|
LOGGER.info("Waiting 2s for delayed service to become responsive");
|
||||||
|
Thread.sleep(2000);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
//After 2 seconds, the circuit breaker should move to "HALF_OPEN" state and retry fetching response from service again
|
||||||
|
assertEquals("HALF_OPEN", delayedServiceCircuitBreaker.getState());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRecovery_ClosedStateTransition() {
|
||||||
|
//Calling delayed service, which will be unhealthy till 4 seconds
|
||||||
|
assertEquals("Delayed service is down", monitoringService.delayedServiceResponse());
|
||||||
|
//As failure threshold is "1", the circuit breaker is changed to OPEN
|
||||||
|
assertEquals("OPEN", delayedServiceCircuitBreaker.getState());
|
||||||
|
|
||||||
|
//Waiting for 4 seconds, which is enough for DelayedService to become healthy and respond successfully.
|
||||||
|
try {
|
||||||
|
LOGGER.info("Waiting 4s for delayed service to become responsive");
|
||||||
|
Thread.sleep(4000);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
//As retry period is 2 seconds (<4 seconds of wait), hence the circuit breaker should be back in HALF_OPEN state.
|
||||||
|
assertEquals("HALF_OPEN", delayedServiceCircuitBreaker.getState());
|
||||||
|
//Check the success response from delayed service.
|
||||||
|
assertEquals("Delayed service is working", monitoringService.delayedServiceResponse());
|
||||||
|
//As the response is success, the state should be CLOSED
|
||||||
|
assertEquals("CLOSED", delayedServiceCircuitBreaker.getState());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -25,56 +25,60 @@ package com.iluwatar.circuitbreaker;
|
|||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
import java.rmi.Remote;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Circuit Breaker test
|
* Circuit Breaker test
|
||||||
*/
|
*/
|
||||||
public class CircuitBreakerTest {
|
public class DefaultCircuitBreakerTest {
|
||||||
|
|
||||||
//long timeout, int failureThreshold, long retryTimePeriod
|
//long timeout, int failureThreshold, long retryTimePeriod
|
||||||
@Test
|
@Test
|
||||||
public void testSetState() {
|
public void testEvaluateState() {
|
||||||
var circuitBreaker = new CircuitBreaker(1, 1, 100);
|
var circuitBreaker = new DefaultCircuitBreaker(null, 1, 1, 100);
|
||||||
//Right now, failureCount<failureThreshold, so state should be closed
|
//Right now, failureCount<failureThreshold, so state should be closed
|
||||||
assertEquals(circuitBreaker.getState(), "CLOSED");
|
assertEquals(circuitBreaker.getState(), "CLOSED");
|
||||||
circuitBreaker.failureCount = 4;
|
circuitBreaker.failureCount = 4;
|
||||||
circuitBreaker.lastFailureTime = System.nanoTime();
|
circuitBreaker.lastFailureTime = System.nanoTime();
|
||||||
circuitBreaker.setState();
|
circuitBreaker.evaluateState();
|
||||||
//Since failureCount>failureThreshold, and lastFailureTime is nearly equal to current time,
|
//Since failureCount>failureThreshold, and lastFailureTime is nearly equal to current time,
|
||||||
//state should be half-open
|
//state should be half-open
|
||||||
assertEquals(circuitBreaker.getState(), "HALF_OPEN");
|
assertEquals(circuitBreaker.getState(), "HALF_OPEN");
|
||||||
//Since failureCount>failureThreshold, and lastFailureTime is much lesser current time,
|
//Since failureCount>failureThreshold, and lastFailureTime is much lesser current time,
|
||||||
//state should be open
|
//state should be open
|
||||||
circuitBreaker.lastFailureTime = System.nanoTime() - 1000 * 1000 * 1000 * 1000;
|
circuitBreaker.lastFailureTime = System.nanoTime() - 1000 * 1000 * 1000 * 1000;
|
||||||
circuitBreaker.setState();
|
circuitBreaker.evaluateState();
|
||||||
assertEquals(circuitBreaker.getState(), "OPEN");
|
assertEquals(circuitBreaker.getState(), "OPEN");
|
||||||
//Now set it back again to closed to test idempotency
|
//Now set it back again to closed to test idempotency
|
||||||
circuitBreaker.failureCount = 0;
|
circuitBreaker.failureCount = 0;
|
||||||
circuitBreaker.setState();
|
circuitBreaker.evaluateState();
|
||||||
assertEquals(circuitBreaker.getState(), "CLOSED");
|
assertEquals(circuitBreaker.getState(), "CLOSED");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSetStateForBypass() {
|
public void testSetStateForBypass() {
|
||||||
var circuitBreaker = new CircuitBreaker(1, 1, 100);
|
var circuitBreaker = new DefaultCircuitBreaker(null, 1, 1, 2000 * 1000 * 1000);
|
||||||
//Right now, failureCount<failureThreshold, so state should be closed
|
//Right now, failureCount<failureThreshold, so state should be closed
|
||||||
//Bypass it and set it to open
|
//Bypass it and set it to open
|
||||||
circuitBreaker.setStateForBypass(State.OPEN);
|
circuitBreaker.setState(State.OPEN);
|
||||||
assertEquals(circuitBreaker.getState(), "OPEN");
|
assertEquals(circuitBreaker.getState(), "OPEN");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testApiResponses() {
|
public void testApiResponses() throws RemoteServiceException {
|
||||||
var circuitBreaker = new CircuitBreaker(1, 1, 100);
|
RemoteService mockService = new RemoteService() {
|
||||||
try {
|
@Override
|
||||||
//Call with the paramater start_time set to huge amount of time in past so that service
|
public String call() throws RemoteServiceException {
|
||||||
//replies with "Ok". Also, state is CLOSED in start
|
return "Remote Success";
|
||||||
var serviceStartTime = System.nanoTime() - 60 * 1000 * 1000 * 1000;
|
}
|
||||||
var response = circuitBreaker.call("delayedService", serviceStartTime);
|
};
|
||||||
assertEquals(response, "Delayed service is working");
|
var circuitBreaker = new DefaultCircuitBreaker(mockService, 1, 1, 100);
|
||||||
} catch (Exception e) {
|
//Call with the paramater start_time set to huge amount of time in past so that service
|
||||||
System.out.println(e.getMessage());
|
//replies with "Ok". Also, state is CLOSED in start
|
||||||
}
|
var serviceStartTime = System.nanoTime() - 60 * 1000 * 1000 * 1000;
|
||||||
|
var response = circuitBreaker.attemptRequest();
|
||||||
|
assertEquals(response, "Remote Success");
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -0,0 +1,59 @@
|
|||||||
|
/*
|
||||||
|
* The MIT License
|
||||||
|
* Copyright © 2014-2019 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.circuitbreaker;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Monitoring Service test
|
||||||
|
*/
|
||||||
|
public class DelayedRemoteServiceTest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Testing immediate response of the delayed service.
|
||||||
|
*
|
||||||
|
* @throws RemoteServiceException
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testDefaultConstructor() throws RemoteServiceException {
|
||||||
|
Assertions.assertThrows(RemoteServiceException.class, () -> {
|
||||||
|
var obj = new DelayedRemoteService();
|
||||||
|
obj.call();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Testing server started in past (2 seconds ago) and with a simulated delay of 1 second.
|
||||||
|
*
|
||||||
|
* @throws RemoteServiceException
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testParameterizedConstructor() throws RemoteServiceException {
|
||||||
|
var obj = new DelayedRemoteService(System.nanoTime()-2000*1000*1000,1);
|
||||||
|
assertEquals("Delayed service is working",obj.call());
|
||||||
|
}
|
||||||
|
}
|
@@ -35,28 +35,45 @@ public class MonitoringServiceTest {
|
|||||||
//long timeout, int failureThreshold, long retryTimePeriod
|
//long timeout, int failureThreshold, long retryTimePeriod
|
||||||
@Test
|
@Test
|
||||||
public void testLocalResponse() {
|
public void testLocalResponse() {
|
||||||
var monitoringService = new MonitoringService();
|
var monitoringService = new MonitoringService(null,null);
|
||||||
var response = monitoringService.localResourceResponse();
|
var response = monitoringService.localResourceResponse();
|
||||||
assertEquals(response, "Local Service is working");
|
assertEquals(response, "Local Service is working");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRemoteResponse() {
|
public void testDelayedRemoteResponseSuccess() {
|
||||||
var monitoringService = new MonitoringService();
|
var delayedService = new DelayedRemoteService(System.nanoTime()-2*1000*1000*1000, 2);
|
||||||
var circuitBreaker = new CircuitBreaker(1, 1, 100);
|
var delayedServiceCircuitBreaker = new DefaultCircuitBreaker(delayedService, 3000,
|
||||||
|
1,
|
||||||
|
2 * 1000 * 1000 * 1000);
|
||||||
|
|
||||||
|
var monitoringService = new MonitoringService(delayedServiceCircuitBreaker,null);
|
||||||
//Set time in past to make the server work
|
//Set time in past to make the server work
|
||||||
var serverStartTime = System.nanoTime() / 10;
|
var response = monitoringService.delayedServiceResponse();
|
||||||
var response = monitoringService.remoteResourceResponse(circuitBreaker, serverStartTime);
|
|
||||||
assertEquals(response, "Delayed service is working");
|
assertEquals(response, "Delayed service is working");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRemoteResponse2() {
|
public void testDelayedRemoteResponseFailure() {
|
||||||
var monitoringService = new MonitoringService();
|
var delayedService = new DelayedRemoteService(System.nanoTime(), 2);
|
||||||
var circuitBreaker = new CircuitBreaker(1, 1, 100);
|
var delayedServiceCircuitBreaker = new DefaultCircuitBreaker(delayedService, 3000,
|
||||||
|
1,
|
||||||
|
2 * 1000 * 1000 * 1000);
|
||||||
|
var monitoringService = new MonitoringService(delayedServiceCircuitBreaker,null);
|
||||||
//Set time as current time as initially server fails
|
//Set time as current time as initially server fails
|
||||||
var serverStartTime = System.nanoTime();
|
var response = monitoringService.delayedServiceResponse();
|
||||||
var response = monitoringService.remoteResourceResponse(circuitBreaker, serverStartTime);
|
assertEquals(response, "Delayed service is down");
|
||||||
assertEquals(response, "Remote service not responding");
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testQuickRemoteServiceResponse() {
|
||||||
|
var delayedService = new QuickRemoteService();
|
||||||
|
var delayedServiceCircuitBreaker = new DefaultCircuitBreaker(delayedService, 3000,
|
||||||
|
1,
|
||||||
|
2 * 1000 * 1000 * 1000);
|
||||||
|
var monitoringService = new MonitoringService(delayedServiceCircuitBreaker,null);
|
||||||
|
//Set time as current time as initially server fails
|
||||||
|
var response = monitoringService.delayedServiceResponse();
|
||||||
|
assertEquals(response, "Quick Service is working");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -14,14 +14,14 @@ Action, Transaction
|
|||||||
|
|
||||||
## Intent
|
## Intent
|
||||||
|
|
||||||
Encapsulate a request as an object, thereby letting you parameterize clients with different
|
Encapsulate a request as an object, thereby letting you parameterize clients with different
|
||||||
requests, queue or log requests, and support undoable operations.
|
requests, queue or log requests, and support undoable operations.
|
||||||
|
|
||||||
## Explanation
|
## Explanation
|
||||||
Real world example
|
Real world example
|
||||||
|
|
||||||
> There is a wizard casting spells on a goblin. The spells are executed on the goblin one by one.
|
> There is a wizard casting spells on a goblin. The spells are executed on the goblin one by one.
|
||||||
> The first spell shrinks the goblin and the second makes him invisible. Then the wizard reverses
|
> The first spell shrinks the goblin and the second makes him invisible. Then the wizard reverses
|
||||||
> the spells one by one. Each spell here is a command object that can be undone.
|
> the spells one by one. Each spell here is a command object that can be undone.
|
||||||
|
|
||||||
In plain words
|
In plain words
|
||||||
@@ -30,8 +30,8 @@ In plain words
|
|||||||
|
|
||||||
Wikipedia says
|
Wikipedia says
|
||||||
|
|
||||||
> In object-oriented programming, the command pattern is a behavioral design pattern in which an
|
> In object-oriented programming, the command pattern is a behavioral design pattern in which an
|
||||||
> object is used to encapsulate all information needed to perform an action or trigger an event at
|
> object is used to encapsulate all information needed to perform an action or trigger an event at
|
||||||
> a later time.
|
> a later time.
|
||||||
|
|
||||||
**Programmatic Example**
|
**Programmatic Example**
|
||||||
@@ -41,25 +41,21 @@ Here's the sample code with wizard and goblin. Let's start from the `Wizard` cla
|
|||||||
```java
|
```java
|
||||||
public class Wizard {
|
public class Wizard {
|
||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(Wizard.class);
|
|
||||||
|
|
||||||
private final Deque<Command> undoStack = new LinkedList<>();
|
private final Deque<Command> undoStack = new LinkedList<>();
|
||||||
private final Deque<Command> redoStack = new LinkedList<>();
|
private final Deque<Command> redoStack = new LinkedList<>();
|
||||||
|
|
||||||
public Wizard() {}
|
public Wizard() {}
|
||||||
|
|
||||||
public void castSpell(Command command, Target target) {
|
public void castSpell(Runnable runnable) {
|
||||||
LOGGER.info("{} casts {} at {}", this, command, target);
|
runnable.run();
|
||||||
command.execute(target);
|
undoStack.offerLast(runnable);
|
||||||
undoStack.offerLast(command);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void undoLastSpell() {
|
public void undoLastSpell() {
|
||||||
if (!undoStack.isEmpty()) {
|
if (!undoStack.isEmpty()) {
|
||||||
var previousSpell = undoStack.pollLast();
|
var previousSpell = undoStack.pollLast();
|
||||||
redoStack.offerLast(previousSpell);
|
redoStack.offerLast(previousSpell);
|
||||||
LOGGER.info("{} undoes {}", this, previousSpell);
|
previousSpell.run();
|
||||||
previousSpell.undo();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,8 +63,7 @@ public class Wizard {
|
|||||||
if (!redoStack.isEmpty()) {
|
if (!redoStack.isEmpty()) {
|
||||||
var previousSpell = redoStack.pollLast();
|
var previousSpell = redoStack.pollLast();
|
||||||
undoStack.offerLast(previousSpell);
|
undoStack.offerLast(previousSpell);
|
||||||
LOGGER.info("{} redoes {}", this, previousSpell);
|
previousSpell.run();
|
||||||
previousSpell.redo();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,84 +74,7 @@ public class Wizard {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Next we present the spell hierarchy.
|
Next, we have the goblin who's the target of the spells.
|
||||||
|
|
||||||
```java
|
|
||||||
public interface Command {
|
|
||||||
|
|
||||||
void execute(Target target);
|
|
||||||
|
|
||||||
void undo();
|
|
||||||
|
|
||||||
void redo();
|
|
||||||
|
|
||||||
String toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public class InvisibilitySpell implements Command {
|
|
||||||
|
|
||||||
private Target target;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(Target target) {
|
|
||||||
target.setVisibility(Visibility.INVISIBLE);
|
|
||||||
this.target = target;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void undo() {
|
|
||||||
if (target != null) {
|
|
||||||
target.setVisibility(Visibility.VISIBLE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void redo() {
|
|
||||||
if (target != null) {
|
|
||||||
target.setVisibility(Visibility.INVISIBLE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "Invisibility spell";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ShrinkSpell implements Command {
|
|
||||||
|
|
||||||
private Size oldSize;
|
|
||||||
private Target target;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute(Target target) {
|
|
||||||
oldSize = target.getSize();
|
|
||||||
target.setSize(Size.SMALL);
|
|
||||||
this.target = target;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void undo() {
|
|
||||||
if (oldSize != null && target != null) {
|
|
||||||
var temp = target.getSize();
|
|
||||||
target.setSize(oldSize);
|
|
||||||
oldSize = temp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void redo() {
|
|
||||||
undo();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "Shrink spell";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Finally, we have the goblin who's the target of the spells.
|
|
||||||
|
|
||||||
```java
|
```java
|
||||||
public abstract class Target {
|
public abstract class Target {
|
||||||
@@ -203,33 +121,73 @@ public class Goblin extends Target {
|
|||||||
return "Goblin";
|
return "Goblin";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void changeSize() {
|
||||||
|
var oldSize = getSize() == Size.NORMAL ? Size.SMALL : Size.NORMAL;
|
||||||
|
setSize(oldSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void changeVisibility() {
|
||||||
|
var visible = getVisibility() == Visibility.INVISIBLE
|
||||||
|
? Visibility.VISIBLE : Visibility.INVISIBLE;
|
||||||
|
setVisibility(visible);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Finally we have the wizard in main function who casts spell
|
||||||
|
|
||||||
|
```java
|
||||||
|
public static void main(String[] args) {
|
||||||
|
var wizard = new Wizard();
|
||||||
|
var goblin = new Goblin();
|
||||||
|
|
||||||
|
// casts shrink/unshrink spell
|
||||||
|
wizard.castSpell(goblin::changeSize);
|
||||||
|
|
||||||
|
// casts visible/invisible spell
|
||||||
|
wizard.castSpell(goblin::changeVisibility);
|
||||||
|
|
||||||
|
// undo and redo casts
|
||||||
|
wizard.undoLastSpell();
|
||||||
|
wizard.redoLastSpell();
|
||||||
|
```
|
||||||
|
|
||||||
Here's the whole example in action.
|
Here's the whole example in action.
|
||||||
|
|
||||||
```java
|
```java
|
||||||
var wizard = new Wizard();
|
var wizard = new Wizard();
|
||||||
var goblin = new Goblin();
|
var goblin = new Goblin();
|
||||||
|
|
||||||
goblin.printStatus();
|
goblin.printStatus();
|
||||||
wizard.castSpell(new ShrinkSpell(), goblin);
|
wizard.castSpell(goblin::changeSize);
|
||||||
goblin.printStatus();
|
goblin.printStatus();
|
||||||
wizard.castSpell(new InvisibilitySpell(), goblin);
|
|
||||||
|
wizard.castSpell(goblin::changeVisibility);
|
||||||
goblin.printStatus();
|
goblin.printStatus();
|
||||||
|
|
||||||
wizard.undoLastSpell();
|
wizard.undoLastSpell();
|
||||||
goblin.printStatus();
|
goblin.printStatus();
|
||||||
|
|
||||||
|
wizard.undoLastSpell();
|
||||||
|
goblin.printStatus();
|
||||||
|
|
||||||
|
wizard.redoLastSpell();
|
||||||
|
goblin.printStatus();
|
||||||
|
|
||||||
|
wizard.redoLastSpell();
|
||||||
|
goblin.printStatus();
|
||||||
```
|
```
|
||||||
|
|
||||||
Here's the program output:
|
Here's the program output:
|
||||||
|
|
||||||
```java
|
```java
|
||||||
// Goblin, [size=normal] [visibility=visible]
|
Goblin, [size=normal] [visibility=visible]
|
||||||
// Wizard casts Shrink spell at Goblin
|
Goblin, [size=small] [visibility=visible]
|
||||||
// Goblin, [size=small] [visibility=visible]
|
Goblin, [size=small] [visibility=invisible]
|
||||||
// Wizard casts Invisibility spell at Goblin
|
Goblin, [size=small] [visibility=visible]
|
||||||
// Goblin, [size=small] [visibility=invisible]
|
Goblin, [size=normal] [visibility=visible]
|
||||||
// Wizard undoes Invisibility spell
|
Goblin, [size=small] [visibility=visible]
|
||||||
// Goblin, [size=small] [visibility=visible]
|
Goblin, [size=small] [visibility=invisible]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Class diagram
|
## Class diagram
|
||||||
@@ -240,26 +198,26 @@ Here's the program output:
|
|||||||
|
|
||||||
Use the Command pattern when you want to:
|
Use the Command pattern when you want to:
|
||||||
|
|
||||||
* Parameterize objects by an action to perform. You can express such parameterization in a
|
* Parameterize objects by an action to perform. You can express such parameterization in a
|
||||||
procedural language with a callback function, that is, a function that's registered somewhere to be
|
procedural language with a callback function, that is, a function that's registered somewhere to be
|
||||||
called at a later point. Commands are an object-oriented replacement for callbacks.
|
called at a later point. Commands are an object-oriented replacement for callbacks.
|
||||||
* Specify, queue, and execute requests at different times. A Command object can have a lifetime
|
* Specify, queue, and execute requests at different times. A Command object can have a lifetime
|
||||||
independent of the original request. If the receiver of a request can be represented in an address
|
independent of the original request. If the receiver of a request can be represented in an address
|
||||||
space-independent way, then you can transfer a command object for the request to a different process
|
space-independent way, then you can transfer a command object for the request to a different process
|
||||||
and fulfill the request there.
|
and fulfill the request there.
|
||||||
* Support undo. The Command's execute operation can store state for reversing its effects in the
|
* Support undo. The Command's execute operation can store state for reversing its effects in the
|
||||||
command itself. The Command interface must have an added un-execute operation that reverses the
|
command itself. The Command interface must have an added un-execute operation that reverses the
|
||||||
effects of a previous call to execute. The executed commands are stored in a history list.
|
effects of a previous call to execute. The executed commands are stored in a history list.
|
||||||
Unlimited-level undo and redo is achieved by traversing this list backwards and forwards calling
|
Unlimited-level undo and redo is achieved by traversing this list backwards and forwards calling
|
||||||
un-execute and execute, respectively.
|
un-execute and execute, respectively.
|
||||||
* Support logging changes so that they can be reapplied in case of a system crash. By augmenting the
|
* Support logging changes so that they can be reapplied in case of a system crash. By augmenting the
|
||||||
Command interface with load and store operations, you can keep a persistent log of changes.
|
Command interface with load and store operations, you can keep a persistent log of changes.
|
||||||
Recovering from a crash involves reloading logged commands from disk and re-executing them with
|
Recovering from a crash involves reloading logged commands from disk and re-executing them with
|
||||||
the execute operation.
|
the execute operation.
|
||||||
* Structure a system around high-level operations build on primitive operations. Such a structure is
|
* Structure a system around high-level operations build on primitive operations. Such a structure is
|
||||||
common in information systems that support transactions. A transaction encapsulates a set of changes
|
common in information systems that support transactions. A transaction encapsulates a set of changes
|
||||||
to data. The Command pattern offers a way to model transactions. Commands have a common interface,
|
to data. The Command pattern offers a way to model transactions. Commands have a common interface,
|
||||||
letting you invoke all transactions the same way. The pattern also makes it easy to extend the
|
letting you invoke all transactions the same way. The pattern also makes it easy to extend the
|
||||||
system with new transactions.
|
system with new transactions.
|
||||||
|
|
||||||
## Typical Use Case
|
## Typical Use Case
|
||||||
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 50 KiB |
@@ -1,116 +1,89 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<class-diagram version="1.1.8" icons="true" automaticImage="PNG" always-add-relationships="false" generalizations="true"
|
<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">
|
realizations="true" associations="true" dependencies="false" nesting-relationships="true">
|
||||||
<class id="1" language="java" name="com.iluwatar.command.ShrinkSpell" project="command"
|
<class id="2" language="java" name="com.iluwatar.command.Goblin" project="command"
|
||||||
file="/command/src/main/java/com/iluwatar/command/ShrinkSpell.java" binary="false" corner="BOTTOM_RIGHT">
|
file="/command/src/main/java/com/iluwatar/command/Goblin.java" binary="false" corner="BOTTOM_RIGHT">
|
||||||
<position height="178" width="141" x="-30" y="681"/>
|
<position height="-1" width="-1" x="129" y="1223"/>
|
||||||
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
|
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
|
||||||
sort-features="false" accessors="true" visibility="true">
|
sort-features="false" accessors="true" visibility="true">
|
||||||
<attributes public="true" package="true" protected="true" private="true" static="true"/>
|
<attributes public="true" package="true" protected="true" private="true" static="true"/>
|
||||||
<operations public="true" package="true" protected="true" private="true" static="true"/>
|
<operations public="true" package="true" protected="true" private="true" static="true"/>
|
||||||
</display>
|
</display>
|
||||||
</class>
|
</class>
|
||||||
<class id="2" language="java" name="com.iluwatar.command.Goblin" project="command"
|
<class id="3" language="java" name="com.iluwatar.command.Wizard" project="command"
|
||||||
file="/command/src/main/java/com/iluwatar/command/Goblin.java" binary="false" corner="BOTTOM_RIGHT">
|
file="/command/src/main/java/com/iluwatar/command/Wizard.java" binary="false" corner="BOTTOM_RIGHT">
|
||||||
<position height="-1" width="-1" x="129" y="1223"/>
|
<position height="-1" width="-1" x="129" y="362"/>
|
||||||
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
|
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
|
||||||
sort-features="false" accessors="true" visibility="true">
|
sort-features="false" accessors="true" visibility="true">
|
||||||
<attributes public="true" package="true" protected="true" private="true" static="true"/>
|
<attributes public="true" package="true" protected="true" private="true" static="true"/>
|
||||||
<operations public="true" package="true" protected="true" private="true" static="true"/>
|
<operations public="true" package="true" protected="true" private="true" static="true"/>
|
||||||
</display>
|
</display>
|
||||||
</class>
|
</class>
|
||||||
<class id="3" language="java" name="com.iluwatar.command.Wizard" project="command"
|
<class id="6" language="java" name="com.iluwatar.command.Target" project="command"
|
||||||
file="/command/src/main/java/com/iluwatar/command/Wizard.java" binary="false" corner="BOTTOM_RIGHT">
|
file="/command/src/main/java/com/iluwatar/command/Target.java" binary="false" corner="BOTTOM_RIGHT">
|
||||||
<position height="-1" width="-1" x="129" y="362"/>
|
<position height="-1" width="-1" x="129" y="1014"/>
|
||||||
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
|
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
|
||||||
sort-features="false" accessors="true" visibility="true">
|
sort-features="false" accessors="true" visibility="true">
|
||||||
<attributes public="true" package="true" protected="true" private="true" static="true"/>
|
<attributes public="true" package="true" protected="true" private="true" static="true"/>
|
||||||
<operations public="true" package="true" protected="true" private="true" static="true"/>
|
<operations public="true" package="true" protected="true" private="true" static="true"/>
|
||||||
</display>
|
</display>
|
||||||
</class>
|
</class>
|
||||||
<class id="4" language="java" name="com.iluwatar.command.Command" project="command"
|
<association id="7">
|
||||||
file="/command/src/main/java/com/iluwatar/command/Command.java" binary="false" corner="BOTTOM_RIGHT">
|
<end type="SOURCE" refId="3" navigable="false">
|
||||||
<position height="-1" width="-1" x="129" y="561"/>
|
<attribute id="8" name="redoStack">
|
||||||
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
|
<position height="20" width="67" x="140" y="451"/>
|
||||||
sort-features="false" accessors="true" visibility="true">
|
</attribute>
|
||||||
<attributes public="true" package="true" protected="true" private="true" static="true"/>
|
<multiplicity id="9" minimum="0" maximum="2147483647">
|
||||||
<operations public="true" package="true" protected="true" private="true" static="true"/>
|
<position height="18" width="25" x="221" y="452"/>
|
||||||
</display>
|
</multiplicity>
|
||||||
</class>
|
</end>
|
||||||
<class id="5" language="java" name="com.iluwatar.command.InvisibilitySpell" project="command"
|
<end type="TARGET" refId="4" navigable="true"/>
|
||||||
file="/command/src/main/java/com/iluwatar/command/InvisibilitySpell.java" binary="false" corner="BOTTOM_RIGHT">
|
<display labels="true" multiplicity="true"/>
|
||||||
<position height="160" width="141" x="151" y="681"/>
|
</association>
|
||||||
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
|
<generalization id="10">
|
||||||
sort-features="false" accessors="true" visibility="true">
|
<end type="SOURCE" refId="2"/>
|
||||||
<attributes public="true" package="true" protected="true" private="true" static="true"/>
|
<end type="TARGET" refId="6"/>
|
||||||
<operations public="true" package="true" protected="true" private="true" static="true"/>
|
</generalization>
|
||||||
</display>
|
<association id="11">
|
||||||
</class>
|
<end type="SOURCE" refId="1" navigable="false">
|
||||||
<class id="6" language="java" name="com.iluwatar.command.Target" project="command"
|
<attribute id="12" name="target"/>
|
||||||
file="/command/src/main/java/com/iluwatar/command/Target.java" binary="false" corner="BOTTOM_RIGHT">
|
<multiplicity id="13" minimum="0" maximum="1"/>
|
||||||
<position height="-1" width="-1" x="129" y="1014"/>
|
</end>
|
||||||
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
|
<end type="TARGET" refId="6" navigable="true"/>
|
||||||
sort-features="false" accessors="true" visibility="true">
|
<display labels="true" multiplicity="true"/>
|
||||||
<attributes public="true" package="true" protected="true" private="true" static="true"/>
|
</association>
|
||||||
<operations public="true" package="true" protected="true" private="true" static="true"/>
|
<generalization id="14">
|
||||||
</display>
|
<end type="SOURCE" refId="1"/>
|
||||||
</class>
|
<end type="TARGET" refId="4"/>
|
||||||
<association id="7">
|
</generalization>
|
||||||
<end type="SOURCE" refId="3" navigable="false">
|
<association id="15">
|
||||||
<attribute id="8" name="redoStack">
|
<end type="SOURCE" refId="3" navigable="false">
|
||||||
<position height="20" width="67" x="140" y="451"/>
|
<attribute id="16" name="undoStack">
|
||||||
</attribute>
|
<position height="20" width="70" x="-17" y="451"/>
|
||||||
<multiplicity id="9" minimum="0" maximum="2147483647">
|
</attribute>
|
||||||
<position height="18" width="25" x="221" y="452"/>
|
<multiplicity id="17" minimum="0" maximum="2147483647">
|
||||||
</multiplicity>
|
<position height="18" width="25" x="60" y="452"/>
|
||||||
</end>
|
</multiplicity>
|
||||||
<end type="TARGET" refId="4" navigable="true"/>
|
</end>
|
||||||
<display labels="true" multiplicity="true"/>
|
<end type="TARGET" refId="4" navigable="true"/>
|
||||||
</association>
|
<display labels="true" multiplicity="true"/>
|
||||||
<generalization id="10">
|
</association>
|
||||||
<end type="SOURCE" refId="2"/>
|
<generalization id="18">
|
||||||
<end type="TARGET" refId="6"/>
|
<end type="SOURCE" refId="5"/>
|
||||||
</generalization>
|
<end type="TARGET" refId="4"/>
|
||||||
<association id="11">
|
</generalization>
|
||||||
<end type="SOURCE" refId="1" navigable="false">
|
<association id="19">
|
||||||
<attribute id="12" name="target"/>
|
<end type="SOURCE" refId="5" navigable="false">
|
||||||
<multiplicity id="13" minimum="0" maximum="1"/>
|
<attribute id="20" name="target"/>
|
||||||
</end>
|
<multiplicity id="21" minimum="0" maximum="1"/>
|
||||||
<end type="TARGET" refId="6" navigable="true"/>
|
</end>
|
||||||
<display labels="true" multiplicity="true"/>
|
<end type="TARGET" refId="6" navigable="true"/>
|
||||||
</association>
|
<display labels="true" multiplicity="true"/>
|
||||||
<generalization id="14">
|
</association>
|
||||||
<end type="SOURCE" refId="1"/>
|
<classifier-display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
|
||||||
<end type="TARGET" refId="4"/>
|
sort-features="false" accessors="true" visibility="true">
|
||||||
</generalization>
|
<attributes public="true" package="true" protected="true" private="true" static="true"/>
|
||||||
<association id="15">
|
<operations public="true" package="true" protected="true" private="true" static="true"/>
|
||||||
<end type="SOURCE" refId="3" navigable="false">
|
</classifier-display>
|
||||||
<attribute id="16" name="undoStack">
|
|
||||||
<position height="20" width="70" x="-17" y="451"/>
|
|
||||||
</attribute>
|
|
||||||
<multiplicity id="17" minimum="0" maximum="2147483647">
|
|
||||||
<position height="18" width="25" x="60" y="452"/>
|
|
||||||
</multiplicity>
|
|
||||||
</end>
|
|
||||||
<end type="TARGET" refId="4" navigable="true"/>
|
|
||||||
<display labels="true" multiplicity="true"/>
|
|
||||||
</association>
|
|
||||||
<generalization id="18">
|
|
||||||
<end type="SOURCE" refId="5"/>
|
|
||||||
<end type="TARGET" refId="4"/>
|
|
||||||
</generalization>
|
|
||||||
<association id="19">
|
|
||||||
<end type="SOURCE" refId="5" navigable="false">
|
|
||||||
<attribute id="20" name="target"/>
|
|
||||||
<multiplicity id="21" minimum="0" maximum="1"/>
|
|
||||||
</end>
|
|
||||||
<end type="TARGET" refId="6" 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"/>
|
<association-display labels="true" multiplicity="true"/>
|
||||||
</class-diagram>
|
</class-diagram>
|
||||||
|
@@ -4,33 +4,11 @@ package com.iluwatar.command {
|
|||||||
+ App()
|
+ App()
|
||||||
+ main(args : String[]) {static}
|
+ main(args : String[]) {static}
|
||||||
}
|
}
|
||||||
interface Command {
|
|
||||||
+ Command()
|
|
||||||
+ execute(Target) {abstract}
|
|
||||||
+ redo() {abstract}
|
|
||||||
+ toString() : String {abstract}
|
|
||||||
+ undo() {abstract}
|
|
||||||
}
|
|
||||||
class Goblin {
|
class Goblin {
|
||||||
+ Goblin()
|
+ Goblin()
|
||||||
+ toString() : String
|
+ toString() : String
|
||||||
}
|
+ changeSize()
|
||||||
class InvisibilitySpell {
|
+ changeVisibility()
|
||||||
- target : Target
|
|
||||||
+ InvisibilitySpell()
|
|
||||||
+ execute(target : Target)
|
|
||||||
+ redo()
|
|
||||||
+ toString() : String
|
|
||||||
+ undo()
|
|
||||||
}
|
|
||||||
class ShrinkSpell {
|
|
||||||
- oldSize : Size
|
|
||||||
- target : Target
|
|
||||||
+ ShrinkSpell()
|
|
||||||
+ execute(target : Target)
|
|
||||||
+ redo()
|
|
||||||
+ toString() : String
|
|
||||||
+ undo()
|
|
||||||
}
|
}
|
||||||
enum Size {
|
enum Size {
|
||||||
+ NORMAL {static}
|
+ NORMAL {static}
|
||||||
@@ -62,22 +40,19 @@ package com.iluwatar.command {
|
|||||||
}
|
}
|
||||||
class Wizard {
|
class Wizard {
|
||||||
- LOGGER : Logger {static}
|
- LOGGER : Logger {static}
|
||||||
- redoStack : Deque<Command>
|
- redoStack : Deque<Runnable>
|
||||||
- undoStack : Deque<Command>
|
- undoStack : Deque<Runnable>
|
||||||
+ Wizard()
|
+ Wizard()
|
||||||
+ castSpell(command : Command, target : Target)
|
+ castSpell(Runnable : runnable)
|
||||||
+ redoLastSpell()
|
+ redoLastSpell()
|
||||||
+ toString() : String
|
+ toString() : String
|
||||||
+ undoLastSpell()
|
+ undoLastSpell()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Target --> "-size" Size
|
Target --> "-size" Size
|
||||||
Wizard --> "-undoStack" Command
|
Wizard --> "-changeSize" Goblin
|
||||||
ShrinkSpell --> "-oldSize" Size
|
Wizard --> "-changeVisibility" Goblin
|
||||||
InvisibilitySpell --> "-target" Target
|
|
||||||
ShrinkSpell --> "-target" Target
|
|
||||||
Target --> "-visibility" Visibility
|
Target --> "-visibility" Visibility
|
||||||
Goblin --|> Target
|
Goblin --|> Target
|
||||||
InvisibilitySpell ..|> Command
|
App --> "castSpell" Wizard
|
||||||
ShrinkSpell ..|> Command
|
|
||||||
@enduml
|
@enduml
|
||||||
|
@@ -30,12 +30,10 @@ package com.iluwatar.command;
|
|||||||
*
|
*
|
||||||
* <p>Four terms always associated with the command pattern are command, receiver, invoker and
|
* <p>Four terms always associated with the command pattern are command, receiver, invoker and
|
||||||
* client. A command object (spell) knows about the receiver (target) and invokes a method of the
|
* client. A command object (spell) knows about the receiver (target) and invokes a method of the
|
||||||
* receiver. Values for parameters of the receiver method are stored in the command. The receiver
|
* receiver. An invoker object (wizard) receives a reference to the command to be executed and
|
||||||
* then does the work. An invoker object (wizard) knows how to execute a command, and optionally
|
* optionally does bookkeeping about the command execution. The invoker does not know anything
|
||||||
* does bookkeeping about the command execution. The invoker does not know anything about a concrete
|
* about how the command is executed. The client decides which commands to execute at which
|
||||||
* command, it knows only about command interface. Both an invoker object and several command
|
* points. To execute a command, it passes a reference of the function to the invoker object.
|
||||||
* objects are held by a client object (app). The client decides which commands to execute at which
|
|
||||||
* points. To execute a command, it passes the command object to the invoker object.
|
|
||||||
*
|
*
|
||||||
* <p>In other words, in this example the wizard casts spells on the goblin. The wizard keeps track
|
* <p>In other words, in this example the wizard casts spells on the goblin. The wizard keeps track
|
||||||
* of the previous spells cast, so it is easy to undo them. In addition, the wizard keeps track of
|
* of the previous spells cast, so it is easy to undo them. In addition, the wizard keeps track of
|
||||||
@@ -54,10 +52,10 @@ public class App {
|
|||||||
|
|
||||||
goblin.printStatus();
|
goblin.printStatus();
|
||||||
|
|
||||||
wizard.castSpell(new ShrinkSpell(), goblin);
|
wizard.castSpell(goblin::changeSize);
|
||||||
goblin.printStatus();
|
goblin.printStatus();
|
||||||
|
|
||||||
wizard.castSpell(new InvisibilitySpell(), goblin);
|
wizard.castSpell(goblin::changeVisibility);
|
||||||
goblin.printStatus();
|
goblin.printStatus();
|
||||||
|
|
||||||
wizard.undoLastSpell();
|
wizard.undoLastSpell();
|
||||||
|
@@ -37,5 +37,4 @@ public class Goblin extends Target {
|
|||||||
public String toString() {
|
public String toString() {
|
||||||
return "Goblin";
|
return "Goblin";
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -62,4 +62,21 @@ public abstract class Target {
|
|||||||
public void printStatus() {
|
public void printStatus() {
|
||||||
LOGGER.info("{}, [size={}] [visibility={}]", this, getSize(), getVisibility());
|
LOGGER.info("{}, [size={}] [visibility={}]", this, getSize(), getVisibility());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the size of the target.
|
||||||
|
*/
|
||||||
|
public void changeSize() {
|
||||||
|
var oldSize = getSize() == Size.NORMAL ? Size.SMALL : Size.NORMAL;
|
||||||
|
setSize(oldSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the visibility of the target.
|
||||||
|
*/
|
||||||
|
public void changeVisibility() {
|
||||||
|
var visible = getVisibility() == Visibility.INVISIBLE
|
||||||
|
? Visibility.VISIBLE : Visibility.INVISIBLE;
|
||||||
|
setVisibility(visible);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -25,30 +25,24 @@ package com.iluwatar.command;
|
|||||||
|
|
||||||
import java.util.Deque;
|
import java.util.Deque;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wizard is the invoker of the commands.
|
* Wizard is the invoker of the commands.
|
||||||
*/
|
*/
|
||||||
public class Wizard {
|
public class Wizard {
|
||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(Wizard.class);
|
private final Deque<Runnable> undoStack = new LinkedList<>();
|
||||||
|
private final Deque<Runnable> redoStack = new LinkedList<>();
|
||||||
private final Deque<Command> undoStack = new LinkedList<>();
|
|
||||||
private final Deque<Command> redoStack = new LinkedList<>();
|
|
||||||
|
|
||||||
public Wizard() {
|
public Wizard() {
|
||||||
// comment to ignore sonar issue: LEVEL critical
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cast spell.
|
* Cast spell.
|
||||||
*/
|
*/
|
||||||
public void castSpell(Command command, Target target) {
|
public void castSpell(Runnable runnable) {
|
||||||
LOGGER.info("{} casts {} at {}", this, command, target);
|
runnable.run();
|
||||||
command.execute(target);
|
undoStack.offerLast(runnable);
|
||||||
undoStack.offerLast(command);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -58,8 +52,7 @@ public class Wizard {
|
|||||||
if (!undoStack.isEmpty()) {
|
if (!undoStack.isEmpty()) {
|
||||||
var previousSpell = undoStack.pollLast();
|
var previousSpell = undoStack.pollLast();
|
||||||
redoStack.offerLast(previousSpell);
|
redoStack.offerLast(previousSpell);
|
||||||
LOGGER.info("{} undoes {}", this, previousSpell);
|
previousSpell.run();
|
||||||
previousSpell.undo();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,8 +63,7 @@ public class Wizard {
|
|||||||
if (!redoStack.isEmpty()) {
|
if (!redoStack.isEmpty()) {
|
||||||
var previousSpell = redoStack.pollLast();
|
var previousSpell = redoStack.pollLast();
|
||||||
undoStack.offerLast(previousSpell);
|
undoStack.offerLast(previousSpell);
|
||||||
LOGGER.info("{} redoes {}", this, previousSpell);
|
previousSpell.run();
|
||||||
previousSpell.redo();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -56,10 +56,10 @@ public class CommandTest {
|
|||||||
var wizard = new Wizard();
|
var wizard = new Wizard();
|
||||||
var goblin = new Goblin();
|
var goblin = new Goblin();
|
||||||
|
|
||||||
wizard.castSpell(new ShrinkSpell(), goblin);
|
wizard.castSpell(goblin::changeSize);
|
||||||
verifyGoblin(goblin, GOBLIN, Size.SMALL, Visibility.VISIBLE);
|
verifyGoblin(goblin, GOBLIN, Size.SMALL, Visibility.VISIBLE);
|
||||||
|
|
||||||
wizard.castSpell(new InvisibilitySpell(), goblin);
|
wizard.castSpell(goblin::changeVisibility);
|
||||||
verifyGoblin(goblin, GOBLIN, Size.SMALL, Visibility.INVISIBLE);
|
verifyGoblin(goblin, GOBLIN, Size.SMALL, Visibility.INVISIBLE);
|
||||||
|
|
||||||
wizard.undoLastSpell();
|
wizard.undoLastSpell();
|
||||||
|
@@ -45,7 +45,7 @@ public class SpaceStationMir extends GameObject {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void collisionResolve(FlamingAsteroid asteroid) {
|
public void collisionResolve(FlamingAsteroid asteroid) {
|
||||||
LOGGER.info(AppConstants.HITS, " {} is damaged! {} is set on fire!", asteroid.getClass()
|
LOGGER.info(AppConstants.HITS + " {} is damaged! {} is set on fire!", asteroid.getClass()
|
||||||
.getSimpleName(),
|
.getSimpleName(),
|
||||||
this.getClass().getSimpleName(), this.getClass().getSimpleName(), this.getClass()
|
this.getClass().getSimpleName(), this.getClass().getSimpleName(), this.getClass()
|
||||||
.getSimpleName());
|
.getSimpleName());
|
||||||
@@ -55,14 +55,14 @@ public class SpaceStationMir extends GameObject {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void collisionResolve(Meteoroid meteoroid) {
|
public void collisionResolve(Meteoroid meteoroid) {
|
||||||
LOGGER.info(AppConstants.HITS, " {} is damaged!", meteoroid.getClass().getSimpleName(),
|
LOGGER.info(AppConstants.HITS + " {} is damaged!", meteoroid.getClass().getSimpleName(),
|
||||||
this.getClass().getSimpleName(), this.getClass().getSimpleName());
|
this.getClass().getSimpleName(), this.getClass().getSimpleName());
|
||||||
setDamaged(true);
|
setDamaged(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void collisionResolve(SpaceStationMir mir) {
|
public void collisionResolve(SpaceStationMir mir) {
|
||||||
LOGGER.info(AppConstants.HITS, " {} is damaged!", mir.getClass().getSimpleName(),
|
LOGGER.info(AppConstants.HITS + " {} is damaged!", mir.getClass().getSimpleName(),
|
||||||
this.getClass().getSimpleName(), this.getClass().getSimpleName());
|
this.getClass().getSimpleName(), this.getClass().getSimpleName());
|
||||||
setDamaged(true);
|
setDamaged(true);
|
||||||
}
|
}
|
||||||
|
@@ -29,6 +29,6 @@ Use the Event Sourcing pattern when
|
|||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
* [Martin Fowler - Event Sourcing](https://martinfowler.com/eaaDev/EventSourcing.html)
|
* [Martin Fowler - Event Sourcing](https://martinfowler.com/eaaDev/EventSourcing.html)
|
||||||
* [Event Sourcing | Microsoft Docs](https://docs.microsoft.com/en-us/azure/architecture/patterns/event-sourcing)
|
* [Event Sourcing in Microsoft's documentation](https://docs.microsoft.com/en-us/azure/architecture/patterns/event-sourcing)
|
||||||
* [Reference 3: Introducing Event Sourcing](https://msdn.microsoft.com/en-us/library/jj591559.aspx)
|
* [Reference 3: Introducing Event Sourcing](https://msdn.microsoft.com/en-us/library/jj591559.aspx)
|
||||||
* [Event Sourcing pattern](https://docs.microsoft.com/en-us/azure/architecture/patterns/event-sourcing)
|
* [Event Sourcing pattern](https://docs.microsoft.com/en-us/azure/architecture/patterns/event-sourcing)
|
||||||
|
1
pom.xml
@@ -89,6 +89,7 @@
|
|||||||
<module>state</module>
|
<module>state</module>
|
||||||
<module>strategy</module>
|
<module>strategy</module>
|
||||||
<module>template-method</module>
|
<module>template-method</module>
|
||||||
|
<module>version-number</module>
|
||||||
<module>visitor</module>
|
<module>visitor</module>
|
||||||
<module>double-checked-locking</module>
|
<module>double-checked-locking</module>
|
||||||
<module>servant</module>
|
<module>servant</module>
|
||||||
|
@@ -35,11 +35,6 @@
|
|||||||
</parent>
|
</parent>
|
||||||
<artifactId>trampoline</artifactId>
|
<artifactId>trampoline</artifactId>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
|
||||||
<groupId>junit</groupId>
|
|
||||||
<artifactId>junit</artifactId>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.junit.jupiter</groupId>
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
@@ -98,12 +98,12 @@ public interface Trampoline<T> {
|
|||||||
return trampoline(this);
|
return trampoline(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
T trampoline(final Trampoline<T> trampoline) {
|
private T trampoline(final Trampoline<T> trampoline) {
|
||||||
return Stream.iterate(trampoline, Trampoline::jump)
|
return Stream.iterate(trampoline, Trampoline::jump)
|
||||||
.filter(Trampoline::complete)
|
.filter(Trampoline::complete)
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.map(Trampoline::result)
|
.map(Trampoline::result)
|
||||||
.orElseThrow();
|
.get();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@@ -23,10 +23,9 @@
|
|||||||
|
|
||||||
package com.iluwatar.trampoline;
|
package com.iluwatar.trampoline;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
import org.junit.Test;
|
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test for trampoline pattern.
|
* Test for trampoline pattern.
|
||||||
@@ -37,7 +36,7 @@ public class TrampolineAppTest {
|
|||||||
@Test
|
@Test
|
||||||
public void testTrampolineWithFactorialFunction() {
|
public void testTrampolineWithFactorialFunction() {
|
||||||
long result = TrampolineApp.loop(10, 1).result();
|
long result = TrampolineApp.loop(10, 1).result();
|
||||||
assertEquals("Be equal", 3628800, result);
|
assertEquals(3_628_800, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
169
version-number/README.md
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
---
|
||||||
|
layout: pattern
|
||||||
|
title: Version Number
|
||||||
|
folder: versionnumber
|
||||||
|
permalink: /patterns/versionnumber/
|
||||||
|
description: Entity versioning with version number
|
||||||
|
|
||||||
|
categories:
|
||||||
|
- Concurrency
|
||||||
|
|
||||||
|
tags:
|
||||||
|
- Data access
|
||||||
|
- Microservices
|
||||||
|
---
|
||||||
|
|
||||||
|
## Name / classification
|
||||||
|
|
||||||
|
Version Number.
|
||||||
|
|
||||||
|
## Also known as
|
||||||
|
|
||||||
|
Entity Versioning, Optimistic Locking.
|
||||||
|
|
||||||
|
## Intent
|
||||||
|
|
||||||
|
Resolve concurrency conflicts when multiple clients are trying to update same entity simultaneously.
|
||||||
|
|
||||||
|
## Explanation
|
||||||
|
|
||||||
|
Real world example
|
||||||
|
|
||||||
|
> Alice and Bob are working on the book, which stored in the database. Our heroes are making
|
||||||
|
> changes simultaneously, and we need some mechanism to prevent them from overwriting each other.
|
||||||
|
|
||||||
|
In plain words
|
||||||
|
|
||||||
|
> Version Number pattern grants protection against concurrent updates to same entity.
|
||||||
|
|
||||||
|
Wikipedia says
|
||||||
|
|
||||||
|
> Optimistic concurrency control assumes that multiple transactions can frequently complete
|
||||||
|
> without interfering with each other. While running, transactions use data resources without
|
||||||
|
> acquiring locks on those resources. Before committing, each transaction verifies that no other
|
||||||
|
> transaction has modified the data it has read. If the check reveals conflicting modifications,
|
||||||
|
> the committing transaction rolls back and can be restarted.
|
||||||
|
|
||||||
|
**Programmatic Example**
|
||||||
|
|
||||||
|
We have a `Book` entity, which is versioned, and has a copy-constructor:
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class Book {
|
||||||
|
private long id;
|
||||||
|
private String title = "";
|
||||||
|
private String author = "";
|
||||||
|
|
||||||
|
private long version = 0; // version number
|
||||||
|
|
||||||
|
public Book(Book book) {
|
||||||
|
this.id = book.id;
|
||||||
|
this.title = book.title;
|
||||||
|
this.author = book.author;
|
||||||
|
this.version = book.version;
|
||||||
|
}
|
||||||
|
|
||||||
|
// getters and setters are omitted here
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We also have `BookRepository`, which implements concurrency control:
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class BookRepository {
|
||||||
|
private final Map<Long, Book> collection = new HashMap<>();
|
||||||
|
|
||||||
|
public void update(Book book) throws BookNotFoundException, VersionMismatchException {
|
||||||
|
if (!collection.containsKey(book.getId())) {
|
||||||
|
throw new BookNotFoundException("Not found book with id: " + book.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
var latestBook = collection.get(book.getId());
|
||||||
|
if (book.getVersion() != latestBook.getVersion()) {
|
||||||
|
throw new VersionMismatchException(
|
||||||
|
"Tried to update stale version " + book.getVersion()
|
||||||
|
+ " while actual version is " + latestBook.getVersion()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// update version, including client representation - modify by reference here
|
||||||
|
book.setVersion(book.getVersion() + 1);
|
||||||
|
|
||||||
|
// save book copy to repository
|
||||||
|
collection.put(book.getId(), new Book(book));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Book get(long bookId) throws BookNotFoundException {
|
||||||
|
if (!collection.containsKey(bookId)) {
|
||||||
|
throw new BookNotFoundException("Not found book with id: " + bookId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// return copy of the book
|
||||||
|
return new Book(collection.get(bookId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Here's the concurrency control in action:
|
||||||
|
|
||||||
|
```java
|
||||||
|
var bookId = 1;
|
||||||
|
// Alice and Bob took the book concurrently
|
||||||
|
final var aliceBook = bookRepository.get(bookId);
|
||||||
|
final var bobBook = bookRepository.get(bookId);
|
||||||
|
|
||||||
|
aliceBook.setTitle("Kama Sutra"); // Alice has updated book title
|
||||||
|
bookRepository.update(aliceBook); // and successfully saved book in database
|
||||||
|
LOGGER.info("Alice updates the book with new version {}", aliceBook.getVersion());
|
||||||
|
|
||||||
|
// now Bob has the stale version of the book with empty title and version = 0
|
||||||
|
// while actual book in database has filled title and version = 1
|
||||||
|
bobBook.setAuthor("Vatsyayana Mallanaga"); // Bob updates the author
|
||||||
|
try {
|
||||||
|
LOGGER.info("Bob tries to update the book with his version {}", bobBook.getVersion());
|
||||||
|
bookRepository.update(bobBook); // Bob tries to save his book to database
|
||||||
|
} catch (VersionMismatchException e) {
|
||||||
|
// Bob update fails, and book in repository remained untouchable
|
||||||
|
LOGGER.info("Exception: {}", e.getMessage());
|
||||||
|
// Now Bob should reread actual book from repository, do his changes again and save again
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Program output:
|
||||||
|
|
||||||
|
```java
|
||||||
|
Alice updates the book with new version 1
|
||||||
|
Bob tries to update the book with his version 0
|
||||||
|
Exception: Tried to update stale version 0 while actual version is 1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Class diagram
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Applicability
|
||||||
|
|
||||||
|
Use Version Number for:
|
||||||
|
|
||||||
|
* resolving concurrent write-access to the data
|
||||||
|
* strong data consistency
|
||||||
|
|
||||||
|
## Tutorials
|
||||||
|
* [Version Number Pattern Tutorial](http://www.java2s.com/Tutorial/Java/0355__JPA/VersioningEntity.htm)
|
||||||
|
|
||||||
|
## Known uses
|
||||||
|
* [Hibernate](https://vladmihalcea.com/jpa-entity-version-property-hibernate/)
|
||||||
|
* [Elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-index_.html#index-versioning)
|
||||||
|
* [Apache Solr](https://lucene.apache.org/solr/guide/6_6/updating-parts-of-documents.html)
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
Version Number pattern allows to implement a concurrency control, which is usually done
|
||||||
|
via Optimistic Offline Lock pattern.
|
||||||
|
|
||||||
|
## Related patterns
|
||||||
|
* [Optimistic Offline Lock](https://martinfowler.com/eaaCatalog/optimisticOfflineLock.html)
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
* [Optimistic Locking in JPA](https://www.baeldung.com/jpa-optimistic-locking)
|
||||||
|
* [JPA entity versioning](https://www.byteslounge.com/tutorials/jpa-entity-versioning-version-and-optimistic-locking)
|
||||||
|
* [J2EE Design Patterns](http://ommolketab.ir/aaf-lib/axkwht7wxrhvgs2aqkxse8hihyu9zv.pdf)
|
BIN
version-number/etc/version-number.urm.png
Normal file
After Width: | Height: | Size: 22 KiB |
32
version-number/etc/version-number.urm.puml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
@startuml
|
||||||
|
package com.iluwatar.versionnumber {
|
||||||
|
class App {
|
||||||
|
- LOGGER : Logger {static}
|
||||||
|
+ App()
|
||||||
|
+ main(args : String[]) {static}
|
||||||
|
}
|
||||||
|
class Book {
|
||||||
|
- author : String
|
||||||
|
- id : long
|
||||||
|
- title : String
|
||||||
|
- version : long
|
||||||
|
+ Book()
|
||||||
|
+ Book(book : Book)
|
||||||
|
+ getAuthor() : String
|
||||||
|
+ getId() : long
|
||||||
|
+ getTitle() : String
|
||||||
|
+ getVersion() : long
|
||||||
|
+ setAuthor(author : String)
|
||||||
|
+ setId(id : long)
|
||||||
|
+ setTitle(title : String)
|
||||||
|
+ setVersion(version : long)
|
||||||
|
}
|
||||||
|
class BookRepository {
|
||||||
|
- collection : Map<Long, Book>
|
||||||
|
+ BookRepository()
|
||||||
|
+ add(book : Book)
|
||||||
|
+ get(bookId : long) : Book
|
||||||
|
+ update(book : Book)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@enduml
|
66
version-number/pom.xml
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<!--
|
||||||
|
|
||||||
|
The MIT License
|
||||||
|
Copyright © 2014-2019 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.
|
||||||
|
|
||||||
|
-->
|
||||||
|
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>com.iluwatar</groupId>
|
||||||
|
<artifactId>java-design-patterns</artifactId>
|
||||||
|
<version>1.24.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
<artifactId>version-number</artifactId>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter-engine</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.mockito</groupId>
|
||||||
|
<artifactId>mockito-core</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-assembly-plugin</artifactId>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<configuration>
|
||||||
|
<archive>
|
||||||
|
<manifest>
|
||||||
|
<mainClass>com.iluwatar.versionnumber.App</mainClass>
|
||||||
|
</manifest>
|
||||||
|
</archive>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
@@ -0,0 +1,84 @@
|
|||||||
|
/*
|
||||||
|
* The MIT License
|
||||||
|
* Copyright © 2014-2019 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.versionnumber;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Version Number pattern helps to resolve concurrency conflicts in applications.
|
||||||
|
* Usually these conflicts arise in database operations, when multiple clients are trying
|
||||||
|
* to update the same record simultaneously.
|
||||||
|
* Resolving such conflicts requires determining whether an object has changed.
|
||||||
|
* For this reason we need a version number that is incremented with each change
|
||||||
|
* to the underlying data, e.g. database. The version number can be used by repositories
|
||||||
|
* to check for external changes and to report concurrency issues to the users.
|
||||||
|
*
|
||||||
|
* <p>In this example we show how Alice and Bob will try to update the {@link Book}
|
||||||
|
* and save it simultaneously to {@link BookRepository}, which represents a typical database.
|
||||||
|
*
|
||||||
|
* <p>As in real databases, each client operates with copy of the data instead of original data
|
||||||
|
* passed by reference, that's why we are using {@link Book} copy-constructor here.
|
||||||
|
*/
|
||||||
|
public class App {
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(App.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Program entry point.
|
||||||
|
*
|
||||||
|
* @param args command line args
|
||||||
|
*/
|
||||||
|
public static void main(String[] args) throws
|
||||||
|
BookDuplicateException,
|
||||||
|
BookNotFoundException,
|
||||||
|
VersionMismatchException {
|
||||||
|
var bookId = 1;
|
||||||
|
|
||||||
|
var bookRepository = new BookRepository();
|
||||||
|
var book = new Book();
|
||||||
|
book.setId(bookId);
|
||||||
|
bookRepository.add(book); // adding a book with empty title and author
|
||||||
|
LOGGER.info("An empty book with version {} was added to repository", book.getVersion());
|
||||||
|
|
||||||
|
// Alice and Bob took the book concurrently
|
||||||
|
final var aliceBook = bookRepository.get(bookId);
|
||||||
|
final var bobBook = bookRepository.get(bookId);
|
||||||
|
|
||||||
|
aliceBook.setTitle("Kama Sutra"); // Alice has updated book title
|
||||||
|
bookRepository.update(aliceBook); // and successfully saved book in database
|
||||||
|
LOGGER.info("Alice updates the book with new version {}", aliceBook.getVersion());
|
||||||
|
|
||||||
|
// now Bob has the stale version of the book with empty title and version = 0
|
||||||
|
// while actual book in database has filled title and version = 1
|
||||||
|
bobBook.setAuthor("Vatsyayana Mallanaga"); // Bob updates the author
|
||||||
|
try {
|
||||||
|
LOGGER.info("Bob tries to update the book with his version {}", bobBook.getVersion());
|
||||||
|
bookRepository.update(bobBook); // Bob tries to save his book to database
|
||||||
|
} catch (VersionMismatchException e) {
|
||||||
|
// Bob update fails, and book in repository remained untouchable
|
||||||
|
LOGGER.info("Exception: {}", e.getMessage());
|
||||||
|
// Now Bob should reread actual book from repository, do his changes again and save again
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
* The MIT License
|
||||||
|
* Copyright © 2014-2019 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.versionnumber;
|
||||||
|
|
||||||
|
public class Book {
|
||||||
|
private long id;
|
||||||
|
private String title = "";
|
||||||
|
private String author = "";
|
||||||
|
|
||||||
|
private long version = 0; // version number
|
||||||
|
|
||||||
|
public Book() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We need this copy constructor to copy book representation in {@link BookRepository}.
|
||||||
|
*/
|
||||||
|
public Book(Book book) {
|
||||||
|
this.id = book.id;
|
||||||
|
this.title = book.title;
|
||||||
|
this.author = book.author;
|
||||||
|
this.version = book.version;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTitle() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTitle(String title) {
|
||||||
|
this.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAuthor() {
|
||||||
|
return author;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAuthor(String author) {
|
||||||
|
this.author = author;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getVersion() {
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setVersion(long version) {
|
||||||
|
this.version = version;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* The MIT License
|
||||||
|
* Copyright © 2014-2019 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.versionnumber;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When someone has tried to add a book which repository already have.
|
||||||
|
*/
|
||||||
|
public class BookDuplicateException extends Exception {
|
||||||
|
public BookDuplicateException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* The MIT License
|
||||||
|
* Copyright © 2014-2019 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.versionnumber;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Client has tried to make an operation with book which repository does not have.
|
||||||
|
*/
|
||||||
|
public class BookNotFoundException extends Exception {
|
||||||
|
public BookNotFoundException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,86 @@
|
|||||||
|
/*
|
||||||
|
* The MIT License
|
||||||
|
* Copyright © 2014-2019 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.versionnumber;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This repository represents simplified database.
|
||||||
|
* As a typical database do, repository operates with copies of object.
|
||||||
|
* So client and repo has different copies of book, which can lead to concurrency conflicts
|
||||||
|
* as much as in real databases.
|
||||||
|
*/
|
||||||
|
public class BookRepository {
|
||||||
|
private final Map<Long, Book> collection = new HashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds book to collection.
|
||||||
|
* Actually we are putting copy of book (saving a book by value, not by reference);
|
||||||
|
*/
|
||||||
|
public void add(Book book) throws BookDuplicateException {
|
||||||
|
if (collection.containsKey(book.getId())) {
|
||||||
|
throw new BookDuplicateException("Duplicated book with id: " + book.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
// add copy of the book
|
||||||
|
collection.put(book.getId(), new Book(book));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates book in collection only if client has modified the latest version of the book.
|
||||||
|
*/
|
||||||
|
public void update(Book book) throws BookNotFoundException, VersionMismatchException {
|
||||||
|
if (!collection.containsKey(book.getId())) {
|
||||||
|
throw new BookNotFoundException("Not found book with id: " + book.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
var latestBook = collection.get(book.getId());
|
||||||
|
if (book.getVersion() != latestBook.getVersion()) {
|
||||||
|
throw new VersionMismatchException(
|
||||||
|
"Tried to update stale version " + book.getVersion()
|
||||||
|
+ " while actual version is " + latestBook.getVersion()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// update version, including client representation - modify by reference here
|
||||||
|
book.setVersion(book.getVersion() + 1);
|
||||||
|
|
||||||
|
// save book copy to repository
|
||||||
|
collection.put(book.getId(), new Book(book));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns book representation to the client.
|
||||||
|
* Representation means we are returning copy of the book.
|
||||||
|
*/
|
||||||
|
public Book get(long bookId) throws BookNotFoundException {
|
||||||
|
if (!collection.containsKey(bookId)) {
|
||||||
|
throw new BookNotFoundException("Not found book with id: " + bookId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// return copy of the book
|
||||||
|
return new Book(collection.get(bookId));
|
||||||
|
}
|
||||||
|
}
|
@@ -1,37 +1,33 @@
|
|||||||
/*
|
/*
|
||||||
* The MIT License
|
* The MIT License
|
||||||
* Copyright © 2014-2019 Ilkka Seppälä
|
* Copyright © 2014-2019 Ilkka Seppälä
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
* in the Software without restriction, including without limitation the rights
|
* in the Software without restriction, including without limitation the rights
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
* furnished to do so, subject to the following conditions:
|
* furnished to do so, subject to the following conditions:
|
||||||
*
|
*
|
||||||
* The above copyright notice and this permission notice shall be included in
|
* The above copyright notice and this permission notice shall be included in
|
||||||
* all copies or substantial portions of the Software.
|
* all copies or substantial portions of the Software.
|
||||||
*
|
*
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
* 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
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
* THE SOFTWARE.
|
* THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.iluwatar.command;
|
package com.iluwatar.versionnumber;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for Commands.
|
* Client has tried to update a stale version of the book.
|
||||||
*/
|
*/
|
||||||
public interface Command {
|
public class VersionMismatchException extends Exception {
|
||||||
void execute(Target target);
|
public VersionMismatchException(String message) {
|
||||||
|
super(message);
|
||||||
void undo();
|
}
|
||||||
|
}
|
||||||
void redo();
|
|
||||||
|
|
||||||
String toString();
|
|
||||||
}
|
|
@@ -21,37 +21,26 @@
|
|||||||
* THE SOFTWARE.
|
* THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.iluwatar.command;
|
package com.iluwatar.versionnumber;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* InvisibilitySpell is a concrete command.
|
* Application test
|
||||||
*/
|
*/
|
||||||
public class InvisibilitySpell implements Command {
|
class AppTest {
|
||||||
|
|
||||||
private Target target;
|
/**
|
||||||
|
* Issue: Add at least one assertion to this test case.
|
||||||
|
*
|
||||||
|
* Solution: Inserted assertion to check whether the execution of the main method in {@link App#main(String[])}
|
||||||
|
* throws an exception.
|
||||||
|
*/
|
||||||
|
|
||||||
@Override
|
@Test
|
||||||
public void execute(Target target) {
|
void shouldExecuteApplicationWithoutException() {
|
||||||
target.setVisibility(Visibility.INVISIBLE);
|
assertDoesNotThrow(() -> App.main(new String[]{}));
|
||||||
this.target = target;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void undo() {
|
|
||||||
if (target != null) {
|
|
||||||
target.setVisibility(Visibility.VISIBLE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void redo() {
|
|
||||||
if (target != null) {
|
|
||||||
target.setVisibility(Visibility.INVISIBLE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "Invisibility spell";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
* The MIT License
|
||||||
|
* Copyright © 2014-2019 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.versionnumber;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link BookRepository}
|
||||||
|
*/
|
||||||
|
class BookRepositoryTest {
|
||||||
|
private final long bookId = 1;
|
||||||
|
private final BookRepository bookRepository = new BookRepository();
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void setUp() throws BookDuplicateException {
|
||||||
|
var book = new Book();
|
||||||
|
book.setId(bookId);
|
||||||
|
bookRepository.add(book);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDefaultVersionRemainsZeroAfterAdd() throws BookNotFoundException {
|
||||||
|
var book = bookRepository.get(bookId);
|
||||||
|
assertEquals(0, book.getVersion());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testAliceAndBobHaveDifferentVersionsAfterAliceUpdate() throws BookNotFoundException, VersionMismatchException {
|
||||||
|
final var aliceBook = bookRepository.get(bookId);
|
||||||
|
final var bobBook = bookRepository.get(bookId);
|
||||||
|
|
||||||
|
aliceBook.setTitle("Kama Sutra");
|
||||||
|
bookRepository.update(aliceBook);
|
||||||
|
|
||||||
|
assertEquals(1, aliceBook.getVersion());
|
||||||
|
assertEquals(0, bobBook.getVersion());
|
||||||
|
var actualBook = bookRepository.get(bookId);
|
||||||
|
assertEquals(aliceBook.getVersion(), actualBook.getVersion());
|
||||||
|
assertEquals(aliceBook.getTitle(), actualBook.getTitle());
|
||||||
|
assertNotEquals(aliceBook.getTitle(), bobBook.getTitle());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testShouldThrowVersionMismatchExceptionOnStaleUpdate() throws BookNotFoundException, VersionMismatchException {
|
||||||
|
final var aliceBook = bookRepository.get(bookId);
|
||||||
|
final var bobBook = bookRepository.get(bookId);
|
||||||
|
|
||||||
|
aliceBook.setTitle("Kama Sutra");
|
||||||
|
bookRepository.update(aliceBook);
|
||||||
|
|
||||||
|
bobBook.setAuthor("Vatsyayana Mallanaga");
|
||||||
|
try {
|
||||||
|
bookRepository.update(bobBook);
|
||||||
|
} catch (VersionMismatchException e) {
|
||||||
|
assertEquals(0, bobBook.getVersion());
|
||||||
|
var actualBook = bookRepository.get(bookId);
|
||||||
|
assertEquals(1, actualBook.getVersion());
|
||||||
|
assertEquals(aliceBook.getVersion(), actualBook.getVersion());
|
||||||
|
assertEquals("", bobBook.getTitle());
|
||||||
|
assertNotEquals(aliceBook.getAuthor(), bobBook.getAuthor());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
139
zh/builder/README.md
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
---
|
||||||
|
layout: pattern
|
||||||
|
title: Builder
|
||||||
|
folder: builder
|
||||||
|
permalink: /patterns/builder/
|
||||||
|
categories: Creational
|
||||||
|
tags:
|
||||||
|
- Gang of Four
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 目的
|
||||||
|
|
||||||
|
将复杂对象的构造与其表示分开,以便同一构造过程可以创建不同的表示。
|
||||||
|
|
||||||
|
## 解释
|
||||||
|
|
||||||
|
现实世界例子
|
||||||
|
|
||||||
|
> 想象一个角色扮演游戏的角色生成器。最简单的选择是让计算机为你创建角色。但是如果你想选择一些像专业,性别,发色等角色细节时,这个角色生成就变成了一个渐进的过程。当所有选择完成时,该过程也将完成。
|
||||||
|
|
||||||
|
用通俗的话说
|
||||||
|
|
||||||
|
> 允许你创建不同口味的对象同时避免构造器污染。当一个对象可能有几种口味,或者一个对象的创建涉及到很多步骤时会很有用。
|
||||||
|
|
||||||
|
维基百科说
|
||||||
|
|
||||||
|
> 建造者模式是一种对象创建的软件设计模式,旨在为伸缩构造器反模式寻找一个解决方案。
|
||||||
|
|
||||||
|
说了这么多,让我补充一下什么是伸缩构造函数反模式。我们肯定都见过像下面这样的构造器:
|
||||||
|
|
||||||
|
```java
|
||||||
|
public Hero(Profession profession, String name, HairType hairType, HairColor hairColor, Armor armor, Weapon weapon) {
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
就像你看到的构造器参数的数量很快就会失控同时参数的排列方式可能变得难以理解。另外,如果您将来希望添加更多选项,则此参数列表可能会继续增长。这就被称伸缩构造器反模式。
|
||||||
|
|
||||||
|
**编程示例**
|
||||||
|
|
||||||
|
明智的选择是使用建造者模式。首先我们有一个英雄要创建。
|
||||||
|
|
||||||
|
```java
|
||||||
|
public final class Hero {
|
||||||
|
private final Profession profession;
|
||||||
|
private final String name;
|
||||||
|
private final HairType hairType;
|
||||||
|
private final HairColor hairColor;
|
||||||
|
private final Armor armor;
|
||||||
|
private final Weapon weapon;
|
||||||
|
|
||||||
|
private Hero(Builder builder) {
|
||||||
|
this.profession = builder.profession;
|
||||||
|
this.name = builder.name;
|
||||||
|
this.hairColor = builder.hairColor;
|
||||||
|
this.hairType = builder.hairType;
|
||||||
|
this.weapon = builder.weapon;
|
||||||
|
this.armor = builder.armor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
然后我们有创建者
|
||||||
|
|
||||||
|
```java
|
||||||
|
public static class Builder {
|
||||||
|
private final Profession profession;
|
||||||
|
private final String name;
|
||||||
|
private HairType hairType;
|
||||||
|
private HairColor hairColor;
|
||||||
|
private Armor armor;
|
||||||
|
private Weapon weapon;
|
||||||
|
|
||||||
|
public Builder(Profession profession, String name) {
|
||||||
|
if (profession == null || name == null) {
|
||||||
|
throw new IllegalArgumentException("profession and name can not be null");
|
||||||
|
}
|
||||||
|
this.profession = profession;
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withHairType(HairType hairType) {
|
||||||
|
this.hairType = hairType;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withHairColor(HairColor hairColor) {
|
||||||
|
this.hairColor = hairColor;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withArmor(Armor armor) {
|
||||||
|
this.armor = armor;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withWeapon(Weapon weapon) {
|
||||||
|
this.weapon = weapon;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Hero build() {
|
||||||
|
return new Hero(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
然后可以这样使用
|
||||||
|
|
||||||
|
```java
|
||||||
|
var mage = new Hero.Builder(Profession.MAGE, "Riobard").withHairColor(HairColor.BLACK).withWeapon(Weapon.DAGGER).build();
|
||||||
|
```
|
||||||
|
|
||||||
|
## 类图
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 适用性
|
||||||
|
|
||||||
|
使用建造者模式当
|
||||||
|
|
||||||
|
* 创建复杂对象的算法应独立于组成对象的零件及其组装方式
|
||||||
|
* 构造过程必须允许所构造的对象具有不同的表示形式
|
||||||
|
|
||||||
|
## Java世界例子
|
||||||
|
|
||||||
|
* [java.lang.StringBuilder](http://docs.oracle.com/javase/8/docs/api/java/lang/StringBuilder.html)
|
||||||
|
* [java.nio.ByteBuffer](http://docs.oracle.com/javase/8/docs/api/java/nio/ByteBuffer.html#put-byte-) as well as similar buffers such as FloatBuffer, IntBuffer and so on.
|
||||||
|
* [java.lang.StringBuffer](http://docs.oracle.com/javase/8/docs/api/java/lang/StringBuffer.html#append-boolean-)
|
||||||
|
* All implementations of [java.lang.Appendable](http://docs.oracle.com/javase/8/docs/api/java/lang/Appendable.html)
|
||||||
|
* [Apache Camel builders](https://github.com/apache/camel/tree/0e195428ee04531be27a0b659005e3aa8d159d23/camel-core/src/main/java/org/apache/camel/builder)
|
||||||
|
* [Apache Commons Option.Builder](https://commons.apache.org/proper/commons-cli/apidocs/org/apache/commons/cli/Option.Builder.html)
|
||||||
|
|
||||||
|
## 鸣谢
|
||||||
|
|
||||||
|
* [Design Patterns: Elements of Reusable Object-Oriented Software](https://www.amazon.com/gp/product/0201633612/ref=as_li_tl?ie=UTF8&camp=1789&creative=9325&creativeASIN=0201633612&linkCode=as2&tag=javadesignpat-20&linkId=675d49790ce11db99d90bde47f1aeb59)
|
||||||
|
* [Effective Java](https://www.amazon.com/gp/product/0134685997/ref=as_li_tl?ie=UTF8&camp=1789&creative=9325&creativeASIN=0134685997&linkCode=as2&tag=javadesignpat-20&linkId=4e349f4b3ff8c50123f8147c828e53eb)
|
||||||
|
* [Head First Design Patterns: A Brain-Friendly Guide](https://www.amazon.com/gp/product/0596007124/ref=as_li_tl?ie=UTF8&camp=1789&creative=9325&creativeASIN=0596007124&linkCode=as2&tag=javadesignpat-20&linkId=6b8b6eea86021af6c8e3cd3fc382cb5b)
|
||||||
|
* [Refactoring to Patterns](https://www.amazon.com/gp/product/0321213351/ref=as_li_tl?ie=UTF8&camp=1789&creative=9325&creativeASIN=0321213351&linkCode=as2&tag=javadesignpat-20&linkId=2a76fcb387234bc71b1c61150b3cc3a7)
|
159
zh/chain/README.md
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
---
|
||||||
|
layout: pattern
|
||||||
|
title: Chain of responsibility
|
||||||
|
folder: chain
|
||||||
|
permalink: /patterns/chain/
|
||||||
|
categories: Behavioral
|
||||||
|
tags:
|
||||||
|
- Gang of Four
|
||||||
|
---
|
||||||
|
|
||||||
|
## 目的
|
||||||
|
通过给多个对象一个处理请求的机会,避免请求的发送者和它的接收者耦合。串联接收对象并在链条中传递请求直到一个对象处理它。
|
||||||
|
|
||||||
|
## 解释
|
||||||
|
|
||||||
|
真实世界例子
|
||||||
|
|
||||||
|
> 兽王大声命令他的军队。最近响应的是指挥官,然后是军官,然后是士兵。指挥官,军官,士兵这里就形成了一个责任链。
|
||||||
|
|
||||||
|
通俗的说
|
||||||
|
|
||||||
|
> 它帮助构建一串对象。请求从一个对象中进入并结束然后进入到一个个对象中直到找到合适的处理器。
|
||||||
|
|
||||||
|
维基百科说
|
||||||
|
|
||||||
|
> 在面向对象设计中,责任链模式是一种由源命令对象和一系列处理对象组成的设计模式。每个处理对象包含了其定义的可处理的命令对象类型的逻辑。剩下的会传递给链条中的下一个处理对象。
|
||||||
|
|
||||||
|
**程序示例**
|
||||||
|
|
||||||
|
用上面的兽人来翻译我们的示例。首先我们有请求类
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class Request {
|
||||||
|
|
||||||
|
private final RequestType requestType;
|
||||||
|
private final String requestDescription;
|
||||||
|
private boolean handled;
|
||||||
|
|
||||||
|
public Request(final RequestType requestType, final String requestDescription) {
|
||||||
|
this.requestType = Objects.requireNonNull(requestType);
|
||||||
|
this.requestDescription = Objects.requireNonNull(requestDescription);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRequestDescription() { return requestDescription; }
|
||||||
|
|
||||||
|
public RequestType getRequestType() { return requestType; }
|
||||||
|
|
||||||
|
public void markHandled() { this.handled = true; }
|
||||||
|
|
||||||
|
public boolean isHandled() { return this.handled; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() { return getRequestDescription(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum RequestType {
|
||||||
|
DEFEND_CASTLE, TORTURE_PRISONER, COLLECT_TAX
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
然后是请求处理器的层次结构
|
||||||
|
|
||||||
|
```java
|
||||||
|
public abstract class RequestHandler {
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(RequestHandler.class);
|
||||||
|
private final RequestHandler next;
|
||||||
|
|
||||||
|
public RequestHandler(RequestHandler next) {
|
||||||
|
this.next = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void handleRequest(Request req) {
|
||||||
|
if (next != null) {
|
||||||
|
next.handleRequest(req);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void printHandling(Request req) {
|
||||||
|
LOGGER.info("{} handling request \"{}\"", this, req);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public abstract String toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class OrcCommander extends RequestHandler {
|
||||||
|
public OrcCommander(RequestHandler handler) {
|
||||||
|
super(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleRequest(Request req) {
|
||||||
|
if (req.getRequestType().equals(RequestType.DEFEND_CASTLE)) {
|
||||||
|
printHandling(req);
|
||||||
|
req.markHandled();
|
||||||
|
} else {
|
||||||
|
super.handleRequest(req);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "Orc commander";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OrcOfficer和OrcSoldier的定义与OrcCommander类似
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
然后我们有兽王下达命令并形成链条
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class OrcKing {
|
||||||
|
RequestHandler chain;
|
||||||
|
|
||||||
|
public OrcKing() {
|
||||||
|
buildChain();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildChain() {
|
||||||
|
chain = new OrcCommander(new OrcOfficer(new OrcSoldier(null)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void makeRequest(Request req) {
|
||||||
|
chain.handleRequest(req);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
然后这样使用它
|
||||||
|
|
||||||
|
```java
|
||||||
|
var king = new OrcKing();
|
||||||
|
king.makeRequest(new Request(RequestType.DEFEND_CASTLE, "defend castle")); // Orc commander handling request "defend castle"
|
||||||
|
king.makeRequest(new Request(RequestType.TORTURE_PRISONER, "torture prisoner")); // Orc officer handling request "torture prisoner"
|
||||||
|
king.makeRequest(new Request(RequestType.COLLECT_TAX, "collect tax")); // Orc soldier handling request "collect tax"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 类图
|
||||||
|

|
||||||
|
|
||||||
|
## 适用性
|
||||||
|
使用责任链模式当
|
||||||
|
|
||||||
|
* 多于一个对象可能要处理请求,并且处理器并不知道一个优先级。处理器应自动确定。
|
||||||
|
* 你想对多个对象之一发出请求而无需明确指定接收者
|
||||||
|
* 处理请求的对象集合应该被动态指定时
|
||||||
|
|
||||||
|
## Java世界例子
|
||||||
|
|
||||||
|
* [java.util.logging.Logger#log()](http://docs.oracle.com/javase/8/docs/api/java/util/logging/Logger.html#log%28java.util.logging.Level,%20java.lang.String%29)
|
||||||
|
* [Apache Commons Chain](https://commons.apache.org/proper/commons-chain/index.html)
|
||||||
|
* [javax.servlet.Filter#doFilter()](http://docs.oracle.com/javaee/7/api/javax/servlet/Filter.html#doFilter-javax.servlet.ServletRequest-javax.servlet.ServletResponse-javax.servlet.FilterChain-)
|
||||||
|
|
||||||
|
## 鸣谢
|
||||||
|
|
||||||
|
* [Design Patterns: Elements of Reusable Object-Oriented Software](https://www.amazon.com/gp/product/0201633612/ref=as_li_tl?ie=UTF8&camp=1789&creative=9325&creativeASIN=0201633612&linkCode=as2&tag=javadesignpat-20&linkId=675d49790ce11db99d90bde47f1aeb59)
|
||||||
|
* [Head First Design Patterns: A Brain-Friendly Guide](https://www.amazon.com/gp/product/0596007124/ref=as_li_tl?ie=UTF8&camp=1789&creative=9325&creativeASIN=0596007124&linkCode=as2&tag=javadesignpat-20&linkId=6b8b6eea86021af6c8e3cd3fc382cb5b)
|
139
zh/decorator/README.md
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
---
|
||||||
|
layout: pattern
|
||||||
|
title: Decorator
|
||||||
|
folder: decorator
|
||||||
|
permalink: /patterns/decorator/
|
||||||
|
categories: Structural
|
||||||
|
tags:
|
||||||
|
- Gang Of Four
|
||||||
|
- Extensibility
|
||||||
|
---
|
||||||
|
|
||||||
|
## 或称
|
||||||
|
包装器
|
||||||
|
|
||||||
|
## 目的
|
||||||
|
动态的为对象附加额外的职责。装饰器为子类提供了灵活的替代方案,以扩展功能。
|
||||||
|
|
||||||
|
## 解释
|
||||||
|
|
||||||
|
真实世界例子
|
||||||
|
|
||||||
|
> 附近的山丘上住着一个愤怒的巨魔。通常它是徒手的,但有时它有武器。为了武装巨魔不必创建新的巨魔,而是用合适的武器动态的装饰它。
|
||||||
|
|
||||||
|
通俗的说
|
||||||
|
|
||||||
|
> 装饰者模式让你可以在运行时通过把对象包装进一个装饰类对象中来动态的改变一个对象的行为。
|
||||||
|
|
||||||
|
维基百科说
|
||||||
|
|
||||||
|
> 在面向对象的编程中,装饰器模式是一种设计模式,它允许将行为静态或动态地添加到单个对象中,而不会影响同一类中其他对象的行为。装饰器模式通常对于遵守单一责任原则很有用,因为它允许将功能划分到具有唯一关注领域的类之间。
|
||||||
|
|
||||||
|
**程序示例**
|
||||||
|
|
||||||
|
以巨魔的为例。首先我有有一个简单的巨魔,实现了巨魔接口。
|
||||||
|
|
||||||
|
程序mple. First of all we have a simple troll implementing the troll interface
|
||||||
|
|
||||||
|
```java
|
||||||
|
public interface Troll {
|
||||||
|
void attack();
|
||||||
|
int getAttackPower();
|
||||||
|
void fleeBattle();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SimpleTroll implements Troll {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(SimpleTroll.class);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void attack() {
|
||||||
|
LOGGER.info("The troll tries to grab you!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getAttackPower() {
|
||||||
|
return 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void fleeBattle() {
|
||||||
|
LOGGER.info("The troll shrieks in horror and runs away!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
下面我们想为巨魔添加球棒。我们可以用装饰者来动态的实现。
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class ClubbedTroll implements Troll {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(ClubbedTroll.class);
|
||||||
|
|
||||||
|
private final Troll decorated;
|
||||||
|
|
||||||
|
public ClubbedTroll(Troll decorated) {
|
||||||
|
this.decorated = decorated;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void attack() {
|
||||||
|
decorated.attack();
|
||||||
|
LOGGER.info("The troll swings at you with a club!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getAttackPower() {
|
||||||
|
return decorated.getAttackPower() + 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void fleeBattle() {
|
||||||
|
decorated.fleeBattle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这里是巨魔的实战
|
||||||
|
|
||||||
|
```java
|
||||||
|
// simple troll
|
||||||
|
var troll = new SimpleTroll();
|
||||||
|
troll.attack(); // The troll tries to grab you!
|
||||||
|
troll.fleeBattle(); // The troll shrieks in horror and runs away!
|
||||||
|
|
||||||
|
// change the behavior of the simple troll by adding a decorator
|
||||||
|
var clubbedTroll = new ClubbedTroll(troll);
|
||||||
|
clubbedTroll.attack(); // The troll tries to grab you! The troll swings at you with a club!
|
||||||
|
clubbedTroll.fleeBattle(); // The troll shrieks in horror and runs away!
|
||||||
|
```
|
||||||
|
|
||||||
|
## 类图
|
||||||
|

|
||||||
|
|
||||||
|
## 适用性
|
||||||
|
使用装饰者
|
||||||
|
|
||||||
|
* 动态透明地向单个对象添加职责,即不影响其他对象
|
||||||
|
* 对于可以撤销的责任
|
||||||
|
* 当通过子类化进行扩展是不切实际的。有时可能会有大量的独立扩展,并且会产生大量的子类来支持每种组合。 否则类定义可能被隐藏或无法用于子类化。
|
||||||
|
|
||||||
|
## 教程
|
||||||
|
* [Decorator Pattern Tutorial](https://www.journaldev.com/1540/decorator-design-pattern-in-java-example)
|
||||||
|
|
||||||
|
## Java世界的例子
|
||||||
|
* [java.io.InputStream](http://docs.oracle.com/javase/8/docs/api/java/io/InputStream.html), [java.io.OutputStream](http://docs.oracle.com/javase/8/docs/api/java/io/OutputStream.html),
|
||||||
|
[java.io.Reader](http://docs.oracle.com/javase/8/docs/api/java/io/Reader.html) and [java.io.Writer](http://docs.oracle.com/javase/8/docs/api/java/io/Writer.html)
|
||||||
|
* [java.util.Collections#synchronizedXXX()](http://docs.oracle.com/javase/8/docs/api/java/util/Collections.html#synchronizedCollection-java.util.Collection-)
|
||||||
|
* [java.util.Collections#unmodifiableXXX()](http://docs.oracle.com/javase/8/docs/api/java/util/Collections.html#unmodifiableCollection-java.util.Collection-)
|
||||||
|
* [java.util.Collections#checkedXXX()](http://docs.oracle.com/javase/8/docs/api/java/util/Collections.html#checkedCollection-java.util.Collection-java.lang.Class-)
|
||||||
|
|
||||||
|
|
||||||
|
## 鸣谢
|
||||||
|
|
||||||
|
* [Design Patterns: Elements of Reusable Object-Oriented Software](https://www.amazon.com/gp/product/0201633612/ref=as_li_tl?ie=UTF8&camp=1789&creative=9325&creativeASIN=0201633612&linkCode=as2&tag=javadesignpat-20&linkId=675d49790ce11db99d90bde47f1aeb59)
|
||||||
|
* [Functional Programming in Java: Harnessing the Power of Java 8 Lambda Expressions](https://www.amazon.com/gp/product/1937785467/ref=as_li_tl?ie=UTF8&camp=1789&creative=9325&creativeASIN=1937785467&linkCode=as2&tag=javadesignpat-20&linkId=7e4e2fb7a141631491534255252fd08b)
|
||||||
|
* [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)
|
||||||
|
* [Head First Design Patterns: A Brain-Friendly Guide](https://www.amazon.com/gp/product/0596007124/ref=as_li_tl?ie=UTF8&camp=1789&creative=9325&creativeASIN=0596007124&linkCode=as2&tag=javadesignpat-20&linkId=6b8b6eea86021af6c8e3cd3fc382cb5b)
|
||||||
|
* [Refactoring to Patterns](https://www.amazon.com/gp/product/0321213351/ref=as_li_tl?ie=UTF8&camp=1789&creative=9325&creativeASIN=0321213351&linkCode=as2&tag=javadesignpat-20&linkId=2a76fcb387234bc71b1c61150b3cc3a7)
|
||||||
|
* [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=f27d2644fbe5026ea448791a8ad09c94)
|
207
zh/facade/README.md
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
---
|
||||||
|
layout: pattern
|
||||||
|
title: Facade
|
||||||
|
folder: facade
|
||||||
|
permalink: /patterns/facade/
|
||||||
|
categories: Structural
|
||||||
|
tags:
|
||||||
|
- Gang Of Four
|
||||||
|
- Decoupling
|
||||||
|
---
|
||||||
|
|
||||||
|
## 目的
|
||||||
|
为一个子系统中的一系列接口提供一个统一的接口。外观定义了一个更高级别的接口以便子系统更容易使用。
|
||||||
|
|
||||||
|
## 解释
|
||||||
|
|
||||||
|
真实世界的例子
|
||||||
|
|
||||||
|
> 一个金矿是怎么工作的?“嗯,矿工下去然后挖金子!”你说。这是你所相信的因为你在使用一个金矿对外提供的一个简单接口,在内部它要却要做很多事情。这个简单的接口对复杂的子系统来说就是一个外观。
|
||||||
|
|
||||||
|
用通俗的话说
|
||||||
|
|
||||||
|
> 外观模式为一个复杂的子系统提供一个简单的接口。
|
||||||
|
|
||||||
|
维基百科说
|
||||||
|
|
||||||
|
> 外观是为很大体量的代码(比如类库)提供简单接口的一种对象。
|
||||||
|
|
||||||
|
**程序示例**
|
||||||
|
|
||||||
|
使用上面金矿的例子。这里我们有矮人的矿工等级制度。
|
||||||
|
|
||||||
|
```java
|
||||||
|
public abstract class DwarvenMineWorker {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(DwarvenMineWorker.class);
|
||||||
|
|
||||||
|
public void goToSleep() {
|
||||||
|
LOGGER.info("{} goes to sleep.", name());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void wakeUp() {
|
||||||
|
LOGGER.info("{} wakes up.", name());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void goHome() {
|
||||||
|
LOGGER.info("{} goes home.", name());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void goToMine() {
|
||||||
|
LOGGER.info("{} goes to the mine.", name());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void action(Action action) {
|
||||||
|
switch (action) {
|
||||||
|
case GO_TO_SLEEP:
|
||||||
|
goToSleep();
|
||||||
|
break;
|
||||||
|
case WAKE_UP:
|
||||||
|
wakeUp();
|
||||||
|
break;
|
||||||
|
case GO_HOME:
|
||||||
|
goHome();
|
||||||
|
break;
|
||||||
|
case GO_TO_MINE:
|
||||||
|
goToMine();
|
||||||
|
break;
|
||||||
|
case WORK:
|
||||||
|
work();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
LOGGER.info("Undefined action");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void action(Action... actions) {
|
||||||
|
Arrays.stream(actions).forEach(this::action);
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract void work();
|
||||||
|
|
||||||
|
public abstract String name();
|
||||||
|
|
||||||
|
enum Action {
|
||||||
|
GO_TO_SLEEP, WAKE_UP, GO_HOME, GO_TO_MINE, WORK
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DwarvenTunnelDigger extends DwarvenMineWorker {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(DwarvenTunnelDigger.class);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void work() {
|
||||||
|
LOGGER.info("{} creates another promising tunnel.", name());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String name() {
|
||||||
|
return "Dwarven tunnel digger";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DwarvenGoldDigger extends DwarvenMineWorker {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(DwarvenGoldDigger.class);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void work() {
|
||||||
|
LOGGER.info("{} digs for gold.", name());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String name() {
|
||||||
|
return "Dwarf gold digger";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DwarvenCartOperator extends DwarvenMineWorker {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(DwarvenCartOperator.class);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void work() {
|
||||||
|
LOGGER.info("{} moves gold chunks out of the mine.", name());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String name() {
|
||||||
|
return "Dwarf cart operator";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
为了操纵所有这些矿工我们有了这个外观
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class DwarvenGoldmineFacade {
|
||||||
|
|
||||||
|
private final List<DwarvenMineWorker> workers;
|
||||||
|
|
||||||
|
public DwarvenGoldmineFacade() {
|
||||||
|
workers = List.of(
|
||||||
|
new DwarvenGoldDigger(),
|
||||||
|
new DwarvenCartOperator(),
|
||||||
|
new DwarvenTunnelDigger());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startNewDay() {
|
||||||
|
makeActions(workers, DwarvenMineWorker.Action.WAKE_UP, DwarvenMineWorker.Action.GO_TO_MINE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void digOutGold() {
|
||||||
|
makeActions(workers, DwarvenMineWorker.Action.WORK);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void endDay() {
|
||||||
|
makeActions(workers, DwarvenMineWorker.Action.GO_HOME, DwarvenMineWorker.Action.GO_TO_SLEEP);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void makeActions(Collection<DwarvenMineWorker> workers,
|
||||||
|
DwarvenMineWorker.Action... actions) {
|
||||||
|
workers.forEach(worker -> worker.action(actions));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
现在来使用外观
|
||||||
|
|
||||||
|
```java
|
||||||
|
DwarvenGoldmineFacade facade = new DwarvenGoldmineFacade();
|
||||||
|
facade.startNewDay();
|
||||||
|
// Dwarf gold digger wakes up.
|
||||||
|
// Dwarf gold digger goes to the mine.
|
||||||
|
// Dwarf cart operator wakes up.
|
||||||
|
// Dwarf cart operator goes to the mine.
|
||||||
|
// Dwarven tunnel digger wakes up.
|
||||||
|
// Dwarven tunnel digger goes to the mine.
|
||||||
|
facade.digOutGold();
|
||||||
|
// Dwarf gold digger digs for gold.
|
||||||
|
// Dwarf cart operator moves gold chunks out of the mine.
|
||||||
|
// Dwarven tunnel digger creates another promising tunnel.
|
||||||
|
facade.endDay();
|
||||||
|
// Dwarf gold digger goes home.
|
||||||
|
// Dwarf gold digger goes to sleep.
|
||||||
|
// Dwarf cart operator goes home.
|
||||||
|
// Dwarf cart operator goes to sleep.
|
||||||
|
// Dwarven tunnel digger goes home.
|
||||||
|
// Dwarven tunnel digger goes to sleep.
|
||||||
|
```
|
||||||
|
|
||||||
|
## 类图
|
||||||
|

|
||||||
|
|
||||||
|
## 适用性
|
||||||
|
使用外观模式当
|
||||||
|
|
||||||
|
* 你想为一个复杂的子系统提供一个简单的接口。随着子系统的发展,它们通常会变得更加复杂。多数模式在应用时会导致更多和更少的类。这使子系统更可重用,更易于自定义,但是对于不需要自定义它的客户来说,使用它也变得更加困难。 外观可以提供子系统的简单默认视图,足以满足大多数客户端的需求。只有需要更多可定制性的客户才需要查看外观外的东西(原子系统提供的接口)。
|
||||||
|
* 客户端与抽象的实现类之间存在许多依赖关系。 引入外观以使子系统与客户端和其他子系统分离,从而提高子系统的独立性和可移植性。
|
||||||
|
* 您想对子系统进行分层。 使用外观来定义每个子系统级别的入口点。 如果子系统是相关的,则可以通过使子系统仅通过其外观相互通信来简化它们之间的依赖性。
|
||||||
|
|
||||||
|
## 鸣谢
|
||||||
|
|
||||||
|
* [Design Patterns: Elements of Reusable Object-Oriented Software](https://www.amazon.com/gp/product/0201633612/ref=as_li_tl?ie=UTF8&camp=1789&creative=9325&creativeASIN=0201633612&linkCode=as2&tag=javadesignpat-20&linkId=675d49790ce11db99d90bde47f1aeb59)
|
||||||
|
* [Head First Design Patterns: A Brain-Friendly Guide](https://www.amazon.com/gp/product/0596007124/ref=as_li_tl?ie=UTF8&camp=1789&creative=9325&creativeASIN=0596007124&linkCode=as2&tag=javadesignpat-20&linkId=6b8b6eea86021af6c8e3cd3fc382cb5b)
|
88
zh/factory-method/README.md
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
---
|
||||||
|
layout: pattern
|
||||||
|
title: Factory Method
|
||||||
|
folder: factory-method
|
||||||
|
permalink: /patterns/factory-method/
|
||||||
|
categories: Creational
|
||||||
|
tags:
|
||||||
|
- Extensibility
|
||||||
|
- Gang Of Four
|
||||||
|
---
|
||||||
|
|
||||||
|
## Also known as
|
||||||
|
# 或称
|
||||||
|
|
||||||
|
虚拟构造器
|
||||||
|
|
||||||
|
## 目的
|
||||||
|
为创建一个对象定义一个接口,但是让子类决定实例化哪个类。工厂方法允许类将实例化延迟到子类。
|
||||||
|
|
||||||
|
## 解释
|
||||||
|
真实世界例子
|
||||||
|
|
||||||
|
> 铁匠生产武器。精灵需要精灵武器,而兽人需要兽人武器。根据客户来召唤正确类型的铁匠。
|
||||||
|
|
||||||
|
通俗的说
|
||||||
|
|
||||||
|
> 它为类提供了一种把实例化的逻辑委托给子类的方式。
|
||||||
|
|
||||||
|
维基百科上说
|
||||||
|
|
||||||
|
> 在基于类的编程中,工厂方法模式是一种创建型设计模式用来解决创建对象的问题,而不需要指定将要创建对象的确切类。这是通过调用工厂方法创建对象来完成的,而不是通过调用构造器。该工厂方法在接口中指定并由子类实现,或者在基类实现并可以选择由子类重写。
|
||||||
|
|
||||||
|
**程序示例**
|
||||||
|
|
||||||
|
以上面的铁匠为例,首先我们有铁匠的接口和一些它的实现。
|
||||||
|
|
||||||
|
```java
|
||||||
|
public interface Blacksmith {
|
||||||
|
Weapon manufactureWeapon(WeaponType weaponType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ElfBlacksmith implements Blacksmith {
|
||||||
|
public Weapon manufactureWeapon(WeaponType weaponType) {
|
||||||
|
return ELFARSENAL.get(weaponType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class OrcBlacksmith implements Blacksmith {
|
||||||
|
public Weapon manufactureWeapon(WeaponType weaponType) {
|
||||||
|
return ORCARSENAL.get(weaponType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
现在随着客户的到来,会召唤出正确类型的铁匠并制造出要求的武器。
|
||||||
|
|
||||||
|
```java
|
||||||
|
var blacksmith = new ElfBlacksmith();
|
||||||
|
blacksmith.manufactureWeapon(WeaponType.SPEAR);
|
||||||
|
blacksmith.manufactureWeapon(WeaponType.AXE);
|
||||||
|
// Elvish weapons are created
|
||||||
|
```
|
||||||
|
|
||||||
|
## 类图
|
||||||
|

|
||||||
|
|
||||||
|
## 适用性
|
||||||
|
使用工厂方法模式当
|
||||||
|
|
||||||
|
* 一个类无法预料它所要必须创建的对象的类
|
||||||
|
* 一个类想要它的子类来指定它要创建的对象
|
||||||
|
* 类将责任委派给几个帮助子类中的一个,而你想定位了解是具体之中的哪一个
|
||||||
|
|
||||||
|
## Java中的例子
|
||||||
|
|
||||||
|
* [java.util.Calendar](http://docs.oracle.com/javase/8/docs/api/java/util/Calendar.html#getInstance--)
|
||||||
|
* [java.util.ResourceBundle](http://docs.oracle.com/javase/8/docs/api/java/util/ResourceBundle.html#getBundle-java.lang.String-)
|
||||||
|
* [java.text.NumberFormat](http://docs.oracle.com/javase/8/docs/api/java/text/NumberFormat.html#getInstance--)
|
||||||
|
* [java.nio.charset.Charset](http://docs.oracle.com/javase/8/docs/api/java/nio/charset/Charset.html#forName-java.lang.String-)
|
||||||
|
* [java.net.URLStreamHandlerFactory](http://docs.oracle.com/javase/8/docs/api/java/net/URLStreamHandlerFactory.html#createURLStreamHandler-java.lang.String-)
|
||||||
|
* [java.util.EnumSet](https://docs.oracle.com/javase/8/docs/api/java/util/EnumSet.html#of-E-)
|
||||||
|
* [javax.xml.bind.JAXBContext](https://docs.oracle.com/javase/8/docs/api/javax/xml/bind/JAXBContext.html#createMarshaller--)
|
||||||
|
|
||||||
|
## 鸣谢
|
||||||
|
|
||||||
|
* [Design Patterns: Elements of Reusable Object-Oriented Software](https://www.amazon.com/gp/product/0201633612/ref=as_li_tl?ie=UTF8&camp=1789&creative=9325&creativeASIN=0201633612&linkCode=as2&tag=javadesignpat-20&linkId=675d49790ce11db99d90bde47f1aeb59)
|
||||||
|
* [Head First Design Patterns: A Brain-Friendly Guide](https://www.amazon.com/gp/product/0596007124/ref=as_li_tl?ie=UTF8&camp=1789&creative=9325&creativeASIN=0596007124&linkCode=as2&tag=javadesignpat-20&linkId=6b8b6eea86021af6c8e3cd3fc382cb5b)
|
||||||
|
* [Refactoring to Patterns](https://www.amazon.com/gp/product/0321213351/ref=as_li_tl?ie=UTF8&camp=1789&creative=9325&creativeASIN=0321213351&linkCode=as2&tag=javadesignpat-20&linkId=2a76fcb387234bc71b1c61150b3cc3a7)
|