Compare commits

..

8 Commits

Author SHA1 Message Date
e2e470291e Update maven-war-plugin version 2022-01-15 20:16:16 +02:00
64dd23da05 Update maven and maven wrapper to the latest versions 2022-01-15 19:54:48 +02:00
07ee94d671 refactoring: execute around idiom (#1945)
* Refactor execute around the idiom

* fix checkstyle errors

Co-authored-by: Subhrodip Mohanta <hello@subho.xyz>
2022-01-10 17:04:19 +05:30
c5492184b7 enhancement: check spelling and update topic (#1943)
Co-authored-by: Subhrodip Mohanta <hello@subho.xyz>
2022-01-08 18:03:19 +05:30
4f8007d674 enhancement: Refactor and add explanation for value object (#1942)
Co-authored-by: Subhrodip Mohanta <hello@subho.xyz>
2022-01-08 18:01:09 +05:30
2d2dec98e8 enhancement: Add explanation for factory kit (#1941)
Co-authored-by: Subhrodip Mohanta <hello@subho.xyz>
2022-01-08 17:59:30 +05:30
3cc9bc2dea refactoring: unit of work (#1940)
Co-authored-by: Subhrodip Mohanta <hello@subho.xyz>
2022-01-08 17:57:11 +05:30
11f20593b2 Update throttling pattern (#1937)
* Create component.urm.puml

* Create App.java

* Add files via upload

* Add files via upload

* Add files via upload

* Add files via upload

* Create AppTest.java

* Add files via upload

* Update README.md

* Update README.md

* Update pom.xml

* Update App.java

* Update BjornGraphicsComponent.java

* Update BjornInputComponent.java

* Update BjornPhysicsComponent.java

* Update Component.java

* Update App.java

* Delete App.java

* Delete BjornGraphicsComponent.java

* Delete BjornInputComponent.java

* Delete BjornPhysicsComponent.java

* Delete Component.java

* Delete GameObject.java

* Delete GraphicsComponent.java

* Delete InputComponent.java

* Delete PhysicsComponent.java

* Create App.java

* Update App.java

* Update App.java

* Create BjornGraphicsComponent.java

* Create BjornInputComponent.java

* Create BjornPhysicsComponent.java

* Create Component.java

* Create GameObject.java

* Create GraphicsComponent.java

* Create InputComponent.java

* Create PhysicsComponent.java

* Delete AppTest.java

* Delete UpdateTest.java

* Create AppTest.java

* Create UpdateTest.java

* Update throttling pattern example

* delete unwanted files

Co-authored-by: YanchaoMiao <11710204@mail.sustech.edu.cn>
2022-01-07 09:46:59 +02:00
28 changed files with 667 additions and 456 deletions

View File

@ -1,25 +1,18 @@
# # Licensed to the Apache Software Foundation (ASF) under one
# The MIT License # or more contributor license agreements. See the NOTICE file
# Copyright © 2014-2021 Ilkka Seppälä # distributed with this work for additional information
# # regarding copyright ownership. The ASF licenses this file
# Permission is hereby granted, free of charge, to any person obtaining a copy # to you under the Apache License, Version 2.0 (the
# of this software and associated documentation files (the "Software"), to deal # "License"); you may not use this file except in compliance
# in the Software without restriction, including without limitation the rights # with the License. You may obtain a copy of the License at
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #
# copies of the Software, and to permit persons to whom the Software is # http://www.apache.org/licenses/LICENSE-2.0
# furnished to do so, subject to the following conditions: #
# # Unless required by applicable law or agreed to in writing,
# The above copyright notice and this permission notice shall be included in # software distributed under the License is distributed on an
# all copies or substantial portions of the Software. # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# # KIND, either express or implied. See the License for the
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # specific language governing permissions and limitations
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # under the License.
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.4/apache-maven-3.8.4-bin.zip
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar
# 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.
#
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip
wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar

View File

@ -17,10 +17,10 @@ the user to specify only what to do with the resource.
## Explanation ## Explanation
Real world example Real-world example
> We need to provide a class that can be used to write text strings to files. To make it easy for > A class needs to be provided for writing text strings to files. To make it easy for
> the user we let our service class open and close the file automatically, the user only has to > the user, the service class opens and closes the file automatically. The user only has to
> specify what is written into which file. > specify what is written into which file.
In plain words In plain words
@ -35,35 +35,50 @@ In plain words
**Programmatic Example** **Programmatic Example**
Let's introduce our file writer class. `SimpleFileWriter` class implements the Execute Around idiom. It takes `FileWriterAction` as a
constructor argument allowing the user to specify what gets written into the file.
```java ```java
@FunctionalInterface @FunctionalInterface
public interface FileWriterAction { public interface FileWriterAction {
void writeFile(FileWriter writer) throws IOException; void writeFile(FileWriter writer) throws IOException;
} }
@Slf4j
public class SimpleFileWriter { public class SimpleFileWriter {
public SimpleFileWriter(String filename, FileWriterAction action) throws IOException {
public SimpleFileWriter(String filename, FileWriterAction action) throws IOException { LOGGER.info("Opening the file");
try (var writer = new FileWriter(filename)) { try (var writer = new FileWriter(filename)) {
action.writeFile(writer); LOGGER.info("Executing the action");
action.writeFile(writer);
LOGGER.info("Closing the file");
}
} }
}
} }
``` ```
To utilize the file writer the following code is needed. The following code demonstrates how `SimpleFileWriter` is used. `Scanner` is used to print the file
contents after the writing finishes.
```java ```java
FileWriterAction writeHello = writer -> { FileWriterAction writeHello = writer -> {
writer.write("Hello"); writer.write("Gandalf was here");
writer.append(" "); };
writer.append("there!"); new SimpleFileWriter("testfile.txt", writeHello);
};
new SimpleFileWriter("testfile.txt", writeHello); var scanner = new Scanner(new File("testfile.txt"));
while (scanner.hasNextLine()) {
LOGGER.info(scanner.nextLine());
}
```
Here's the console output.
```
21:18:07.185 [main] INFO com.iluwatar.execute.around.SimpleFileWriter - Opening the file
21:18:07.188 [main] INFO com.iluwatar.execute.around.SimpleFileWriter - Executing the action
21:18:07.189 [main] INFO com.iluwatar.execute.around.SimpleFileWriter - Closing the file
21:18:07.199 [main] INFO com.iluwatar.execute.around.App - Gandalf was here
``` ```
## Class diagram ## Class diagram
@ -74,8 +89,7 @@ To utilize the file writer the following code is needed.
Use the Execute Around idiom when Use the Execute Around idiom when
* You use an API that requires methods to be called in pairs such as open/close or * An API requires methods to be called in pairs such as open/close or allocate/deallocate.
allocate/deallocate.
## Credits ## Credits

View File

@ -23,10 +23,14 @@
package com.iluwatar.execute.around; package com.iluwatar.execute.around;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.Scanner;
import lombok.extern.slf4j.Slf4j;
/** /**
* The Execute Around idiom specifies some code to be executed before and after a method. Typically * The Execute Around idiom specifies executable code before and after a method. Typically
* the idiom is used when the API has methods to be executed in pairs, such as resource * the idiom is used when the API has methods to be executed in pairs, such as resource
* allocation/deallocation or lock acquisition/release. * allocation/deallocation or lock acquisition/release.
* *
@ -34,6 +38,7 @@ import java.io.IOException;
* the user. The user specifies only what to do with the file by providing the {@link * the user. The user specifies only what to do with the file by providing the {@link
* FileWriterAction} implementation. * FileWriterAction} implementation.
*/ */
@Slf4j
public class App { public class App {
/** /**
@ -41,11 +46,16 @@ public class App {
*/ */
public static void main(String[] args) throws IOException { public static void main(String[] args) throws IOException {
// create the file writer and execute the custom action
FileWriterAction writeHello = writer -> { FileWriterAction writeHello = writer -> {
writer.write("Hello"); writer.write("Gandalf was here");
writer.append(" ");
writer.append("there!");
}; };
new SimpleFileWriter("testfile.txt", writeHello); new SimpleFileWriter("testfile.txt", writeHello);
// print the file contents
var scanner = new Scanner(new File("testfile.txt"));
while (scanner.hasNextLine()) {
LOGGER.info(scanner.nextLine());
}
} }
} }

View File

@ -26,18 +26,24 @@ package com.iluwatar.execute.around;
import java.io.FileWriter; import java.io.FileWriter;
import java.io.IOException; import java.io.IOException;
import lombok.extern.slf4j.Slf4j;
/** /**
* SimpleFileWriter handles opening and closing file for the user. The user only has to specify what * SimpleFileWriter handles opening and closing file for the user. The user only has to specify what
* to do with the file resource through {@link FileWriterAction} parameter. * to do with the file resource through {@link FileWriterAction} parameter.
*/ */
@Slf4j
public class SimpleFileWriter { public class SimpleFileWriter {
/** /**
* Constructor. * Constructor.
*/ */
public SimpleFileWriter(String filename, FileWriterAction action) throws IOException { public SimpleFileWriter(String filename, FileWriterAction action) throws IOException {
LOGGER.info("Opening the file");
try (var writer = new FileWriter(filename)) { try (var writer = new FileWriter(filename)) {
LOGGER.info("Executing the action");
action.writeFile(writer); action.writeFile(writer);
LOGGER.info("Closing the file");
} }
} }
} }

View File

@ -10,19 +10,115 @@ tags:
--- ---
## Intent ## Intent
Define a factory of immutable content with separated builder and factory interfaces. Define a factory of immutable content with separated builder and factory interfaces.
## Explanation
Real-world example
> Imagine a magical weapon factory that can create any type of weapon wished for. When the factory
> is unboxed, the master recites the weapon types needed to prepare it. After that, any of those
> weapon types can be summoned in an instant.
In plain words
> Factory kit is a configurable object builder.
**Programmatic Example**
Let's first define the simple `Weapon` hierarchy.
```java
public interface Weapon {
}
public enum WeaponType {
SWORD,
AXE,
BOW,
SPEAR
}
public class Sword implements Weapon {
@Override
public String toString() {
return "Sword";
}
}
// Axe, Bow, and Spear are defined similarly
```
Next, we define a functional interface that allows adding a builder with a name to the factory.
```java
public interface Builder {
void add(WeaponType name, Supplier<Weapon> supplier);
}
```
The meat of the example is the `WeaponFactory` interface that effectively implements the factory
kit pattern. The method `#factory` is used to configure the factory with the classes it needs to
be able to construct. The method `#create` is then used to create object instances.
```java
public interface WeaponFactory {
static WeaponFactory factory(Consumer<Builder> consumer) {
var map = new HashMap<WeaponType, Supplier<Weapon>>();
consumer.accept(map::put);
return name -> map.get(name).get();
}
Weapon create(WeaponType name);
}
```
Now, we can show how `WeaponFactory` can be used.
```java
var factory = WeaponFactory.factory(builder -> {
builder.add(WeaponType.SWORD, Sword::new);
builder.add(WeaponType.AXE, Axe::new);
builder.add(WeaponType.SPEAR, Spear::new);
builder.add(WeaponType.BOW, Bow::new);
});
var list = new ArrayList<Weapon>();
list.add(factory.create(WeaponType.AXE));
list.add(factory.create(WeaponType.SPEAR));
list.add(factory.create(WeaponType.SWORD));
list.add(factory.create(WeaponType.BOW));
list.stream().forEach(weapon -> LOGGER.info("{}", weapon.toString()));
```
Here is the console output when the example is run.
```
21:15:49.709 [main] INFO com.iluwatar.factorykit.App - Axe
21:15:49.713 [main] INFO com.iluwatar.factorykit.App - Spear
21:15:49.713 [main] INFO com.iluwatar.factorykit.App - Sword
21:15:49.713 [main] INFO com.iluwatar.factorykit.App - Bow
```
## Class diagram ## Class diagram
![alt text](./etc/factory-kit.png "Factory Kit") ![alt text](./etc/factory-kit.png "Factory Kit")
## Applicability ## Applicability
Use the Factory Kit pattern when Use the Factory Kit pattern when
* a class can't anticipate the class of objects it must create * The factory class can't anticipate the types of objects it must create
* you just want a new instance of a custom builder instead of the global one * A new instance of a custom builder is needed instead of a global one
* you explicitly want to define types of objects, that factory can build * The types of objects that the factory can build need to be defined outside the class
* you want a separated builder and creator interface * The builder and creator interfaces need to be separated
## Related patterns
* [Builder](https://java-design-patterns.com/patterns/builder/)
* [Factory](https://java-design-patterns.com/patterns/factory/)
## Credits ## Credits
* [Design Pattern Reloaded by Remi Forax: ](https://www.youtube.com/watch?v=-k2X7guaArU) * [Design Pattern Reloaded by Remi Forax](https://www.youtube.com/watch?v=-k2X7guaArU)

View File

@ -23,14 +23,16 @@
package com.iluwatar.factorykit; package com.iluwatar.factorykit;
import java.util.ArrayList;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
/** /**
* Factory-kit is a creational pattern which defines a factory of immutable content with separated * Factory kit is a creational pattern that defines a factory of immutable content with separated
* builder and factory interfaces to deal with the problem of creating one of the objects specified * builder and factory interfaces to deal with the problem of creating one of the objects specified
* directly in the factory-kit instance. * directly in the factory kit instance.
* *
* <p>In the given example {@link WeaponFactory} represents the factory-kit, that contains four * <p>In the given example {@link WeaponFactory} represents the factory kit, that contains four
* {@link Builder}s for creating new objects of the classes implementing {@link Weapon} interface. * {@link Builder}s for creating new objects of the classes implementing {@link Weapon} interface.
* *
* <p>Each of them can be called with {@link WeaponFactory#create(WeaponType)} method, with * <p>Each of them can be called with {@link WeaponFactory#create(WeaponType)} method, with
@ -52,7 +54,11 @@ public class App {
builder.add(WeaponType.SPEAR, Spear::new); builder.add(WeaponType.SPEAR, Spear::new);
builder.add(WeaponType.BOW, Bow::new); builder.add(WeaponType.BOW, Bow::new);
}); });
var axe = factory.create(WeaponType.AXE); var list = new ArrayList<Weapon>();
LOGGER.info(axe.toString()); list.add(factory.create(WeaponType.AXE));
list.add(factory.create(WeaponType.SPEAR));
list.add(factory.create(WeaponType.SWORD));
list.add(factory.create(WeaponType.BOW));
list.stream().forEach(weapon -> LOGGER.info("{}", weapon.toString()));
} }
} }

View File

@ -41,7 +41,7 @@
<zk.version>9.0.0</zk.version> <zk.version>9.0.0</zk.version>
<guava.version>19.0</guava.version> <guava.version>19.0</guava.version>
<jetty-maven-plugin.version>9.4.28.v20200408</jetty-maven-plugin.version> <jetty-maven-plugin.version>9.4.28.v20200408</jetty-maven-plugin.version>
<maven-war-plugin.version>2.1.1</maven-war-plugin.version> <maven-war-plugin.version>3.3.2</maven-war-plugin.version>
<maven-assembly-plugin.version>2.2</maven-assembly-plugin.version> <maven-assembly-plugin.version>2.2</maven-assembly-plugin.version>
<maven.build.timestamp.format>yyyy-MM-dd</maven.build.timestamp.format> <maven.build.timestamp.format>yyyy-MM-dd</maven.build.timestamp.format>
<packname>-${project.version}-FL-${maven.build.timestamp}</packname> <packname>-${project.version}-FL-${maven.build.timestamp}</packname>

18
mvnw vendored
View File

@ -36,6 +36,10 @@
if [ -z "$MAVEN_SKIP_RC" ] ; then if [ -z "$MAVEN_SKIP_RC" ] ; then
if [ -f /usr/local/etc/mavenrc ] ; then
. /usr/local/etc/mavenrc
fi
if [ -f /etc/mavenrc ] ; then if [ -f /etc/mavenrc ] ; then
. /etc/mavenrc . /etc/mavenrc
fi fi
@ -145,7 +149,7 @@ if [ -z "$JAVACMD" ] ; then
JAVACMD="$JAVA_HOME/bin/java" JAVACMD="$JAVA_HOME/bin/java"
fi fi
else else
JAVACMD="`which java`" JAVACMD="`\\unset -f command; \\command -v java`"
fi fi
fi fi
@ -212,9 +216,9 @@ else
echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
fi fi
if [ -n "$MVNW_REPOURL" ]; then if [ -n "$MVNW_REPOURL" ]; then
jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
else else
jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
fi fi
while IFS="=" read key value; do while IFS="=" read key value; do
case "$key" in (wrapperUrl) jarUrl="$value"; break ;; case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
@ -233,9 +237,9 @@ else
echo "Found wget ... using wget" echo "Found wget ... using wget"
fi fi
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
wget "$jarUrl" -O "$wrapperJarPath" wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
else else
wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
fi fi
elif command -v curl > /dev/null; then elif command -v curl > /dev/null; then
if [ "$MVNW_VERBOSE" = true ]; then if [ "$MVNW_VERBOSE" = true ]; then
@ -305,6 +309,8 @@ WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
exec "$JAVACMD" \ exec "$JAVACMD" \
$MAVEN_OPTS \ $MAVEN_OPTS \
$MAVEN_DEBUG_OPTS \
-classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
"-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ "-Dmaven.home=${M2_HOME}" \
"-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"

66
mvnw.cmd vendored
View File

@ -1,25 +1,21 @@
@REM ----------------------------------------------------------------------------
@REM Licensed to the Apache Software Foundation (ASF) under one
@REM or more contributor license agreements. See the NOTICE file
@REM distributed with this work for additional information
@REM regarding copyright ownership. The ASF licenses this file
@REM to you under the Apache License, Version 2.0 (the
@REM "License"); you may not use this file except in compliance
@REM with the License. You may obtain a copy of the License at
@REM @REM
@REM The MIT License @REM http://www.apache.org/licenses/LICENSE-2.0
@REM Copyright © 2014-2021 Ilkka Seppälä
@REM
@REM Permission is hereby granted, free of charge, to any person obtaining a copy
@REM of this software and associated documentation files (the "Software"), to deal
@REM in the Software without restriction, including without limitation the rights
@REM to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
@REM copies of the Software, and to permit persons to whom the Software is
@REM furnished to do so, subject to the following conditions:
@REM
@REM The above copyright notice and this permission notice shall be included in
@REM all copies or substantial portions of the Software.
@REM
@REM THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
@REM IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
@REM FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
@REM AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
@REM LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
@REM OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
@REM THE SOFTWARE.
@REM @REM
@REM Unless required by applicable law or agreed to in writing,
@REM software distributed under the License is distributed on an
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@REM KIND, either express or implied. See the License for the
@REM specific language governing permissions and limitations
@REM under the License.
@REM ----------------------------------------------------------------------------
@REM ---------------------------------------------------------------------------- @REM ----------------------------------------------------------------------------
@REM Maven Start Up Batch script @REM Maven Start Up Batch script
@ -50,8 +46,8 @@ if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
@REM Execute a user defined script before this one @REM Execute a user defined script before this one
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
@REM check for pre script, once with legacy .bat ending and once with .cmd ending @REM check for pre script, once with legacy .bat ending and once with .cmd ending
if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
:skipRcPre :skipRcPre
@setlocal @setlocal
@ -124,9 +120,9 @@ SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
) )
@ -138,7 +134,7 @@ if exist %WRAPPER_JAR% (
) )
) else ( ) else (
if not "%MVNW_REPOURL%" == "" ( if not "%MVNW_REPOURL%" == "" (
SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
) )
if "%MVNW_VERBOSE%" == "true" ( if "%MVNW_VERBOSE%" == "true" (
echo Couldn't find %WRAPPER_JAR%, downloading it ... echo Couldn't find %WRAPPER_JAR%, downloading it ...
@ -162,7 +158,13 @@ if exist %WRAPPER_JAR% (
@REM work with both Windows and non-Windows executions. @REM work with both Windows and non-Windows executions.
set MAVEN_CMD_LINE_ARGS=%* set MAVEN_CMD_LINE_ARGS=%*
%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* %MAVEN_JAVA_EXE% ^
%JVM_CONFIG_MAVEN_PROPS% ^
%MAVEN_OPTS% ^
%MAVEN_DEBUG_OPTS% ^
-classpath %WRAPPER_JAR% ^
"-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
%WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
if ERRORLEVEL 1 goto error if ERRORLEVEL 1 goto error
goto end goto end
@ -172,15 +174,15 @@ set ERROR_CODE=1
:end :end
@endlocal & set ERROR_CODE=%ERROR_CODE% @endlocal & set ERROR_CODE=%ERROR_CODE%
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
@REM check for post script, once with legacy .bat ending and once with .cmd ending @REM check for post script, once with legacy .bat ending and once with .cmd ending
if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
:skipRcPost :skipRcPost
@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
if "%MAVEN_BATCH_PAUSE%" == "on" pause if "%MAVEN_BATCH_PAUSE%"=="on" pause
if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
exit /B %ERROR_CODE% cmd /C exit /B %ERROR_CODE%

View File

@ -150,7 +150,7 @@
</plugin> </plugin>
<plugin> <plugin>
<artifactId>maven-war-plugin</artifactId> <artifactId>maven-war-plugin</artifactId>
<version>2.4</version> <version>3.3.2</version>
</plugin> </plugin>
<plugin> <plugin>
<groupId>org.mortbay.jetty</groupId> <groupId>org.mortbay.jetty</groupId>

View File

@ -18,11 +18,11 @@ threads and eliminating the latency of creating new threads.
## Explanation ## Explanation
Real world example Real-world example
> We have a large number of relatively short tasks at hand. We need to peel huge amounts of potatoes > We have a large number of relatively short tasks at hand. We need to peel huge amounts of potatoes
> and serve mighty amount of coffee cups. Creating a new thread for each task would be a waste so we > and serve a mighty amount of coffee cups. Creating a new thread for each task would be a waste so
> establish a thread pool. > we establish a thread pool.
In plain words In plain words
@ -99,7 +99,7 @@ public class PotatoPeelingTask extends Task {
} }
``` ```
Next we present a runnable `Worker` class that the thread pool will utilize to handle all the potato Next, we present a runnable `Worker` class that the thread pool will utilize to handle all the potato
peeling and coffee making. peeling and coffee making.
```java ```java

View File

@ -16,10 +16,12 @@ Ensure that a given client is not able to access service resources more than the
## Explanation ## Explanation
Real world example Real-world example
> A large multinational corporation offers API to its customers. The API is rate-limited and each > A young human and an old dwarf walk into a bar. They start ordering beers from the bartender.
> customer can only make certain amount of calls per second. > The bartender immediately sees that the young human shouldn't consume too many drinks too fast
> and refuses to serve if enough time has not passed. For the old dwarf, the serving rate can
> be higher.
In plain words In plain words
@ -33,30 +35,25 @@ In plain words
**Programmatic Example** **Programmatic Example**
Tenant class presents the clients of the API. CallsCount tracks the number of API calls per tenant. `BarCustomer` class presents the clients of the `Bartender` API. `CallsCount` tracks the number of
calls per `BarCustomer`.
```java ```java
public class Tenant { public class BarCustomer {
private final String name; @Getter
private final int allowedCallsPerSecond; private final String name;
@Getter
private final int allowedCallsPerSecond;
public Tenant(String name, int allowedCallsPerSecond, CallsCount callsCount) { public BarCustomer(String name, int allowedCallsPerSecond, CallsCount callsCount) {
if (allowedCallsPerSecond < 0) { if (allowedCallsPerSecond < 0) {
throw new InvalidParameterException("Number of calls less than 0 not allowed"); throw new InvalidParameterException("Number of calls less than 0 not allowed");
}
this.name = name;
this.allowedCallsPerSecond = allowedCallsPerSecond;
callsCount.addTenant(name);
} }
this.name = name;
this.allowedCallsPerSecond = allowedCallsPerSecond;
callsCount.addTenant(name);
}
public String getName() {
return name;
}
public int getAllowedCallsPerSecond() {
return allowedCallsPerSecond;
}
} }
@Slf4j @Slf4j
@ -76,14 +73,14 @@ public final class CallsCount {
} }
public void reset() { public void reset() {
LOGGER.debug("Resetting the map.");
tenantCallsCount.replaceAll((k, v) -> new AtomicLong(0)); tenantCallsCount.replaceAll((k, v) -> new AtomicLong(0));
LOGGER.info("reset counters");
} }
} }
``` ```
Next we introduce the service that the tenants are calling. To track the call count we use the Next, the service that the tenants are calling is introduced. To track the call count, a throttler
throttler timer. timer is used.
```java ```java
public interface Throttler { public interface Throttler {
@ -111,71 +108,103 @@ public class ThrottleTimerImpl implements Throttler {
}, 0, throttlePeriod); }, 0, throttlePeriod);
} }
} }
```
class B2BService { `Bartender` offers the `orderDrink` service to the `BarCustomer`s. The customers probably don't
know that the beer serving rate is limited by their appearances.
private static final Logger LOGGER = LoggerFactory.getLogger(B2BService.class); ```java
private final CallsCount callsCount; class Bartender {
public B2BService(Throttler timer, CallsCount callsCount) { private static final Logger LOGGER = LoggerFactory.getLogger(Bartender.class);
this.callsCount = callsCount; private final CallsCount callsCount;
timer.start();
}
public int dummyCustomerApi(Tenant tenant) { public Bartender(Throttler timer, CallsCount callsCount) {
var tenantName = tenant.getName(); this.callsCount = callsCount;
var count = callsCount.getCount(tenantName); timer.start();
LOGGER.debug("Counter for {} : {} ", tenant.getName(), count);
if (count >= tenant.getAllowedCallsPerSecond()) {
LOGGER.error("API access per second limit reached for: {}", tenantName);
return -1;
} }
callsCount.incrementCount(tenantName);
return getRandomCustomerId();
}
private int getRandomCustomerId() { public int orderDrink(BarCustomer barCustomer) {
return ThreadLocalRandom.current().nextInt(1, 10000); var tenantName = barCustomer.getName();
} var count = callsCount.getCount(tenantName);
if (count >= barCustomer.getAllowedCallsPerSecond()) {
LOGGER.error("I'm sorry {}, you've had enough for today!", tenantName);
return -1;
}
callsCount.incrementCount(tenantName);
LOGGER.debug("Serving beer to {} : [{} consumed] ", barCustomer.getName(), count+1);
return getRandomCustomerId();
}
private int getRandomCustomerId() {
return ThreadLocalRandom.current().nextInt(1, 10000);
}
} }
``` ```
Now we are ready to see the full example in action. Tenant Adidas is rate-limited to 5 calls per Now it is possible to see the full example in action. `BarCustomer` young human is rate-limited to 2
second and Nike to 6. calls per second and the old dwarf to 4.
```java ```java
public static void main(String[] args) { public static void main(String[] args) {
var callsCount = new CallsCount(); var callsCount = new CallsCount();
var adidas = new Tenant("Adidas", 5, callsCount); var human = new BarCustomer("young human", 2, callsCount);
var nike = new Tenant("Nike", 6, callsCount); var dwarf = new BarCustomer("dwarf soldier", 4, callsCount);
var executorService = Executors.newFixedThreadPool(2); var executorService = Executors.newFixedThreadPool(2);
executorService.execute(() -> makeServiceCalls(adidas, callsCount));
executorService.execute(() -> makeServiceCalls(nike, callsCount));
executorService.shutdown();
try {
executorService.awaitTermination(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
LOGGER.error("Executor Service terminated: {}", e.getMessage());
}
}
private static void makeServiceCalls(Tenant tenant, CallsCount callsCount) { executorService.execute(() -> makeServiceCalls(human, callsCount));
var timer = new ThrottleTimerImpl(10, callsCount); executorService.execute(() -> makeServiceCalls(dwarf, callsCount));
var service = new B2BService(timer, callsCount);
executorService.shutdown();
try {
executorService.awaitTermination(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
LOGGER.error("Executor service terminated: {}", e.getMessage());
}
}
private static void makeServiceCalls(BarCustomer barCustomer, CallsCount callsCount) {
var timer = new ThrottleTimerImpl(1000, callsCount);
var service = new Bartender(timer, callsCount);
// Sleep is introduced to keep the output in check and easy to view and analyze the results. // Sleep is introduced to keep the output in check and easy to view and analyze the results.
IntStream.range(0, 20).forEach(i -> { IntStream.range(0, 50).forEach(i -> {
service.dummyCustomerApi(tenant); service.orderDrink(barCustomer);
try { try {
Thread.sleep(1); Thread.sleep(100);
} catch (InterruptedException e) { } catch (InterruptedException e) {
LOGGER.error("Thread interrupted: {}", e.getMessage()); LOGGER.error("Thread interrupted: {}", e.getMessage());
} }
}); });
} }
``` ```
An excerpt from the example's console output:
```
18:46:36.218 [Timer-0] INFO com.iluwatar.throttling.CallsCount - reset counters
18:46:36.218 [Timer-1] INFO com.iluwatar.throttling.CallsCount - reset counters
18:46:36.242 [pool-1-thread-2] DEBUG com.iluwatar.throttling.Bartender - Serving beer to dwarf soldier : [1 consumed]
18:46:36.242 [pool-1-thread-1] DEBUG com.iluwatar.throttling.Bartender - Serving beer to young human : [1 consumed]
18:46:36.342 [pool-1-thread-2] DEBUG com.iluwatar.throttling.Bartender - Serving beer to dwarf soldier : [2 consumed]
18:46:36.342 [pool-1-thread-1] DEBUG com.iluwatar.throttling.Bartender - Serving beer to young human : [2 consumed]
18:46:36.443 [pool-1-thread-1] ERROR com.iluwatar.throttling.Bartender - I'm sorry young human, you've had enough for today!
18:46:36.443 [pool-1-thread-2] DEBUG com.iluwatar.throttling.Bartender - Serving beer to dwarf soldier : [3 consumed]
18:46:36.544 [pool-1-thread-1] ERROR com.iluwatar.throttling.Bartender - I'm sorry young human, you've had enough for today!
18:46:36.544 [pool-1-thread-2] DEBUG com.iluwatar.throttling.Bartender - Serving beer to dwarf soldier : [4 consumed]
18:46:36.645 [pool-1-thread-2] ERROR com.iluwatar.throttling.Bartender - I'm sorry dwarf soldier, you've had enough for today!
18:46:36.645 [pool-1-thread-1] ERROR com.iluwatar.throttling.Bartender - I'm sorry young human, you've had enough for today!
18:46:36.745 [pool-1-thread-1] ERROR com.iluwatar.throttling.Bartender - I'm sorry young human, you've had enough for today!
18:46:36.745 [pool-1-thread-2] ERROR com.iluwatar.throttling.Bartender - I'm sorry dwarf soldier, you've had enough for today!
18:46:36.846 [pool-1-thread-1] ERROR com.iluwatar.throttling.Bartender - I'm sorry young human, you've had enough for today!
18:46:36.846 [pool-1-thread-2] ERROR com.iluwatar.throttling.Bartender - I'm sorry dwarf soldier, you've had enough for today!
18:46:36.947 [pool-1-thread-2] ERROR com.iluwatar.throttling.Bartender - I'm sorry dwarf soldier, you've had enough for today!
18:46:36.947 [pool-1-thread-1] ERROR com.iluwatar.throttling.Bartender - I'm sorry young human, you've had enough for today!
18:46:37.048 [pool-1-thread-2] ERROR com.iluwatar.throttling.Bartender - I'm sorry dwarf soldier, you've had enough for today!
18:46:37.048 [pool-1-thread-1] ERROR com.iluwatar.throttling.Bartender - I'm sorry young human, you've had enough for today!
18:46:37.148 [pool-1-thread-1] ERROR com.iluwatar.throttling.Bartender - I'm sorry young human, you've had enough for today!
18:46:37.148 [pool-1-thread-2] ERROR com.iluwatar.throttling.Bartender - I'm sorry dwarf soldier, you've had enough for today!
```
## Class diagram ## Class diagram
@ -185,7 +214,7 @@ second and Nike to 6.
The Throttling pattern should be used: The Throttling pattern should be used:
* When a service access needs to be restricted to not have high impacts on the performance of the service. * When service access needs to be restricted not to have high impact on the performance of the service.
* When multiple clients are consuming the same service resources and restriction has to be made according to the usage per client. * When multiple clients are consuming the same service resources and restriction has to be made according to the usage per client.
## Credits ## Credits

View File

@ -34,11 +34,11 @@ import lombok.extern.slf4j.Slf4j;
* complete service by users or a particular tenant. This can allow systems to continue to function * complete service by users or a particular tenant. This can allow systems to continue to function
* and meet service level agreements, even when an increase in demand places load on resources. * and meet service level agreements, even when an increase in demand places load on resources.
* <p> * <p>
* In this example we have ({@link App}) as the initiating point of the service. This is a time * In this example there is a {@link Bartender} serving beer to {@link BarCustomer}s. This is a time
* based throttling, i.e. only a certain number of calls are allowed per second. * based throttling, i.e. only a certain number of calls are allowed per second.
* </p> * </p>
* ({@link Tenant}) is the Tenant POJO class with which many tenants can be created ({@link * ({@link BarCustomer}) is the service tenant class having a name and the number of calls allowed.
* B2BService}) is the service which is consumed by the tenants and is throttled. * ({@link Bartender}) is the service which is consumed by the tenants and is throttled.
*/ */
@Slf4j @Slf4j
public class App { public class App {
@ -50,33 +50,35 @@ public class App {
*/ */
public static void main(String[] args) { public static void main(String[] args) {
var callsCount = new CallsCount(); var callsCount = new CallsCount();
var adidas = new Tenant("Adidas", 5, callsCount); var human = new BarCustomer("young human", 2, callsCount);
var nike = new Tenant("Nike", 6, callsCount); var dwarf = new BarCustomer("dwarf soldier", 4, callsCount);
var executorService = Executors.newFixedThreadPool(2); var executorService = Executors.newFixedThreadPool(2);
executorService.execute(() -> makeServiceCalls(adidas, callsCount)); executorService.execute(() -> makeServiceCalls(human, callsCount));
executorService.execute(() -> makeServiceCalls(nike, callsCount)); executorService.execute(() -> makeServiceCalls(dwarf, callsCount));
executorService.shutdown(); executorService.shutdown();
try { try {
executorService.awaitTermination(10, TimeUnit.SECONDS); if (!executorService.awaitTermination(10, TimeUnit.SECONDS)) {
executorService.shutdownNow();
}
} catch (InterruptedException e) { } catch (InterruptedException e) {
LOGGER.error("Executor Service terminated: {}", e.getMessage()); executorService.shutdownNow();
} }
} }
/** /**
* Make calls to the B2BService dummy API. * Make calls to the bartender.
*/ */
private static void makeServiceCalls(Tenant tenant, CallsCount callsCount) { private static void makeServiceCalls(BarCustomer barCustomer, CallsCount callsCount) {
var timer = new ThrottleTimerImpl(10, callsCount); var timer = new ThrottleTimerImpl(1000, callsCount);
var service = new B2BService(timer, callsCount); var service = new Bartender(timer, callsCount);
// Sleep is introduced to keep the output in check and easy to view and analyze the results. // Sleep is introduced to keep the output in check and easy to view and analyze the results.
IntStream.range(0, 20).forEach(i -> { IntStream.range(0, 50).forEach(i -> {
service.dummyCustomerApi(tenant); service.orderDrink(barCustomer);
try { try {
Thread.sleep(1); Thread.sleep(100);
} catch (InterruptedException e) { } catch (InterruptedException e) {
LOGGER.error("Thread interrupted: {}", e.getMessage()); LOGGER.error("Thread interrupted: {}", e.getMessage());
} }

View File

@ -25,22 +25,26 @@ package com.iluwatar.throttling;
import java.security.InvalidParameterException; import java.security.InvalidParameterException;
/** import lombok.Getter;
* A Pojo class to create a basic Tenant with the allowed calls per second.
*/
public class Tenant {
/**
* BarCustomer is a tenant with a name and a number of allowed calls per second.
*/
public class BarCustomer {
@Getter
private final String name; private final String name;
@Getter
private final int allowedCallsPerSecond; private final int allowedCallsPerSecond;
/** /**
* Constructor. * Constructor.
* *
* @param name Name of the tenant * @param name Name of the BarCustomer
* @param allowedCallsPerSecond The number of calls allowed for a particular tenant. * @param allowedCallsPerSecond The number of calls allowed for this particular tenant.
* @throws InvalidParameterException If number of calls is less than 0, throws exception. * @throws InvalidParameterException If number of calls is less than 0, throws exception.
*/ */
public Tenant(String name, int allowedCallsPerSecond, CallsCount callsCount) { public BarCustomer(String name, int allowedCallsPerSecond, CallsCount callsCount) {
if (allowedCallsPerSecond < 0) { if (allowedCallsPerSecond < 0) {
throw new InvalidParameterException("Number of calls less than 0 not allowed"); throw new InvalidParameterException("Number of calls less than 0 not allowed");
} }
@ -48,12 +52,4 @@ public class Tenant {
this.allowedCallsPerSecond = allowedCallsPerSecond; this.allowedCallsPerSecond = allowedCallsPerSecond;
callsCount.addTenant(name); callsCount.addTenant(name);
} }
public String getName() {
return name;
}
public int getAllowedCallsPerSecond() {
return allowedCallsPerSecond;
}
} }

View File

@ -29,33 +29,32 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
/** /**
* A service which accepts a tenant and throttles the resource based on the time given to the * Bartender is a service which accepts a BarCustomer (tenant) and throttles
* tenant. * the resource based on the time given to the tenant.
*/ */
class B2BService { class Bartender {
private static final Logger LOGGER = LoggerFactory.getLogger(B2BService.class); private static final Logger LOGGER = LoggerFactory.getLogger(Bartender.class);
private final CallsCount callsCount; private final CallsCount callsCount;
public B2BService(Throttler timer, CallsCount callsCount) { public Bartender(Throttler timer, CallsCount callsCount) {
this.callsCount = callsCount; this.callsCount = callsCount;
timer.start(); timer.start();
} }
/** /**
* Calls dummy customer api. * Orders a drink from the bartender.
*
* @return customer id which is randomly generated * @return customer id which is randomly generated
*/ */
public int dummyCustomerApi(Tenant tenant) { public int orderDrink(BarCustomer barCustomer) {
var tenantName = tenant.getName(); var tenantName = barCustomer.getName();
var count = callsCount.getCount(tenantName); var count = callsCount.getCount(tenantName);
LOGGER.debug("Counter for {} : {} ", tenant.getName(), count); if (count >= barCustomer.getAllowedCallsPerSecond()) {
if (count >= tenant.getAllowedCallsPerSecond()) { LOGGER.error("I'm sorry {}, you've had enough for today!", tenantName);
LOGGER.error("API access per second limit reached for: {}", tenantName);
return -1; return -1;
} }
callsCount.incrementCount(tenantName); callsCount.incrementCount(tenantName);
LOGGER.debug("Serving beer to {} : [{} consumed] ", barCustomer.getName(), count + 1);
return getRandomCustomerId(); return getRandomCustomerId();
} }

View File

@ -69,7 +69,7 @@ public final class CallsCount {
* Resets the count of all the tenants in the map. * Resets the count of all the tenants in the map.
*/ */
public void reset() { public void reset() {
LOGGER.debug("Resetting the map.");
tenantCallsCount.replaceAll((k, v) -> new AtomicLong(0)); tenantCallsCount.replaceAll((k, v) -> new AtomicLong(0));
LOGGER.info("reset counters");
} }
} }

View File

@ -23,20 +23,21 @@
package com.iluwatar.throttling; package com.iluwatar.throttling;
import static org.junit.jupiter.api.Assertions.assertThrows; import org.junit.jupiter.api.Test;
import java.security.InvalidParameterException; import java.security.InvalidParameterException;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertThrows;
/** /**
* TenantTest to test the creation of Tenant with valid parameters. * TenantTest to test the creation of Tenant with valid parameters.
*/ */
public class TenantTest { public class BarCustomerTest {
@Test @Test
void constructorTest() { void constructorTest() {
assertThrows(InvalidParameterException.class, () -> { assertThrows(InvalidParameterException.class, () -> {
new Tenant("FailTenant", -1, new CallsCount()); new BarCustomer("sirBrave", -1, new CallsCount());
}); });
} }
} }

View File

@ -32,19 +32,18 @@ import org.junit.jupiter.api.Test;
/** /**
* B2BServiceTest class to test the B2BService * B2BServiceTest class to test the B2BService
*/ */
public class B2BServiceTest { public class BartenderTest {
private final CallsCount callsCount = new CallsCount(); private final CallsCount callsCount = new CallsCount();
@Test @Test
void dummyCustomerApiTest() { void dummyCustomerApiTest() {
var tenant = new Tenant("testTenant", 2, callsCount); var tenant = new BarCustomer("pirate", 2, callsCount);
// In order to assure that throttling limits will not be reset, we use an empty throttling implementation // In order to assure that throttling limits will not be reset, we use an empty throttling implementation
var timer = (Throttler) () -> { var timer = (Throttler) () -> {};
}; var service = new Bartender(timer, callsCount);
var service = new B2BService(timer, callsCount);
IntStream.range(0, 5).mapToObj(i -> tenant).forEach(service::dummyCustomerApi); IntStream.range(0, 5).mapToObj(i -> tenant).forEach(service::orderDrink);
var counter = callsCount.getCount(tenant.getName()); var counter = callsCount.getCount(tenant.getName());
assertEquals(2, counter, "Counter limit must be reached"); assertEquals(2, counter, "Counter limit must be reached");
} }

View File

@ -12,20 +12,20 @@ tags:
## Intent ## Intent
When a business transaction is completed, all the the updates are sent as one big unit of work to be When a business transaction is completed, all the updates are sent as one big unit of work to be
persisted in one go to minimize database round-trips. persisted in one go to minimize database round-trips.
## Explanation ## Explanation
Real world example Real-world example
> We have a database containing student information. Administrators all over the country are > Arms dealer has a database containing weapon information. Merchants all over the town are
> constantly updating this information and it causes high load on the database server. To make the > constantly updating this information and it causes a high load on the database server. To make the
> load more manageable we apply to Unit of Work pattern to send many small updates in batches. > load more manageable we apply to Unit of Work pattern to send many small updates in batches.
In plain words In plain words
> Unit of Work merges many small database updates in single batch to optimize the number of > Unit of Work merges many small database updates in a single batch to optimize the number of
> round-trips. > round-trips.
[MartinFowler.com](https://martinfowler.com/eaaCatalog/unitOfWork.html) says [MartinFowler.com](https://martinfowler.com/eaaCatalog/unitOfWork.html) says
@ -35,37 +35,20 @@ In plain words
**Programmatic Example** **Programmatic Example**
Here's the `Student` entity that is being persisted to the database. Here's the `Weapon` entity that is being persisted in the database.
```java ```java
public class Student { @Getter
private final Integer id; @RequiredArgsConstructor
private final String name; public class Weapon {
private final String address; private final Integer id;
private final String name;
public Student(Integer id, String name, String address) {
this.id = id;
this.name = name;
this.address = address;
}
public String getName() {
return name;
}
public Integer getId() {
return id;
}
public String getAddress() {
return address;
}
} }
``` ```
The essence of the implementation is the `StudentRepository` implementing the Unit of Work pattern. The essence of the implementation is the `ArmsDealer` implementing the Unit of Work pattern.
It maintains a map of database operations (`context`) that need to be done and when `commit` is It maintains a map of database operations (`context`) that need to be done and when `commit` is
called it applies them in single batch. called it applies them in a single batch.
```java ```java
public interface IUnitOfWork<T> { public interface IUnitOfWork<T> {
@ -84,96 +67,117 @@ public interface IUnitOfWork<T> {
} }
@Slf4j @Slf4j
public class StudentRepository implements IUnitOfWork<Student> { @RequiredArgsConstructor
public class ArmsDealer implements IUnitOfWork<Weapon> {
private final Map<String, List<Student>> context; private final Map<String, List<Weapon>> context;
private final StudentDatabase studentDatabase; private final WeaponDatabase weaponDatabase;
public StudentRepository(Map<String, List<Student>> context, StudentDatabase studentDatabase) { @Override
this.context = context; public void registerNew(Weapon weapon) {
this.studentDatabase = studentDatabase; LOGGER.info("Registering {} for insert in context.", weapon.getName());
} register(weapon, UnitActions.INSERT.getActionValue());
@Override
public void registerNew(Student student) {
LOGGER.info("Registering {} for insert in context.", student.getName());
register(student, IUnitOfWork.INSERT);
}
@Override
public void registerModified(Student student) {
LOGGER.info("Registering {} for modify in context.", student.getName());
register(student, IUnitOfWork.MODIFY);
}
@Override
public void registerDeleted(Student student) {
LOGGER.info("Registering {} for delete in context.", student.getName());
register(student, IUnitOfWork.DELETE);
}
private void register(Student student, String operation) {
var studentsToOperate = context.get(operation);
if (studentsToOperate == null) {
studentsToOperate = new ArrayList<>();
}
studentsToOperate.add(student);
context.put(operation, studentsToOperate);
}
@Override
public void commit() {
if (context == null || context.size() == 0) {
return;
}
LOGGER.info("Commit started");
if (context.containsKey(IUnitOfWork.INSERT)) {
commitInsert();
} }
if (context.containsKey(IUnitOfWork.MODIFY)) { @Override
commitModify(); public void registerModified(Weapon weapon) {
} LOGGER.info("Registering {} for modify in context.", weapon.getName());
if (context.containsKey(IUnitOfWork.DELETE)) { register(weapon, UnitActions.MODIFY.getActionValue());
commitDelete();
}
LOGGER.info("Commit finished.");
}
private void commitInsert() {
var studentsToBeInserted = context.get(IUnitOfWork.INSERT);
for (var student : studentsToBeInserted) {
LOGGER.info("Saving {} to database.", student.getName());
studentDatabase.insert(student);
} }
}
private void commitModify() { @Override
var modifiedStudents = context.get(IUnitOfWork.MODIFY); public void registerDeleted(Weapon weapon) {
for (var student : modifiedStudents) { LOGGER.info("Registering {} for delete in context.", weapon.getName());
LOGGER.info("Modifying {} to database.", student.getName()); register(weapon, UnitActions.DELETE.getActionValue());
studentDatabase.modify(student);
} }
}
private void commitDelete() { private void register(Weapon weapon, String operation) {
var deletedStudents = context.get(IUnitOfWork.DELETE); var weaponsToOperate = context.get(operation);
for (var student : deletedStudents) { if (weaponsToOperate == null) {
LOGGER.info("Deleting {} to database.", student.getName()); weaponsToOperate = new ArrayList<>();
studentDatabase.delete(student); }
weaponsToOperate.add(weapon);
context.put(operation, weaponsToOperate);
}
/**
* All UnitOfWork operations are batched and executed together on commit only.
*/
@Override
public void commit() {
if (context == null || context.size() == 0) {
return;
}
LOGGER.info("Commit started");
if (context.containsKey(UnitActions.INSERT.getActionValue())) {
commitInsert();
}
if (context.containsKey(UnitActions.MODIFY.getActionValue())) {
commitModify();
}
if (context.containsKey(UnitActions.DELETE.getActionValue())) {
commitDelete();
}
LOGGER.info("Commit finished.");
}
private void commitInsert() {
var weaponsToBeInserted = context.get(UnitActions.INSERT.getActionValue());
for (var weapon : weaponsToBeInserted) {
LOGGER.info("Inserting a new weapon {} to sales rack.", weapon.getName());
weaponDatabase.insert(weapon);
}
}
private void commitModify() {
var modifiedWeapons = context.get(UnitActions.MODIFY.getActionValue());
for (var weapon : modifiedWeapons) {
LOGGER.info("Scheduling {} for modification work.", weapon.getName());
weaponDatabase.modify(weapon);
}
}
private void commitDelete() {
var deletedWeapons = context.get(UnitActions.DELETE.getActionValue());
for (var weapon : deletedWeapons) {
LOGGER.info("Scrapping {}.", weapon.getName());
weaponDatabase.delete(weapon);
}
} }
}
} }
``` ```
Finally, here's how we use the `StudentRepository` and `commit` the transaction. Here is how the whole app is put together.
```java ```java
studentRepository.registerNew(ram); // create some weapons
studentRepository.registerModified(shyam); var enchantedHammer = new Weapon(1, "enchanted hammer");
studentRepository.registerDeleted(gopi); var brokenGreatSword = new Weapon(2, "broken great sword");
studentRepository.commit(); var silverTrident = new Weapon(3, "silver trident");
// create repository
var weaponRepository = new ArmsDealer(new HashMap<String, List<Weapon>>(), new WeaponDatabase());
// perform operations on the weapons
weaponRepository.registerNew(enchantedHammer);
weaponRepository.registerModified(silverTrident);
weaponRepository.registerDeleted(brokenGreatSword);
weaponRepository.commit();
```
Here is the console output.
```
21:39:21.984 [main] INFO com.iluwatar.unitofwork.ArmsDealer - Registering enchanted hammer for insert in context.
21:39:21.989 [main] INFO com.iluwatar.unitofwork.ArmsDealer - Registering silver trident for modify in context.
21:39:21.989 [main] INFO com.iluwatar.unitofwork.ArmsDealer - Registering broken great sword for delete in context.
21:39:21.989 [main] INFO com.iluwatar.unitofwork.ArmsDealer - Commit started
21:39:21.989 [main] INFO com.iluwatar.unitofwork.ArmsDealer - Inserting a new weapon enchanted hammer to sales rack.
21:39:21.989 [main] INFO com.iluwatar.unitofwork.ArmsDealer - Scheduling silver trident for modification work.
21:39:21.989 [main] INFO com.iluwatar.unitofwork.ArmsDealer - Scrapping broken great sword.
21:39:21.989 [main] INFO com.iluwatar.unitofwork.ArmsDealer - Commit finished.
``` ```
## Class diagram ## Class diagram
@ -186,7 +190,7 @@ Use the Unit Of Work pattern when
* To optimize the time taken for database transactions. * To optimize the time taken for database transactions.
* To send changes to database as a unit of work which ensures atomicity of the transaction. * To send changes to database as a unit of work which ensures atomicity of the transaction.
* To reduce number of database calls. * To reduce the number of database calls.
## Tutorials ## Tutorials

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<class-diagram version="1.2.1" icons="true" always-add-relationships="false" generalizations="true" realizations="true" <class-diagram version="1.2.1" icons="true" always-add-relationships="false" generalizations="true" realizations="true"
associations="true" dependencies="false" nesting-relationships="true" router="FAN"> associations="true" dependencies="false" nesting-relationships="true" router="FAN">
<class id="1" language="java" name="com.iluwatar.unitofwork.StudentDatabase" project="unit-of-work" <class id="1" language="java" name="com.iluwatar.unitofwork.WeaponDatabase" project="unit-of-work"
file="/unit-of-work/src/main/java/com/iluwatar/unitofwork/StudentDatabase.java" binary="false" corner="BOTTOM_RIGHT"> file="/unit-of-work/src/main/java/com/iluwatar/unitofwork/StudentDatabase.java" binary="false" corner="BOTTOM_RIGHT">
<position height="-1" width="-1" x="170" y="406"/> <position height="-1" width="-1" x="170" y="406"/>
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true" <display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
@ -28,7 +28,7 @@
<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.unitofwork.StudentRepository" project="unit-of-work" <class id="4" language="java" name="com.iluwatar.unitofwork.ArmsDealer" project="unit-of-work"
file="/unit-of-work/src/main/java/com/iluwatar/unitofwork/StudentRepository.java" binary="false" file="/unit-of-work/src/main/java/com/iluwatar/unitofwork/StudentRepository.java" binary="false"
corner="BOTTOM_RIGHT"> corner="BOTTOM_RIGHT">
<position height="-1" width="-1" x="377" y="166"/> <position height="-1" width="-1" x="377" y="166"/>
@ -38,7 +38,7 @@
<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="5" language="java" name="com.iluwatar.unitofwork.Student" project="unit-of-work" <class id="5" language="java" name="com.iluwatar.unitofwork.Weapon" project="unit-of-work"
file="/unit-of-work/src/main/java/com/iluwatar/unitofwork/Student.java" binary="false" corner="BOTTOM_RIGHT"> file="/unit-of-work/src/main/java/com/iluwatar/unitofwork/Student.java" binary="false" corner="BOTTOM_RIGHT">
<position height="-1" width="-1" x="696" y="130"/> <position height="-1" width="-1" x="696" y="130"/>
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true" <display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"

View File

@ -27,7 +27,7 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
/** /**
* {@link App} Application for managing student data. * {@link App} Application demonstrating unit of work pattern.
*/ */
public class App { public class App {
/** /**
@ -37,17 +37,19 @@ public class App {
*/ */
public static void main(String[] args) { public static void main(String[] args) {
var ram = new Student(1, "Ram", "Street 9, Cupertino"); // create some weapons
var shyam = new Student(2, "Shyam", "Z bridge, Pune"); var enchantedHammer = new Weapon(1, "enchanted hammer");
var gopi = new Student(3, "Gopi", "Street 10, Mumbai"); var brokenGreatSword = new Weapon(2, "broken great sword");
var silverTrident = new Weapon(3, "silver trident");
var context = new HashMap<String, List<Student>>(); // create repository
var studentDatabase = new StudentDatabase(); var weaponRepository = new ArmsDealer(new HashMap<String, List<Weapon>>(),
var studentRepository = new StudentRepository(context, studentDatabase); new WeaponDatabase());
studentRepository.registerNew(ram); // perform operations on the weapons
studentRepository.registerModified(shyam); weaponRepository.registerNew(enchantedHammer);
studentRepository.registerDeleted(gopi); weaponRepository.registerModified(silverTrident);
studentRepository.commit(); weaponRepository.registerDeleted(brokenGreatSword);
weaponRepository.commit();
} }
} }

View File

@ -30,41 +30,41 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
/** /**
* {@link StudentRepository} Student database repository. supports unit of work for student data. * {@link ArmsDealer} Weapon repository that supports unit of work for weapons.
*/ */
@Slf4j @Slf4j
@RequiredArgsConstructor @RequiredArgsConstructor
public class StudentRepository implements IUnitOfWork<Student> { public class ArmsDealer implements IUnitOfWork<Weapon> {
private final Map<String, List<Student>> context; private final Map<String, List<Weapon>> context;
private final StudentDatabase studentDatabase; private final WeaponDatabase weaponDatabase;
@Override @Override
public void registerNew(Student student) { public void registerNew(Weapon weapon) {
LOGGER.info("Registering {} for insert in context.", student.getName()); LOGGER.info("Registering {} for insert in context.", weapon.getName());
register(student, UnitActions.INSERT.getActionValue()); register(weapon, UnitActions.INSERT.getActionValue());
} }
@Override @Override
public void registerModified(Student student) { public void registerModified(Weapon weapon) {
LOGGER.info("Registering {} for modify in context.", student.getName()); LOGGER.info("Registering {} for modify in context.", weapon.getName());
register(student, UnitActions.MODIFY.getActionValue()); register(weapon, UnitActions.MODIFY.getActionValue());
} }
@Override @Override
public void registerDeleted(Student student) { public void registerDeleted(Weapon weapon) {
LOGGER.info("Registering {} for delete in context.", student.getName()); LOGGER.info("Registering {} for delete in context.", weapon.getName());
register(student, UnitActions.DELETE.getActionValue()); register(weapon, UnitActions.DELETE.getActionValue());
} }
private void register(Student student, String operation) { private void register(Weapon weapon, String operation) {
var studentsToOperate = context.get(operation); var weaponsToOperate = context.get(operation);
if (studentsToOperate == null) { if (weaponsToOperate == null) {
studentsToOperate = new ArrayList<>(); weaponsToOperate = new ArrayList<>();
} }
studentsToOperate.add(student); weaponsToOperate.add(weapon);
context.put(operation, studentsToOperate); context.put(operation, weaponsToOperate);
} }
/** /**
@ -90,26 +90,26 @@ public class StudentRepository implements IUnitOfWork<Student> {
} }
private void commitInsert() { private void commitInsert() {
var studentsToBeInserted = context.get(UnitActions.INSERT.getActionValue()); var weaponsToBeInserted = context.get(UnitActions.INSERT.getActionValue());
for (var student : studentsToBeInserted) { for (var weapon : weaponsToBeInserted) {
LOGGER.info("Saving {} to database.", student.getName()); LOGGER.info("Inserting a new weapon {} to sales rack.", weapon.getName());
studentDatabase.insert(student); weaponDatabase.insert(weapon);
} }
} }
private void commitModify() { private void commitModify() {
var modifiedStudents = context.get(UnitActions.MODIFY.getActionValue()); var modifiedWeapons = context.get(UnitActions.MODIFY.getActionValue());
for (var student : modifiedStudents) { for (var weapon : modifiedWeapons) {
LOGGER.info("Modifying {} to database.", student.getName()); LOGGER.info("Scheduling {} for modification work.", weapon.getName());
studentDatabase.modify(student); weaponDatabase.modify(weapon);
} }
} }
private void commitDelete() { private void commitDelete() {
var deletedStudents = context.get(UnitActions.DELETE.getActionValue()); var deletedWeapons = context.get(UnitActions.DELETE.getActionValue());
for (var student : deletedStudents) { for (var weapon : deletedWeapons) {
LOGGER.info("Deleting {} to database.", student.getName()); LOGGER.info("Scrapping {}.", weapon.getName());
studentDatabase.delete(student); weaponDatabase.delete(weapon);
} }
} }
} }

View File

@ -27,14 +27,12 @@ import lombok.Getter;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
/** /**
* {@link Student} is an entity. * {@link Weapon} is an entity.
*/ */
@Getter @Getter
@RequiredArgsConstructor @RequiredArgsConstructor
public class Student { public class Weapon {
private final Integer id; private final Integer id;
private final String name; private final String name;
private final String address;
} }

View File

@ -24,19 +24,19 @@
package com.iluwatar.unitofwork; package com.iluwatar.unitofwork;
/** /**
* Act as Database for student records. * Act as database for weapon records.
*/ */
public class StudentDatabase { public class WeaponDatabase {
public void insert(Student student) { public void insert(Weapon weapon) {
//Some insert logic to DB //Some insert logic to DB
} }
public void modify(Student student) { public void modify(Weapon weapon) {
//Some modify logic to DB //Some modify logic to DB
} }
public void delete(Student student) { public void delete(Weapon weapon) {
//Some delete logic to DB //Some delete logic to DB
} }
} }

View File

@ -36,102 +36,102 @@ import java.util.Map;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
/** /**
* tests {@link StudentRepository} * tests {@link ArmsDealer}
*/ */
class StudentRepositoryTest { class ArmsDealerTest {
private final Student student1 = new Student(1, "Ram", "street 9, cupertino"); private final Weapon weapon1 = new Weapon(1, "battle ram");
private final Student student2 = new Student(1, "Sham", "Z bridge, pune"); private final Weapon weapon2 = new Weapon(1, "wooden lance");
private final Map<String, List<Student>> context = new HashMap<>(); private final Map<String, List<Weapon>> context = new HashMap<>();
private final StudentDatabase studentDatabase = mock(StudentDatabase.class); private final WeaponDatabase weaponDatabase = mock(WeaponDatabase.class);
private final StudentRepository studentRepository = new StudentRepository(context, studentDatabase);; private final ArmsDealer armsDealer = new ArmsDealer(context, weaponDatabase);;
@Test @Test
void shouldSaveNewStudentWithoutWritingToDb() { void shouldSaveNewStudentWithoutWritingToDb() {
studentRepository.registerNew(student1); armsDealer.registerNew(weapon1);
studentRepository.registerNew(student2); armsDealer.registerNew(weapon2);
assertEquals(2, context.get(UnitActions.INSERT.getActionValue()).size()); assertEquals(2, context.get(UnitActions.INSERT.getActionValue()).size());
verifyNoMoreInteractions(studentDatabase); verifyNoMoreInteractions(weaponDatabase);
} }
@Test @Test
void shouldSaveDeletedStudentWithoutWritingToDb() { void shouldSaveDeletedStudentWithoutWritingToDb() {
studentRepository.registerDeleted(student1); armsDealer.registerDeleted(weapon1);
studentRepository.registerDeleted(student2); armsDealer.registerDeleted(weapon2);
assertEquals(2, context.get(UnitActions.DELETE.getActionValue()).size()); assertEquals(2, context.get(UnitActions.DELETE.getActionValue()).size());
verifyNoMoreInteractions(studentDatabase); verifyNoMoreInteractions(weaponDatabase);
} }
@Test @Test
void shouldSaveModifiedStudentWithoutWritingToDb() { void shouldSaveModifiedStudentWithoutWritingToDb() {
studentRepository.registerModified(student1); armsDealer.registerModified(weapon1);
studentRepository.registerModified(student2); armsDealer.registerModified(weapon2);
assertEquals(2, context.get(UnitActions.MODIFY.getActionValue()).size()); assertEquals(2, context.get(UnitActions.MODIFY.getActionValue()).size());
verifyNoMoreInteractions(studentDatabase); verifyNoMoreInteractions(weaponDatabase);
} }
@Test @Test
void shouldSaveAllLocalChangesToDb() { void shouldSaveAllLocalChangesToDb() {
context.put(UnitActions.INSERT.getActionValue(), List.of(student1)); context.put(UnitActions.INSERT.getActionValue(), List.of(weapon1));
context.put(UnitActions.MODIFY.getActionValue(), List.of(student1)); context.put(UnitActions.MODIFY.getActionValue(), List.of(weapon1));
context.put(UnitActions.DELETE.getActionValue(), List.of(student1)); context.put(UnitActions.DELETE.getActionValue(), List.of(weapon1));
studentRepository.commit(); armsDealer.commit();
verify(studentDatabase, times(1)).insert(student1); verify(weaponDatabase, times(1)).insert(weapon1);
verify(studentDatabase, times(1)).modify(student1); verify(weaponDatabase, times(1)).modify(weapon1);
verify(studentDatabase, times(1)).delete(student1); verify(weaponDatabase, times(1)).delete(weapon1);
} }
@Test @Test
void shouldNotWriteToDbIfContextIsNull() { void shouldNotWriteToDbIfContextIsNull() {
var studentRepository = new StudentRepository(null, studentDatabase); var weaponRepository = new ArmsDealer(null, weaponDatabase);
studentRepository.commit(); weaponRepository.commit();
verifyNoMoreInteractions(studentDatabase); verifyNoMoreInteractions(weaponDatabase);
} }
@Test @Test
void shouldNotWriteToDbIfNothingToCommit() { void shouldNotWriteToDbIfNothingToCommit() {
var studentRepository = new StudentRepository(new HashMap<>(), studentDatabase); var weaponRepository = new ArmsDealer(new HashMap<>(), weaponDatabase);
studentRepository.commit(); weaponRepository.commit();
verifyNoMoreInteractions(studentDatabase); verifyNoMoreInteractions(weaponDatabase);
} }
@Test @Test
void shouldNotInsertToDbIfNoRegisteredStudentsToBeCommitted() { void shouldNotInsertToDbIfNoRegisteredStudentsToBeCommitted() {
context.put(UnitActions.MODIFY.getActionValue(), List.of(student1)); context.put(UnitActions.MODIFY.getActionValue(), List.of(weapon1));
context.put(UnitActions.DELETE.getActionValue(), List.of(student1)); context.put(UnitActions.DELETE.getActionValue(), List.of(weapon1));
studentRepository.commit(); armsDealer.commit();
verify(studentDatabase, never()).insert(student1); verify(weaponDatabase, never()).insert(weapon1);
} }
@Test @Test
void shouldNotModifyToDbIfNotRegisteredStudentsToBeCommitted() { void shouldNotModifyToDbIfNotRegisteredStudentsToBeCommitted() {
context.put(UnitActions.INSERT.getActionValue(), List.of(student1)); context.put(UnitActions.INSERT.getActionValue(), List.of(weapon1));
context.put(UnitActions.DELETE.getActionValue(), List.of(student1)); context.put(UnitActions.DELETE.getActionValue(), List.of(weapon1));
studentRepository.commit(); armsDealer.commit();
verify(studentDatabase, never()).modify(student1); verify(weaponDatabase, never()).modify(weapon1);
} }
@Test @Test
void shouldNotDeleteFromDbIfNotRegisteredStudentsToBeCommitted() { void shouldNotDeleteFromDbIfNotRegisteredStudentsToBeCommitted() {
context.put(UnitActions.INSERT.getActionValue(), List.of(student1)); context.put(UnitActions.INSERT.getActionValue(), List.of(weapon1));
context.put(UnitActions.MODIFY.getActionValue(), List.of(student1)); context.put(UnitActions.MODIFY.getActionValue(), List.of(weapon1));
studentRepository.commit(); armsDealer.commit();
verify(studentDatabase, never()).delete(student1); verify(weaponDatabase, never()).delete(weapon1);
} }
} }

View File

@ -10,19 +10,80 @@ tags:
--- ---
## Intent ## Intent
Provide objects which follow value semantics rather than reference semantics. Provide objects which follow value semantics rather than reference semantics.
This means value objects' equality are not based on identity. Two value objects are This means value objects' equality is not based on identity. Two value objects are
equal when they have the same value, not necessarily being the same object. equal when they have the same value, not necessarily being the same object.
## Explanation
Real-world example
> There is a class for hero statistics in a role-playing game. The statistics contain attributes
> such as strength, intelligence, and luck. The statistics of different heroes should be equal
> when all the attributes are equal.
In plain words
> Value objects are equal when their attributes have the same value
Wikipedia says
> In computer science, a value object is a small object that represents a simple entity whose
> equality is not based on identity: i.e. two value objects are equal when they have the same
> value, not necessarily being the same object.
**Programmatic Example**
Here is the `HeroStat` class that is the value object. Notice the use of
[Lombok's `@Value`](https://projectlombok.org/features/Value) annotation.
```java
@Value(staticConstructor = "valueOf")
class HeroStat {
int strength;
int intelligence;
int luck;
}
```
The example creates three different `HeroStat`s and compares their equality.
```java
var statA = HeroStat.valueOf(10, 5, 0);
var statB = HeroStat.valueOf(10, 5, 0);
var statC = HeroStat.valueOf(5, 1, 8);
LOGGER.info(statA.toString());
LOGGER.info(statB.toString());
LOGGER.info(statC.toString());
LOGGER.info("Is statA and statB equal : {}", statA.equals(statB));
LOGGER.info("Is statA and statC equal : {}", statA.equals(statC));
```
Here's the console output.
```
20:11:12.199 [main] INFO com.iluwatar.value.object.App - HeroStat(strength=10, intelligence=5, luck=0)
20:11:12.202 [main] INFO com.iluwatar.value.object.App - HeroStat(strength=10, intelligence=5, luck=0)
20:11:12.202 [main] INFO com.iluwatar.value.object.App - HeroStat(strength=5, intelligence=1, luck=8)
20:11:12.202 [main] INFO com.iluwatar.value.object.App - Is statA and statB equal : true
20:11:12.203 [main] INFO com.iluwatar.value.object.App - Is statA and statC equal : false
```
## Class diagram ## Class diagram
![alt text](./etc/value-object.png "Value Object") ![alt text](./etc/value-object.png "Value Object")
## Applicability ## Applicability
Use the Value Object when Use the Value Object when
* You need to measure the objects' equality based on the objects' value * The object's equality needs to be based on the object's value
## Real world examples ## Known uses
* [java.util.Optional](https://docs.oracle.com/javase/8/docs/api/java/util/Optional.html) * [java.util.Optional](https://docs.oracle.com/javase/8/docs/api/java/util/Optional.html)
* [java.time.LocalDate](https://docs.oracle.com/javase/8/docs/api/java/time/LocalDate.html) * [java.time.LocalDate](https://docs.oracle.com/javase/8/docs/api/java/time/LocalDate.html)
@ -31,6 +92,7 @@ Use the Value Object when
## Credits ## Credits
* [Patterns of Enterprise Application Architecture](http://www.martinfowler.com/books/eaa.html) * [Patterns of Enterprise Application Architecture](http://www.martinfowler.com/books/eaa.html)
* [ValueObject](https://martinfowler.com/bliki/ValueObject.html)
* [VALJOs - Value Java Objects : Stephen Colebourne's blog](http://blog.joda.org/2014/03/valjos-value-java-objects.html) * [VALJOs - Value Java Objects : Stephen Colebourne's blog](http://blog.joda.org/2014/03/valjos-value-java-objects.html)
* [Value Object : Wikipedia](https://en.wikipedia.org/wiki/Value_object) * [Value Object : Wikipedia](https://en.wikipedia.org/wiki/Value_object)
* [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) * [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)

View File

@ -43,7 +43,7 @@ import lombok.extern.slf4j.Slf4j;
public class App { public class App {
/** /**
* This practice creates three HeroStats(Value object) and checks equality between those. * This example creates three HeroStats (value objects) and checks equality between those.
*/ */
public static void main(String[] args) { public static void main(String[] args) {
var statA = HeroStat.valueOf(10, 5, 0); var statA = HeroStat.valueOf(10, 5, 0);
@ -51,6 +51,8 @@ public class App {
var statC = HeroStat.valueOf(5, 1, 8); var statC = HeroStat.valueOf(5, 1, 8);
LOGGER.info(statA.toString()); LOGGER.info(statA.toString());
LOGGER.info(statB.toString());
LOGGER.info(statC.toString());
LOGGER.info("Is statA and statB equal : {}", statA.equals(statB)); LOGGER.info("Is statA and statB equal : {}", statA.equals(statB));
LOGGER.info("Is statA and statC equal : {}", statA.equals(statC)); LOGGER.info("Is statA and statC equal : {}", statA.equals(statC));

View File

@ -23,10 +23,7 @@
package com.iluwatar.value.object; package com.iluwatar.value.object;
import lombok.EqualsAndHashCode; import lombok.Value;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
/** /**
* HeroStat is a value object. * HeroStat is a value object.
@ -35,23 +32,10 @@ import lombok.ToString;
* http://docs.oracle.com/javase/8/docs/api/java/lang/doc-files/ValueBased.html * http://docs.oracle.com/javase/8/docs/api/java/lang/doc-files/ValueBased.html
* </a> * </a>
*/ */
@Getter @Value(staticConstructor = "valueOf")
@ToString class HeroStat {
@EqualsAndHashCode
@RequiredArgsConstructor
public class HeroStat {
// Stats for a hero
private final int strength;
private final int intelligence;
private final int luck;
// Static factory method to create new instances.
public static HeroStat valueOf(int strength, int intelligence, int luck) {
return new HeroStat(strength, intelligence, luck);
}
// The clone() method should not be public. Just don't override it.
int strength;
int intelligence;
int luck;
} }