();
+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
+

## Applicability
+
Use the Factory Kit pattern when
-* a class can't anticipate the class of objects it must create
-* you just want a new instance of a custom builder instead of the global one
-* you explicitly want to define types of objects, that factory can build
-* you want a separated builder and creator interface
+* The factory class can't anticipate the types of objects it must create
+* A new instance of a custom builder is needed instead of a global one
+* The types of objects that the factory can build need to be defined outside the class
+* 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
-* [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)
diff --git a/factory-kit/src/main/java/com/iluwatar/factorykit/App.java b/factory-kit/src/main/java/com/iluwatar/factorykit/App.java
index e5f0b4285..7c636609a 100644
--- a/factory-kit/src/main/java/com/iluwatar/factorykit/App.java
+++ b/factory-kit/src/main/java/com/iluwatar/factorykit/App.java
@@ -23,14 +23,16 @@
package com.iluwatar.factorykit;
+import java.util.ArrayList;
+
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
- * directly in the factory-kit instance.
+ * directly in the factory kit instance.
*
- * In the given example {@link WeaponFactory} represents the factory-kit, that contains four
+ *
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.
*
*
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.BOW, Bow::new);
});
- var axe = factory.create(WeaponType.AXE);
- LOGGER.info(axe.toString());
+ var list = new ArrayList();
+ 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()));
}
}
diff --git a/metadata-mapping/README.md b/metadata-mapping/README.md
new file mode 100644
index 000000000..5dcd932a7
--- /dev/null
+++ b/metadata-mapping/README.md
@@ -0,0 +1,182 @@
+---
+layout: pattern
+title: Metadata Mapping
+folder: metadata-mapping
+permalink: /patterns/metadata-mapping/
+categories: Architectural
+language: en
+tags:
+ - Data access
+---
+
+## Intent
+
+Holds details of object-relational mapping in the metadata.
+
+## Explanation
+
+Real world example
+
+> Hibernate ORM Tool uses Metadata Mapping Pattern to specify the mapping between classes and tables either using XML or annotations in code.
+
+In plain words
+
+> Metadata Mapping specifies the mapping between classes and tables so that we could treat a table of any database like a Java class.
+
+Wikipedia says
+
+> Create a "virtual [object database](https://en.wikipedia.org/wiki/Object_database)" that can be used from within the programming language.
+
+**Programmatic Example**
+
+We give an example about visiting the information of `USER` table in `h2` database. Firstly, we create `USER` table with `h2`:
+
+```java
+@Slf4j
+public class DatabaseUtil {
+ private static final String DB_URL = "jdbc:h2:mem:metamapping";
+ private static final String CREATE_SCHEMA_SQL = "DROP TABLE IF EXISTS `user`;"
+ + "CREATE TABLE `user` (\n"
+ + " `id` int(11) NOT NULL AUTO_INCREMENT,\n"
+ + " `username` varchar(255) NOT NULL,\n"
+ + " `password` varchar(255) NOT NULL,\n"
+ + " PRIMARY KEY (`id`)\n"
+ + ");";
+
+ /**
+ * Create database.
+ */
+ static {
+ LOGGER.info("create h2 database");
+ var source = new JdbcDataSource();
+ source.setURL(DB_URL);
+ try (var statement = source.getConnection().createStatement()) {
+ statement.execute(CREATE_SCHEMA_SQL);
+ } catch (SQLException e) {
+ LOGGER.error("unable to create h2 data source", e);
+ }
+ }
+}
+```
+
+Correspondingly, here's the basic `User` entity.
+
+```java
+@Setter
+@Getter
+@ToString
+public class User {
+ private Integer id;
+ private String username;
+ private String password;
+
+ /**
+ * Get a user.
+ * @param username user name
+ * @param password user password
+ */
+ public User(String username, String password) {
+ this.username = username;
+ this.password = password;
+ }
+}
+```
+
+Then we write a `xml` file to show the mapping between the table and the object:
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+We use `Hibernate` to resolve the mapping and connect to our database, here's its configuration:
+
+```xml
+
+
+
+
+
+ jdbc:h2:mem:metamapping
+ org.h2.Driver
+
+ 1
+
+ org.hibernate.dialect.H2Dialect
+
+ false
+
+ create-drop
+
+
+
+```
+
+Then we can get access to the table just like an object with `Hibernate`, here's some CRUDs:
+
+```java
+@Slf4j
+public class UserService {
+ private static final SessionFactory factory = HibernateUtil.getSessionFactory();
+
+ /**
+ * List all users.
+ * @return list of users
+ */
+ public List listUser() {
+ LOGGER.info("list all users.");
+ List users = new ArrayList<>();
+ try (var session = factory.openSession()) {
+ var tx = session.beginTransaction();
+ List userIter = session.createQuery("FROM User").list();
+ for (var iterator = userIter.iterator(); iterator.hasNext();) {
+ users.add(iterator.next());
+ }
+ tx.commit();
+ } catch (HibernateException e) {
+ LOGGER.debug("fail to get users", e);
+ }
+ return users;
+ }
+
+ // other CRUDs ->
+ ...
+
+ public void close() {
+ HibernateUtil.shutdown();
+ }
+}
+```
+
+## Class diagram
+
+
+
+## Applicability
+
+Use the Metadata Mapping when:
+
+- you want reduce the amount of work needed to handle database mapping.
+
+## Known uses
+
+[Hibernate](https://hibernate.org/), [EclipseLink](https://www.eclipse.org/eclipselink/), [MyBatis](https://blog.mybatis.org/)......
+
+## Credits
+
+- [J2EE Design Patterns](https://www.amazon.com/gp/product/0596004273/ref=as_li_tl?ie=UTF8&camp=1789&creative=9325&creativeASIN=0596004273&linkCode=as2&tag=javadesignpat-20&linkId=48d37c67fb3d845b802fa9b619ad8f31)
+
diff --git a/metadata-mapping/etc/metamapping.png b/metadata-mapping/etc/metamapping.png
new file mode 100644
index 000000000..b1e89f8af
Binary files /dev/null and b/metadata-mapping/etc/metamapping.png differ
diff --git a/metadata-mapping/etc/metamapping.puml b/metadata-mapping/etc/metamapping.puml
new file mode 100644
index 000000000..daa8c37de
--- /dev/null
+++ b/metadata-mapping/etc/metamapping.puml
@@ -0,0 +1,32 @@
+@startuml
+interface com.iluwatar.metamapping.service.UserService {
++ List listUser()
++ int createUser(User)
++ void updateUser(Integer,User)
++ void deleteUser(Integer)
++ User getUser(Integer)
++ void close()
+}
+class com.iluwatar.metamapping.utils.DatabaseUtil {
++ {static} void createDataSource()
+}
+class com.iluwatar.metamapping.model.User {
+- Integer id
+- String username
+- String password
++ User(String username, String password)
+}
+class com.iluwatar.metamapping.utils.HibernateUtil {
++ {static} SessionFactory getSessionFactory()
++ {static} void shutdown()
+}
+class com.iluwatar.metamapping.App {
++ {static} void main(String[])
++ {static} List generateSampleUsers()
+}
+
+com.iluwatar.metamapping.service.UserService <.. com.iluwatar.metamapping.App
+com.iluwatar.metamapping.model.User <.. com.iluwatar.metamapping.service.UserService
+com.iluwatar.metamapping.utils.HibernateUtil <.. com.iluwatar.metamapping.service.UserService
+com.iluwatar.metamapping.utils.DatabaseUtil <-- com.iluwatar.metamapping.utils.HibernateUtil
+@enduml
\ No newline at end of file
diff --git a/metadata-mapping/pom.xml b/metadata-mapping/pom.xml
new file mode 100644
index 000000000..43c9621df
--- /dev/null
+++ b/metadata-mapping/pom.xml
@@ -0,0 +1,87 @@
+
+
+
+
+ java-design-patterns
+ com.iluwatar
+ 1.26.0-SNAPSHOT
+
+ 4.0.0
+
+ metadata-mapping
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+ com.h2database
+ h2
+
+
+ org.hibernate
+ hibernate-core
+
+
+ com.h2database
+ h2
+
+
+ javax.xml.bind
+ jaxb-api
+
+
+ com.sun.xml.bind
+ jaxb-impl
+
+
+ com.sun.istack
+ istack-commons-runtime
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-assembly-plugin
+
+
+
+
+
+ com.iluwatar.metamapping.App
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/metadata-mapping/src/main/java/com/iluwatar/metamapping/App.java b/metadata-mapping/src/main/java/com/iluwatar/metamapping/App.java
new file mode 100644
index 000000000..ff0377590
--- /dev/null
+++ b/metadata-mapping/src/main/java/com/iluwatar/metamapping/App.java
@@ -0,0 +1,72 @@
+package com.iluwatar.metamapping;
+
+import com.iluwatar.metamapping.model.User;
+import com.iluwatar.metamapping.service.UserService;
+import com.iluwatar.metamapping.utils.DatabaseUtil;
+import java.util.List;
+import lombok.extern.slf4j.Slf4j;
+import org.hibernate.service.ServiceRegistry;
+
+/**
+ * Metadata Mapping specifies the mapping
+ * between classes and tables so that
+ * we could treat a table of any database like a Java class.
+ *
+ * With hibernate, we achieve list/create/update/delete/get operations:
+ * 1)Create the H2 Database in {@link DatabaseUtil}.
+ * 2)Hibernate resolve hibernate.cfg.xml and generate service like save/list/get/delete.
+ * For learning metadata mapping pattern, we go deeper into Hibernate here:
+ * a)read properties from hibernate.cfg.xml and mapping from *.hbm.xml
+ * b)create session factory to generate session interacting with database
+ * c)generate session with factory pattern
+ * d)create query object or use basic api with session,
+ * hibernate will convert all query to database query according to metadata
+ * 3)We encapsulate hibernate service in {@link UserService} for our use.
+ * @see org.hibernate.cfg.Configuration#configure(String)
+ * @see org.hibernate.cfg.Configuration#buildSessionFactory(ServiceRegistry)
+ * @see org.hibernate.internal.SessionFactoryImpl#openSession()
+ */
+@Slf4j
+public class App {
+ /**
+ * Program entry point.
+ *
+ * @param args command line args.
+ * @throws Exception if any error occurs.
+ */
+ public static void main(String[] args) throws Exception {
+ // get service
+ var userService = new UserService();
+ // use create service to add users
+ for (var user: generateSampleUsers()) {
+ var id = userService.createUser(user);
+ LOGGER.info("Add user" + user + "at" + id + ".");
+ }
+ // use list service to get users
+ var users = userService.listUser();
+ LOGGER.info(String.valueOf(users));
+ // use get service to get a user
+ var user = userService.getUser(1);
+ LOGGER.info(String.valueOf(user));
+ // change password of user 1
+ user.setPassword("new123");
+ // use update service to update user 1
+ userService.updateUser(1, user);
+ // use delete service to delete user 2
+ userService.deleteUser(2);
+ // close service
+ userService.close();
+ }
+
+ /**
+ * Generate users.
+ *
+ * @return list of users.
+ */
+ public static List generateSampleUsers() {
+ final var user1 = new User("ZhangSan", "zhs123");
+ final var user2 = new User("LiSi", "ls123");
+ final var user3 = new User("WangWu", "ww123");
+ return List.of(user1, user2, user3);
+ }
+}
\ No newline at end of file
diff --git a/metadata-mapping/src/main/java/com/iluwatar/metamapping/model/User.java b/metadata-mapping/src/main/java/com/iluwatar/metamapping/model/User.java
new file mode 100644
index 000000000..0bf2575b1
--- /dev/null
+++ b/metadata-mapping/src/main/java/com/iluwatar/metamapping/model/User.java
@@ -0,0 +1,29 @@
+package com.iluwatar.metamapping.model;
+
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+
+/**
+ * User Entity.
+ */
+@Setter
+@Getter
+@ToString
+public class User {
+ private Integer id;
+ private String username;
+ private String password;
+
+ public User() {}
+
+ /**
+ * Get a user.
+ * @param username user name
+ * @param password user password
+ */
+ public User(String username, String password) {
+ this.username = username;
+ this.password = password;
+ }
+}
\ No newline at end of file
diff --git a/metadata-mapping/src/main/java/com/iluwatar/metamapping/service/UserService.java b/metadata-mapping/src/main/java/com/iluwatar/metamapping/service/UserService.java
new file mode 100644
index 000000000..1f85be0d5
--- /dev/null
+++ b/metadata-mapping/src/main/java/com/iluwatar/metamapping/service/UserService.java
@@ -0,0 +1,114 @@
+package com.iluwatar.metamapping.service;
+
+import com.iluwatar.metamapping.model.User;
+import com.iluwatar.metamapping.utils.HibernateUtil;
+import java.util.ArrayList;
+import java.util.List;
+import lombok.extern.slf4j.Slf4j;
+import org.hibernate.HibernateException;
+import org.hibernate.SessionFactory;
+
+/**
+ * Service layer for user.
+ */
+@Slf4j
+public class UserService {
+ private static final SessionFactory factory = HibernateUtil.getSessionFactory();
+
+ /**
+ * List all users.
+ * @return list of users
+ */
+ public List listUser() {
+ LOGGER.info("list all users.");
+ List users = new ArrayList<>();
+ try (var session = factory.openSession()) {
+ var tx = session.beginTransaction();
+ List userIter = session.createQuery("FROM User").list();
+ for (var iterator = userIter.iterator(); iterator.hasNext();) {
+ users.add(iterator.next());
+ }
+ tx.commit();
+ } catch (HibernateException e) {
+ LOGGER.debug("fail to get users", e);
+ }
+ return users;
+ }
+
+ /**
+ * Add a user.
+ * @param user user entity
+ * @return user id
+ */
+ public int createUser(User user) {
+ LOGGER.info("create user: " + user.getUsername());
+ var id = -1;
+ try (var session = factory.openSession()) {
+ var tx = session.beginTransaction();
+ id = (Integer) session.save(user);
+ tx.commit();
+ } catch (HibernateException e) {
+ LOGGER.debug("fail to create user", e);
+ }
+ LOGGER.info("create user " + user.getUsername() + " at " + id);
+ return id;
+ }
+
+ /**
+ * Update user.
+ * @param id user id
+ * @param user new user entity
+ */
+ public void updateUser(Integer id, User user) {
+ LOGGER.info("update user at " + id);
+ try (var session = factory.openSession()) {
+ var tx = session.beginTransaction();
+ user.setId(id);
+ session.update(user);
+ tx.commit();
+ } catch (HibernateException e) {
+ LOGGER.debug("fail to update user", e);
+ }
+ }
+
+ /**
+ * Delete user.
+ * @param id user id
+ */
+ public void deleteUser(Integer id) {
+ LOGGER.info("delete user at: " + id);
+ try (var session = factory.openSession()) {
+ var tx = session.beginTransaction();
+ var user = session.get(User.class, id);
+ session.delete(user);
+ tx.commit();
+ } catch (HibernateException e) {
+ LOGGER.debug("fail to delete user", e);
+ }
+ }
+
+ /**
+ * Get user.
+ * @param id user id
+ * @return deleted user
+ */
+ public User getUser(Integer id) {
+ LOGGER.info("get user at: " + id);
+ User user = null;
+ try (var session = factory.openSession()) {
+ var tx = session.beginTransaction();
+ user = session.get(User.class, id);
+ tx.commit();
+ } catch (HibernateException e) {
+ LOGGER.debug("fail to get user", e);
+ }
+ return user;
+ }
+
+ /**
+ * Close hibernate.
+ */
+ public void close() {
+ HibernateUtil.shutdown();
+ }
+}
\ No newline at end of file
diff --git a/metadata-mapping/src/main/java/com/iluwatar/metamapping/utils/DatabaseUtil.java b/metadata-mapping/src/main/java/com/iluwatar/metamapping/utils/DatabaseUtil.java
new file mode 100644
index 000000000..b6d0200e8
--- /dev/null
+++ b/metadata-mapping/src/main/java/com/iluwatar/metamapping/utils/DatabaseUtil.java
@@ -0,0 +1,39 @@
+package com.iluwatar.metamapping.utils;
+
+import java.sql.SQLException;
+import lombok.extern.slf4j.Slf4j;
+import org.h2.jdbcx.JdbcDataSource;
+
+/**
+ * Create h2 database.
+ */
+@Slf4j
+public class DatabaseUtil {
+ private static final String DB_URL = "jdbc:h2:mem:metamapping";
+ private static final String CREATE_SCHEMA_SQL = "DROP TABLE IF EXISTS `user`;"
+ + "CREATE TABLE `user` (\n"
+ + " `id` int(11) NOT NULL AUTO_INCREMENT,\n"
+ + " `username` varchar(255) NOT NULL,\n"
+ + " `password` varchar(255) NOT NULL,\n"
+ + " PRIMARY KEY (`id`)\n"
+ + ");";
+
+ /**
+ * Hide constructor.
+ */
+ private DatabaseUtil() {}
+
+ /**
+ * Create database.
+ */
+ static {
+ LOGGER.info("create h2 database");
+ var source = new JdbcDataSource();
+ source.setURL(DB_URL);
+ try (var statement = source.getConnection().createStatement()) {
+ statement.execute(CREATE_SCHEMA_SQL);
+ } catch (SQLException e) {
+ LOGGER.error("unable to create h2 data source", e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/metadata-mapping/src/main/java/com/iluwatar/metamapping/utils/HibernateUtil.java b/metadata-mapping/src/main/java/com/iluwatar/metamapping/utils/HibernateUtil.java
new file mode 100644
index 000000000..ba405eb74
--- /dev/null
+++ b/metadata-mapping/src/main/java/com/iluwatar/metamapping/utils/HibernateUtil.java
@@ -0,0 +1,45 @@
+package com.iluwatar.metamapping.utils;
+
+import lombok.extern.slf4j.Slf4j;
+import org.hibernate.SessionFactory;
+import org.hibernate.cfg.Configuration;
+
+/**
+ * Manage hibernate.
+ */
+@Slf4j
+public class HibernateUtil {
+
+ private static final SessionFactory sessionFactory = buildSessionFactory();
+
+ /**
+ * Hide constructor.
+ */
+ private HibernateUtil() {}
+
+ /**
+ * Build session factory.
+ * @return session factory
+ */
+ private static SessionFactory buildSessionFactory() {
+ // Create the SessionFactory from hibernate.cfg.xml
+ return new Configuration().configure().buildSessionFactory();
+ }
+
+ /**
+ * Get session factory.
+ * @return session factory
+ */
+ public static SessionFactory getSessionFactory() {
+ return sessionFactory;
+ }
+
+ /**
+ * Close session factory.
+ */
+ public static void shutdown() {
+ // Close caches and connection pools
+ getSessionFactory().close();
+ }
+
+}
\ No newline at end of file
diff --git a/metadata-mapping/src/main/resources/com/iluwatar/metamapping/model/User.hbm.xml b/metadata-mapping/src/main/resources/com/iluwatar/metamapping/model/User.hbm.xml
new file mode 100644
index 000000000..cd63c552d
--- /dev/null
+++ b/metadata-mapping/src/main/resources/com/iluwatar/metamapping/model/User.hbm.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/metadata-mapping/src/main/resources/hibernate.cfg.xml b/metadata-mapping/src/main/resources/hibernate.cfg.xml
new file mode 100644
index 000000000..18dc198e8
--- /dev/null
+++ b/metadata-mapping/src/main/resources/hibernate.cfg.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+ jdbc:h2:mem:metamapping
+ org.h2.Driver
+
+ 1
+
+ org.hibernate.dialect.H2Dialect
+
+ false
+
+ create-drop
+
+
+
\ No newline at end of file
diff --git a/metadata-mapping/src/test/java/com/iluwatar/metamapping/AppTest.java b/metadata-mapping/src/test/java/com/iluwatar/metamapping/AppTest.java
new file mode 100644
index 000000000..127ddad0f
--- /dev/null
+++ b/metadata-mapping/src/test/java/com/iluwatar/metamapping/AppTest.java
@@ -0,0 +1,20 @@
+package com.iluwatar.metamapping;
+
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+
+/**
+ * Tests that metadata mapping example runs without errors.
+ */
+class AppTest {
+ /**
+ * 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.
+ */
+ @Test
+ void shouldExecuteMetaMappingWithoutException() {
+ assertDoesNotThrow(() -> App.main(new String[]{}));
+ }
+}
diff --git a/pom.xml b/pom.xml
index 5c3634b95..e58146c72 100644
--- a/pom.xml
+++ b/pom.xml
@@ -75,6 +75,7 @@
3.0
1.4.8
2.7
+ 4.0.1
https://sonarcloud.io
iluwatar
@@ -227,6 +228,8 @@
lockable-object
fanout-fanin
domain-model
+ composite-view
+ metadata-mapping
@@ -377,6 +380,11 @@
commons-io
${commons-io.version}
+
+ com.sun.istack
+ istack-commons-runtime
+ ${istack-commons-runtime.version}
+
diff --git a/reactor/src/main/java/com/iluwatar/reactor/framework/NioReactor.java b/reactor/src/main/java/com/iluwatar/reactor/framework/NioReactor.java
index 2db01bf88..afcaf0015 100644
--- a/reactor/src/main/java/com/iluwatar/reactor/framework/NioReactor.java
+++ b/reactor/src/main/java/com/iluwatar/reactor/framework/NioReactor.java
@@ -96,9 +96,11 @@ public class NioReactor {
* @throws IOException if any I/O error occurs.
*/
public void stop() throws InterruptedException, IOException {
- reactorMain.shutdownNow();
+ reactorMain.shutdown();
selector.wakeup();
- reactorMain.awaitTermination(4, TimeUnit.SECONDS);
+ if (!reactorMain.awaitTermination(4, TimeUnit.SECONDS)) {
+ reactorMain.shutdownNow();
+ }
selector.close();
LOGGER.info("Reactor stopped");
}
diff --git a/reactor/src/main/java/com/iluwatar/reactor/framework/ThreadPoolDispatcher.java b/reactor/src/main/java/com/iluwatar/reactor/framework/ThreadPoolDispatcher.java
index d8af72c96..7d4f610d1 100644
--- a/reactor/src/main/java/com/iluwatar/reactor/framework/ThreadPoolDispatcher.java
+++ b/reactor/src/main/java/com/iluwatar/reactor/framework/ThreadPoolDispatcher.java
@@ -64,6 +64,8 @@ public class ThreadPoolDispatcher implements Dispatcher {
@Override
public void stop() throws InterruptedException {
executorService.shutdown();
- executorService.awaitTermination(4, TimeUnit.SECONDS);
+ if (executorService.awaitTermination(4, TimeUnit.SECONDS)) {
+ executorService.shutdownNow();
+ }
}
}
diff --git a/spatial-partition/src/main/java/com/iluwatar/spatialpartition/SpatialPartitionBubbles.java b/spatial-partition/src/main/java/com/iluwatar/spatialpartition/SpatialPartitionBubbles.java
index 5faef509d..897466a99 100644
--- a/spatial-partition/src/main/java/com/iluwatar/spatialpartition/SpatialPartitionBubbles.java
+++ b/spatial-partition/src/main/java/com/iluwatar/spatialpartition/SpatialPartitionBubbles.java
@@ -34,11 +34,11 @@ import java.util.HashMap;
public class SpatialPartitionBubbles extends SpatialPartitionGeneric {
private final HashMap bubbles;
- private final QuadTree quadTree;
+ private final QuadTree bubblesQuadTree;
- SpatialPartitionBubbles(HashMap bubbles, QuadTree quadTree) {
+ SpatialPartitionBubbles(HashMap bubbles, QuadTree bubblesQuadTree) {
this.bubbles = bubbles;
- this.quadTree = quadTree;
+ this.bubblesQuadTree = bubblesQuadTree;
}
void handleCollisionsUsingQt(Bubble b) {
@@ -46,7 +46,7 @@ public class SpatialPartitionBubbles extends SpatialPartitionGeneric {
// centre of bubble and length = radius of bubble
var rect = new Rect(b.coordinateX, b.coordinateY, 2D * b.radius, 2D * b.radius);
var quadTreeQueryResult = new ArrayList();
- this.quadTree.query(rect, quadTreeQueryResult);
+ this.bubblesQuadTree.query(rect, quadTreeQueryResult);
//handling these collisions
b.handleCollision(quadTreeQueryResult, this.bubbles);
}
diff --git a/thread-pool/README.md b/thread-pool/README.md
index d6cb11c1f..248c3a5d4 100644
--- a/thread-pool/README.md
+++ b/thread-pool/README.md
@@ -18,11 +18,11 @@ threads and eliminating the latency of creating new threads.
## 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
-> and serve mighty amount of coffee cups. Creating a new thread for each task would be a waste so we
-> establish a thread pool.
+> and serve a mighty amount of coffee cups. Creating a new thread for each task would be a waste so
+> we establish a thread pool.
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.
```java
diff --git a/throttling/README.md b/throttling/README.md
index 66c02a1c0..333b714f1 100644
--- a/throttling/README.md
+++ b/throttling/README.md
@@ -16,10 +16,12 @@ Ensure that a given client is not able to access service resources more than the
## Explanation
-Real world example
+Real-world example
-> A large multinational corporation offers API to its customers. The API is rate-limited and each
-> customer can only make certain amount of calls per second.
+> A young human and an old dwarf walk into a bar. They start ordering beers from the bartender.
+> 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
@@ -33,30 +35,25 @@ In plain words
**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
-public class Tenant {
+public class BarCustomer {
- private final String name;
- private final int allowedCallsPerSecond;
+ @Getter
+ private final String name;
+ @Getter
+ private final int allowedCallsPerSecond;
- public Tenant(String name, int allowedCallsPerSecond, CallsCount callsCount) {
- if (allowedCallsPerSecond < 0) {
- throw new InvalidParameterException("Number of calls less than 0 not allowed");
+ public BarCustomer(String name, int allowedCallsPerSecond, CallsCount callsCount) {
+ if (allowedCallsPerSecond < 0) {
+ 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
@@ -76,14 +73,14 @@ public final class CallsCount {
}
public void reset() {
- LOGGER.debug("Resetting the map.");
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
-throttler timer.
+Next, the service that the tenants are calling is introduced. To track the call count, a throttler
+timer is used.
```java
public interface Throttler {
@@ -111,71 +108,103 @@ public class ThrottleTimerImpl implements Throttler {
}, 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);
- private final CallsCount callsCount;
+```java
+class Bartender {
- public B2BService(Throttler timer, CallsCount callsCount) {
- this.callsCount = callsCount;
- timer.start();
- }
+ private static final Logger LOGGER = LoggerFactory.getLogger(Bartender.class);
+ private final CallsCount callsCount;
- public int dummyCustomerApi(Tenant tenant) {
- var tenantName = tenant.getName();
- var count = callsCount.getCount(tenantName);
- LOGGER.debug("Counter for {} : {} ", tenant.getName(), count);
- if (count >= tenant.getAllowedCallsPerSecond()) {
- LOGGER.error("API access per second limit reached for: {}", tenantName);
- return -1;
+ public Bartender(Throttler timer, CallsCount callsCount) {
+ this.callsCount = callsCount;
+ timer.start();
}
- callsCount.incrementCount(tenantName);
- return getRandomCustomerId();
- }
- private int getRandomCustomerId() {
- return ThreadLocalRandom.current().nextInt(1, 10000);
- }
+ public int orderDrink(BarCustomer barCustomer) {
+ 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
-second and Nike to 6.
+Now it is possible to see the full example in action. `BarCustomer` young human is rate-limited to 2
+calls per second and the old dwarf to 4.
```java
- public static void main(String[] args) {
+public static void main(String[] args) {
var callsCount = new CallsCount();
- var adidas = new Tenant("Adidas", 5, callsCount);
- var nike = new Tenant("Nike", 6, callsCount);
+ var human = new BarCustomer("young human", 2, callsCount);
+ var dwarf = new BarCustomer("dwarf soldier", 4, callsCount);
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) {
- var timer = new ThrottleTimerImpl(10, callsCount);
- var service = new B2BService(timer, callsCount);
+ executorService.execute(() -> makeServiceCalls(human, callsCount));
+ executorService.execute(() -> makeServiceCalls(dwarf, 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.
- IntStream.range(0, 20).forEach(i -> {
- service.dummyCustomerApi(tenant);
- try {
- Thread.sleep(1);
- } catch (InterruptedException e) {
- LOGGER.error("Thread interrupted: {}", e.getMessage());
- }
+ IntStream.range(0, 50).forEach(i -> {
+ service.orderDrink(barCustomer);
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ 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
@@ -185,7 +214,7 @@ second and Nike to 6.
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.
## Credits
diff --git a/throttling/src/main/java/com/iluwatar/throttling/App.java b/throttling/src/main/java/com/iluwatar/throttling/App.java
index ab8aa8601..1db6ecf93 100644
--- a/throttling/src/main/java/com/iluwatar/throttling/App.java
+++ b/throttling/src/main/java/com/iluwatar/throttling/App.java
@@ -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
* and meet service level agreements, even when an increase in demand places load on resources.
*
- * 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.
*
- * ({@link Tenant}) is the Tenant POJO class with which many tenants can be created ({@link
- * B2BService}) is the service which is consumed by the tenants and is throttled.
+ * ({@link BarCustomer}) is the service tenant class having a name and the number of calls allowed.
+ * ({@link Bartender}) is the service which is consumed by the tenants and is throttled.
*/
@Slf4j
public class App {
@@ -50,33 +50,35 @@ public class App {
*/
public static void main(String[] args) {
var callsCount = new CallsCount();
- var adidas = new Tenant("Adidas", 5, callsCount);
- var nike = new Tenant("Nike", 6, callsCount);
+ var human = new BarCustomer("young human", 2, callsCount);
+ var dwarf = new BarCustomer("dwarf soldier", 4, callsCount);
var executorService = Executors.newFixedThreadPool(2);
- executorService.execute(() -> makeServiceCalls(adidas, callsCount));
- executorService.execute(() -> makeServiceCalls(nike, callsCount));
+ executorService.execute(() -> makeServiceCalls(human, callsCount));
+ executorService.execute(() -> makeServiceCalls(dwarf, callsCount));
executorService.shutdown();
try {
- executorService.awaitTermination(10, TimeUnit.SECONDS);
+ if (!executorService.awaitTermination(10, TimeUnit.SECONDS)) {
+ executorService.shutdownNow();
+ }
} 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) {
- var timer = new ThrottleTimerImpl(10, callsCount);
- var service = new B2BService(timer, callsCount);
+ 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.
- IntStream.range(0, 20).forEach(i -> {
- service.dummyCustomerApi(tenant);
+ IntStream.range(0, 50).forEach(i -> {
+ service.orderDrink(barCustomer);
try {
- Thread.sleep(1);
+ Thread.sleep(100);
} catch (InterruptedException e) {
LOGGER.error("Thread interrupted: {}", e.getMessage());
}
diff --git a/throttling/src/main/java/com/iluwatar/throttling/Tenant.java b/throttling/src/main/java/com/iluwatar/throttling/BarCustomer.java
similarity index 81%
rename from throttling/src/main/java/com/iluwatar/throttling/Tenant.java
rename to throttling/src/main/java/com/iluwatar/throttling/BarCustomer.java
index 60a08704f..b46670861 100644
--- a/throttling/src/main/java/com/iluwatar/throttling/Tenant.java
+++ b/throttling/src/main/java/com/iluwatar/throttling/BarCustomer.java
@@ -25,22 +25,26 @@ package com.iluwatar.throttling;
import java.security.InvalidParameterException;
-/**
- * A Pojo class to create a basic Tenant with the allowed calls per second.
- */
-public class Tenant {
+import lombok.Getter;
+/**
+ * BarCustomer is a tenant with a name and a number of allowed calls per second.
+ */
+public class BarCustomer {
+
+ @Getter
private final String name;
+ @Getter
private final int allowedCallsPerSecond;
/**
* Constructor.
*
- * @param name Name of the tenant
- * @param allowedCallsPerSecond The number of calls allowed for a particular tenant.
+ * @param name Name of the BarCustomer
+ * @param allowedCallsPerSecond The number of calls allowed for this particular tenant.
* @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) {
throw new InvalidParameterException("Number of calls less than 0 not allowed");
}
@@ -48,12 +52,4 @@ public class Tenant {
this.allowedCallsPerSecond = allowedCallsPerSecond;
callsCount.addTenant(name);
}
-
- public String getName() {
- return name;
- }
-
- public int getAllowedCallsPerSecond() {
- return allowedCallsPerSecond;
- }
}
diff --git a/throttling/src/main/java/com/iluwatar/throttling/B2BService.java b/throttling/src/main/java/com/iluwatar/throttling/Bartender.java
similarity index 75%
rename from throttling/src/main/java/com/iluwatar/throttling/B2BService.java
rename to throttling/src/main/java/com/iluwatar/throttling/Bartender.java
index 70657afd8..e81d770d7 100644
--- a/throttling/src/main/java/com/iluwatar/throttling/B2BService.java
+++ b/throttling/src/main/java/com/iluwatar/throttling/Bartender.java
@@ -29,33 +29,32 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
- * A service which accepts a tenant and throttles the resource based on the time given to the
- * tenant.
+ * Bartender is a service which accepts a BarCustomer (tenant) and throttles
+ * 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;
- public B2BService(Throttler timer, CallsCount callsCount) {
+ public Bartender(Throttler timer, CallsCount callsCount) {
this.callsCount = callsCount;
timer.start();
}
/**
- * Calls dummy customer api.
- *
+ * Orders a drink from the bartender.
* @return customer id which is randomly generated
*/
- public int dummyCustomerApi(Tenant tenant) {
- var tenantName = tenant.getName();
+ public int orderDrink(BarCustomer barCustomer) {
+ var tenantName = barCustomer.getName();
var count = callsCount.getCount(tenantName);
- LOGGER.debug("Counter for {} : {} ", tenant.getName(), count);
- if (count >= tenant.getAllowedCallsPerSecond()) {
- LOGGER.error("API access per second limit reached for: {}", 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();
}
diff --git a/throttling/src/main/java/com/iluwatar/throttling/CallsCount.java b/throttling/src/main/java/com/iluwatar/throttling/CallsCount.java
index 88a80d481..4196e10dd 100644
--- a/throttling/src/main/java/com/iluwatar/throttling/CallsCount.java
+++ b/throttling/src/main/java/com/iluwatar/throttling/CallsCount.java
@@ -69,7 +69,7 @@ public final class CallsCount {
* Resets the count of all the tenants in the map.
*/
public void reset() {
- LOGGER.debug("Resetting the map.");
tenantCallsCount.replaceAll((k, v) -> new AtomicLong(0));
+ LOGGER.info("reset counters");
}
}
diff --git a/throttling/src/test/java/com/iluwatar/throttling/TenantTest.java b/throttling/src/test/java/com/iluwatar/throttling/BarCustomerTest.java
similarity index 94%
rename from throttling/src/test/java/com/iluwatar/throttling/TenantTest.java
rename to throttling/src/test/java/com/iluwatar/throttling/BarCustomerTest.java
index 2ea33ec3d..9818fdf6b 100644
--- a/throttling/src/test/java/com/iluwatar/throttling/TenantTest.java
+++ b/throttling/src/test/java/com/iluwatar/throttling/BarCustomerTest.java
@@ -23,20 +23,21 @@
package com.iluwatar.throttling;
-import static org.junit.jupiter.api.Assertions.assertThrows;
+import org.junit.jupiter.api.Test;
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.
*/
-public class TenantTest {
+public class BarCustomerTest {
@Test
void constructorTest() {
assertThrows(InvalidParameterException.class, () -> {
- new Tenant("FailTenant", -1, new CallsCount());
+ new BarCustomer("sirBrave", -1, new CallsCount());
});
}
}
diff --git a/throttling/src/test/java/com/iluwatar/throttling/B2BServiceTest.java b/throttling/src/test/java/com/iluwatar/throttling/BartenderTest.java
similarity index 89%
rename from throttling/src/test/java/com/iluwatar/throttling/B2BServiceTest.java
rename to throttling/src/test/java/com/iluwatar/throttling/BartenderTest.java
index 93cf3efaa..b2d80fbd4 100644
--- a/throttling/src/test/java/com/iluwatar/throttling/B2BServiceTest.java
+++ b/throttling/src/test/java/com/iluwatar/throttling/BartenderTest.java
@@ -32,19 +32,18 @@ import org.junit.jupiter.api.Test;
/**
* B2BServiceTest class to test the B2BService
*/
-public class B2BServiceTest {
+public class BartenderTest {
private final CallsCount callsCount = new CallsCount();
@Test
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
- var timer = (Throttler) () -> {
- };
- var service = new B2BService(timer, callsCount);
+ var timer = (Throttler) () -> {};
+ var service = new Bartender(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());
assertEquals(2, counter, "Counter limit must be reached");
}
diff --git a/trampoline/README.md b/trampoline/README.md
index 6eb870227..eceaf3f1f 100644
--- a/trampoline/README.md
+++ b/trampoline/README.md
@@ -17,19 +17,19 @@ and to interleave the execution of functions without hard coding them together.
## Explanation
Recursion is a frequently adopted technique for solving algorithmic problems in a divide and conquer
-style. For example calculating fibonacci accumulating sum and factorials. In these kinds of problems
-recursion is more straightforward than their loop counterpart. Furthermore recursion may need less
-code and looks more concise. There is a saying that every recursion problem can be solved using
-a loop with the cost of writing code that is more difficult to understand.
+style. For example, calculating Fibonacci accumulating sum and factorials. In these kinds of
+problems, recursion is more straightforward than its loop counterpart. Furthermore, recursion may
+need less code and looks more concise. There is a saying that every recursion problem can be solved
+using a loop with the cost of writing code that is more difficult to understand.
-However recursion type solutions have one big caveat. For each recursive call it typically needs
+However, recursion-type solutions have one big caveat. For each recursive call, it typically needs
an intermediate value stored and there is a limited amount of stack memory available. Running out of
stack memory creates a stack overflow error and halts the program execution.
-Trampoline pattern is a trick that allows us define recursive algorithms in Java without blowing the
+Trampoline pattern is a trick that allows defining recursive algorithms in Java without blowing the
stack.
-Real world example
+Real-world example
> A recursive Fibonacci calculation without the stack overflow problem using the Trampoline pattern.
@@ -105,24 +105,26 @@ public interface Trampoline {
Using the `Trampoline` to get Fibonacci values.
```java
- public static Trampoline loop(int times, int prod) {
+public static void main(String[] args) {
+ LOGGER.info("Start calculating war casualties");
+ var result = loop(10, 1).result();
+ LOGGER.info("The number of orcs perished in the war: {}", result);
+}
+
+public static Trampoline loop(int times, int prod) {
if (times == 0) {
- return Trampoline.done(prod);
+ return Trampoline.done(prod);
} else {
- return Trampoline.more(() -> loop(times - 1, prod * times));
+ return Trampoline.more(() -> loop(times - 1, prod * times));
}
- }
-
- log.info("start pattern");
- var result = loop(10, 1).result();
- log.info("result {}", result);
+}
```
Program output:
```
-start pattern
-result 3628800
+19:22:24.462 [main] INFO com.iluwatar.trampoline.TrampolineApp - Start calculating war casualties
+19:22:24.472 [main] INFO com.iluwatar.trampoline.TrampolineApp - The number of orcs perished in the war: 3628800
```
## Class diagram
@@ -133,8 +135,8 @@ result 3628800
Use the Trampoline pattern when
-* For implementing tail recursive function. This pattern allows to switch on a stackless operation.
-* For interleaving the execution of two or more functions on the same thread.
+* For implementing tail-recursive functions. This pattern allows to switch on a stackless operation.
+* For interleaving execution of two or more functions on the same thread.
## Known uses
diff --git a/trampoline/src/main/java/com/iluwatar/trampoline/Trampoline.java b/trampoline/src/main/java/com/iluwatar/trampoline/Trampoline.java
index 1d2ea91d3..d60ef3602 100644
--- a/trampoline/src/main/java/com/iluwatar/trampoline/Trampoline.java
+++ b/trampoline/src/main/java/com/iluwatar/trampoline/Trampoline.java
@@ -107,6 +107,4 @@ public interface Trampoline {
}
};
}
-
-
}
diff --git a/trampoline/src/main/java/com/iluwatar/trampoline/TrampolineApp.java b/trampoline/src/main/java/com/iluwatar/trampoline/TrampolineApp.java
index 32a3f1850..bfeed4d69 100644
--- a/trampoline/src/main/java/com/iluwatar/trampoline/TrampolineApp.java
+++ b/trampoline/src/main/java/com/iluwatar/trampoline/TrampolineApp.java
@@ -39,9 +39,9 @@ public class TrampolineApp {
* Main program for showing pattern. It does loop with factorial function.
*/
public static void main(String[] args) {
- LOGGER.info("start pattern");
+ LOGGER.info("Start calculating war casualties");
var result = loop(10, 1).result();
- LOGGER.info("result {}", result);
+ LOGGER.info("The number of orcs perished in the war: {}", result);
}
@@ -55,5 +55,4 @@ public class TrampolineApp {
return Trampoline.more(() -> loop(times - 1, prod * times));
}
}
-
}
diff --git a/unit-of-work/README.md b/unit-of-work/README.md
index df2d03df2..f60667810 100644
--- a/unit-of-work/README.md
+++ b/unit-of-work/README.md
@@ -12,20 +12,20 @@ tags:
## 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.
## Explanation
-Real world example
+Real-world example
-> We have a database containing student information. Administrators all over the country are
-> constantly updating this information and it causes high load on the database server. To make the
+> Arms dealer has a database containing weapon information. Merchants all over the town are
+> 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.
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.
[MartinFowler.com](https://martinfowler.com/eaaCatalog/unitOfWork.html) says
@@ -35,37 +35,20 @@ In plain words
**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
-public class Student {
- private final Integer id;
- private final String name;
- private final String address;
-
- 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;
- }
+@Getter
+@RequiredArgsConstructor
+public class Weapon {
+ private final Integer id;
+ private final String name;
}
```
-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
-called it applies them in single batch.
+called it applies them in a single batch.
```java
public interface IUnitOfWork {
@@ -84,96 +67,117 @@ public interface IUnitOfWork {
}
@Slf4j
-public class StudentRepository implements IUnitOfWork {
+@RequiredArgsConstructor
+public class ArmsDealer implements IUnitOfWork {
- private final Map> context;
- private final StudentDatabase studentDatabase;
+ private final Map> context;
+ private final WeaponDatabase weaponDatabase;
- public StudentRepository(Map> context, StudentDatabase studentDatabase) {
- this.context = context;
- this.studentDatabase = studentDatabase;
- }
-
- @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();
+ @Override
+ public void registerNew(Weapon weapon) {
+ LOGGER.info("Registering {} for insert in context.", weapon.getName());
+ register(weapon, UnitActions.INSERT.getActionValue());
}
- if (context.containsKey(IUnitOfWork.MODIFY)) {
- commitModify();
- }
- if (context.containsKey(IUnitOfWork.DELETE)) {
- commitDelete();
- }
- LOGGER.info("Commit finished.");
- }
+ @Override
+ public void registerModified(Weapon weapon) {
+ LOGGER.info("Registering {} for modify in context.", weapon.getName());
+ register(weapon, UnitActions.MODIFY.getActionValue());
- 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() {
- var modifiedStudents = context.get(IUnitOfWork.MODIFY);
- for (var student : modifiedStudents) {
- LOGGER.info("Modifying {} to database.", student.getName());
- studentDatabase.modify(student);
+ @Override
+ public void registerDeleted(Weapon weapon) {
+ LOGGER.info("Registering {} for delete in context.", weapon.getName());
+ register(weapon, UnitActions.DELETE.getActionValue());
}
- }
- private void commitDelete() {
- var deletedStudents = context.get(IUnitOfWork.DELETE);
- for (var student : deletedStudents) {
- LOGGER.info("Deleting {} to database.", student.getName());
- studentDatabase.delete(student);
+ private void register(Weapon weapon, String operation) {
+ var weaponsToOperate = context.get(operation);
+ if (weaponsToOperate == null) {
+ weaponsToOperate = new ArrayList<>();
+ }
+ 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
- studentRepository.registerNew(ram);
- studentRepository.registerModified(shyam);
- studentRepository.registerDeleted(gopi);
- studentRepository.commit();
+// create some weapons
+var enchantedHammer = new Weapon(1, "enchanted hammer");
+var brokenGreatSword = new Weapon(2, "broken great sword");
+var silverTrident = new Weapon(3, "silver trident");
+
+// create repository
+var weaponRepository = new ArmsDealer(new HashMap>(), 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
@@ -186,7 +190,7 @@ Use the Unit Of Work pattern when
* 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 reduce number of database calls.
+* To reduce the number of database calls.
## Tutorials
diff --git a/unit-of-work/etc/unit-of-work.ucls b/unit-of-work/etc/unit-of-work.ucls
index 98181f805..0a80d680d 100644
--- a/unit-of-work/etc/unit-of-work.ucls
+++ b/unit-of-work/etc/unit-of-work.ucls
@@ -1,7 +1,7 @@
-
-
@@ -38,7 +38,7 @@
-
>();
- var studentDatabase = new StudentDatabase();
- var studentRepository = new StudentRepository(context, studentDatabase);
+ // create repository
+ var weaponRepository = new ArmsDealer(new HashMap>(),
+ new WeaponDatabase());
- studentRepository.registerNew(ram);
- studentRepository.registerModified(shyam);
- studentRepository.registerDeleted(gopi);
- studentRepository.commit();
+ // perform operations on the weapons
+ weaponRepository.registerNew(enchantedHammer);
+ weaponRepository.registerModified(silverTrident);
+ weaponRepository.registerDeleted(brokenGreatSword);
+ weaponRepository.commit();
}
}
diff --git a/unit-of-work/src/main/java/com/iluwatar/unitofwork/StudentRepository.java b/unit-of-work/src/main/java/com/iluwatar/unitofwork/ArmsDealer.java
similarity index 55%
rename from unit-of-work/src/main/java/com/iluwatar/unitofwork/StudentRepository.java
rename to unit-of-work/src/main/java/com/iluwatar/unitofwork/ArmsDealer.java
index 991aef12a..c222e47d4 100644
--- a/unit-of-work/src/main/java/com/iluwatar/unitofwork/StudentRepository.java
+++ b/unit-of-work/src/main/java/com/iluwatar/unitofwork/ArmsDealer.java
@@ -30,41 +30,41 @@ import lombok.RequiredArgsConstructor;
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
@RequiredArgsConstructor
-public class StudentRepository implements IUnitOfWork {
+public class ArmsDealer implements IUnitOfWork {
- private final Map> context;
- private final StudentDatabase studentDatabase;
+ private final Map> context;
+ private final WeaponDatabase weaponDatabase;
@Override
- public void registerNew(Student student) {
- LOGGER.info("Registering {} for insert in context.", student.getName());
- register(student, UnitActions.INSERT.getActionValue());
+ public void registerNew(Weapon weapon) {
+ LOGGER.info("Registering {} for insert in context.", weapon.getName());
+ register(weapon, UnitActions.INSERT.getActionValue());
}
@Override
- public void registerModified(Student student) {
- LOGGER.info("Registering {} for modify in context.", student.getName());
- register(student, UnitActions.MODIFY.getActionValue());
+ public void registerModified(Weapon weapon) {
+ LOGGER.info("Registering {} for modify in context.", weapon.getName());
+ register(weapon, UnitActions.MODIFY.getActionValue());
}
@Override
- public void registerDeleted(Student student) {
- LOGGER.info("Registering {} for delete in context.", student.getName());
- register(student, UnitActions.DELETE.getActionValue());
+ public void registerDeleted(Weapon weapon) {
+ LOGGER.info("Registering {} for delete in context.", weapon.getName());
+ register(weapon, UnitActions.DELETE.getActionValue());
}
- private void register(Student student, String operation) {
- var studentsToOperate = context.get(operation);
- if (studentsToOperate == null) {
- studentsToOperate = new ArrayList<>();
+ private void register(Weapon weapon, String operation) {
+ var weaponsToOperate = context.get(operation);
+ if (weaponsToOperate == null) {
+ weaponsToOperate = new ArrayList<>();
}
- studentsToOperate.add(student);
- context.put(operation, studentsToOperate);
+ weaponsToOperate.add(weapon);
+ context.put(operation, weaponsToOperate);
}
/**
@@ -90,26 +90,26 @@ public class StudentRepository implements IUnitOfWork {
}
private void commitInsert() {
- var studentsToBeInserted = context.get(UnitActions.INSERT.getActionValue());
- for (var student : studentsToBeInserted) {
- LOGGER.info("Saving {} to database.", student.getName());
- studentDatabase.insert(student);
+ 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 modifiedStudents = context.get(UnitActions.MODIFY.getActionValue());
- for (var student : modifiedStudents) {
- LOGGER.info("Modifying {} to database.", student.getName());
- studentDatabase.modify(student);
+ 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 deletedStudents = context.get(UnitActions.DELETE.getActionValue());
- for (var student : deletedStudents) {
- LOGGER.info("Deleting {} to database.", student.getName());
- studentDatabase.delete(student);
+ var deletedWeapons = context.get(UnitActions.DELETE.getActionValue());
+ for (var weapon : deletedWeapons) {
+ LOGGER.info("Scrapping {}.", weapon.getName());
+ weaponDatabase.delete(weapon);
}
}
}
diff --git a/unit-of-work/src/main/java/com/iluwatar/unitofwork/Student.java b/unit-of-work/src/main/java/com/iluwatar/unitofwork/Weapon.java
similarity index 93%
rename from unit-of-work/src/main/java/com/iluwatar/unitofwork/Student.java
rename to unit-of-work/src/main/java/com/iluwatar/unitofwork/Weapon.java
index b3de369b4..bf4a3e071 100644
--- a/unit-of-work/src/main/java/com/iluwatar/unitofwork/Student.java
+++ b/unit-of-work/src/main/java/com/iluwatar/unitofwork/Weapon.java
@@ -27,14 +27,12 @@ import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
- * {@link Student} is an entity.
+ * {@link Weapon} is an entity.
*/
@Getter
@RequiredArgsConstructor
-public class Student {
+public class Weapon {
private final Integer id;
private final String name;
- private final String address;
-
}
diff --git a/unit-of-work/src/main/java/com/iluwatar/unitofwork/StudentDatabase.java b/unit-of-work/src/main/java/com/iluwatar/unitofwork/WeaponDatabase.java
similarity index 87%
rename from unit-of-work/src/main/java/com/iluwatar/unitofwork/StudentDatabase.java
rename to unit-of-work/src/main/java/com/iluwatar/unitofwork/WeaponDatabase.java
index c64c47e30..c9a8cee4b 100644
--- a/unit-of-work/src/main/java/com/iluwatar/unitofwork/StudentDatabase.java
+++ b/unit-of-work/src/main/java/com/iluwatar/unitofwork/WeaponDatabase.java
@@ -24,19 +24,19 @@
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
}
- public void modify(Student student) {
+ public void modify(Weapon weapon) {
//Some modify logic to DB
}
- public void delete(Student student) {
+ public void delete(Weapon weapon) {
//Some delete logic to DB
}
}
diff --git a/unit-of-work/src/test/java/com/iluwatar/unitofwork/StudentRepositoryTest.java b/unit-of-work/src/test/java/com/iluwatar/unitofwork/ArmsDealerTest.java
similarity index 51%
rename from unit-of-work/src/test/java/com/iluwatar/unitofwork/StudentRepositoryTest.java
rename to unit-of-work/src/test/java/com/iluwatar/unitofwork/ArmsDealerTest.java
index 760d4a21b..65b65a623 100644
--- a/unit-of-work/src/test/java/com/iluwatar/unitofwork/StudentRepositoryTest.java
+++ b/unit-of-work/src/test/java/com/iluwatar/unitofwork/ArmsDealerTest.java
@@ -36,102 +36,102 @@ import java.util.Map;
import org.junit.jupiter.api.Test;
/**
- * tests {@link StudentRepository}
+ * tests {@link ArmsDealer}
*/
-class StudentRepositoryTest {
- private final Student student1 = new Student(1, "Ram", "street 9, cupertino");
- private final Student student2 = new Student(1, "Sham", "Z bridge, pune");
+class ArmsDealerTest {
+ private final Weapon weapon1 = new Weapon(1, "battle ram");
+ private final Weapon weapon2 = new Weapon(1, "wooden lance");
- private final Map> context = new HashMap<>();
- private final StudentDatabase studentDatabase = mock(StudentDatabase.class);
- private final StudentRepository studentRepository = new StudentRepository(context, studentDatabase);;
+ private final Map> context = new HashMap<>();
+ private final WeaponDatabase weaponDatabase = mock(WeaponDatabase.class);
+ private final ArmsDealer armsDealer = new ArmsDealer(context, weaponDatabase);;
@Test
void shouldSaveNewStudentWithoutWritingToDb() {
- studentRepository.registerNew(student1);
- studentRepository.registerNew(student2);
+ armsDealer.registerNew(weapon1);
+ armsDealer.registerNew(weapon2);
assertEquals(2, context.get(UnitActions.INSERT.getActionValue()).size());
- verifyNoMoreInteractions(studentDatabase);
+ verifyNoMoreInteractions(weaponDatabase);
}
@Test
void shouldSaveDeletedStudentWithoutWritingToDb() {
- studentRepository.registerDeleted(student1);
- studentRepository.registerDeleted(student2);
+ armsDealer.registerDeleted(weapon1);
+ armsDealer.registerDeleted(weapon2);
assertEquals(2, context.get(UnitActions.DELETE.getActionValue()).size());
- verifyNoMoreInteractions(studentDatabase);
+ verifyNoMoreInteractions(weaponDatabase);
}
@Test
void shouldSaveModifiedStudentWithoutWritingToDb() {
- studentRepository.registerModified(student1);
- studentRepository.registerModified(student2);
+ armsDealer.registerModified(weapon1);
+ armsDealer.registerModified(weapon2);
assertEquals(2, context.get(UnitActions.MODIFY.getActionValue()).size());
- verifyNoMoreInteractions(studentDatabase);
+ verifyNoMoreInteractions(weaponDatabase);
}
@Test
void shouldSaveAllLocalChangesToDb() {
- context.put(UnitActions.INSERT.getActionValue(), List.of(student1));
- context.put(UnitActions.MODIFY.getActionValue(), List.of(student1));
- context.put(UnitActions.DELETE.getActionValue(), List.of(student1));
+ context.put(UnitActions.INSERT.getActionValue(), List.of(weapon1));
+ context.put(UnitActions.MODIFY.getActionValue(), List.of(weapon1));
+ context.put(UnitActions.DELETE.getActionValue(), List.of(weapon1));
- studentRepository.commit();
+ armsDealer.commit();
- verify(studentDatabase, times(1)).insert(student1);
- verify(studentDatabase, times(1)).modify(student1);
- verify(studentDatabase, times(1)).delete(student1);
+ verify(weaponDatabase, times(1)).insert(weapon1);
+ verify(weaponDatabase, times(1)).modify(weapon1);
+ verify(weaponDatabase, times(1)).delete(weapon1);
}
@Test
void shouldNotWriteToDbIfContextIsNull() {
- var studentRepository = new StudentRepository(null, studentDatabase);
+ var weaponRepository = new ArmsDealer(null, weaponDatabase);
- studentRepository.commit();
+ weaponRepository.commit();
- verifyNoMoreInteractions(studentDatabase);
+ verifyNoMoreInteractions(weaponDatabase);
}
@Test
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
void shouldNotInsertToDbIfNoRegisteredStudentsToBeCommitted() {
- context.put(UnitActions.MODIFY.getActionValue(), List.of(student1));
- context.put(UnitActions.DELETE.getActionValue(), List.of(student1));
+ context.put(UnitActions.MODIFY.getActionValue(), List.of(weapon1));
+ context.put(UnitActions.DELETE.getActionValue(), List.of(weapon1));
- studentRepository.commit();
+ armsDealer.commit();
- verify(studentDatabase, never()).insert(student1);
+ verify(weaponDatabase, never()).insert(weapon1);
}
@Test
void shouldNotModifyToDbIfNotRegisteredStudentsToBeCommitted() {
- context.put(UnitActions.INSERT.getActionValue(), List.of(student1));
- context.put(UnitActions.DELETE.getActionValue(), List.of(student1));
+ context.put(UnitActions.INSERT.getActionValue(), List.of(weapon1));
+ context.put(UnitActions.DELETE.getActionValue(), List.of(weapon1));
- studentRepository.commit();
+ armsDealer.commit();
- verify(studentDatabase, never()).modify(student1);
+ verify(weaponDatabase, never()).modify(weapon1);
}
@Test
void shouldNotDeleteFromDbIfNotRegisteredStudentsToBeCommitted() {
- context.put(UnitActions.INSERT.getActionValue(), List.of(student1));
- context.put(UnitActions.MODIFY.getActionValue(), List.of(student1));
+ context.put(UnitActions.INSERT.getActionValue(), List.of(weapon1));
+ context.put(UnitActions.MODIFY.getActionValue(), List.of(weapon1));
- studentRepository.commit();
+ armsDealer.commit();
- verify(studentDatabase, never()).delete(student1);
+ verify(weaponDatabase, never()).delete(weapon1);
}
}
diff --git a/value-object/README.md b/value-object/README.md
index 9b85d82b1..c98674381 100644
--- a/value-object/README.md
+++ b/value-object/README.md
@@ -10,19 +10,80 @@ tags:
---
## Intent
+
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.
+## 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
+

## Applicability
+
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.time.LocalDate](https://docs.oracle.com/javase/8/docs/api/java/time/LocalDate.html)
@@ -31,6 +92,7 @@ Use the Value Object when
## Credits
* [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)
* [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)
diff --git a/value-object/src/main/java/com/iluwatar/value/object/App.java b/value-object/src/main/java/com/iluwatar/value/object/App.java
index 49375e94c..cc952fd47 100644
--- a/value-object/src/main/java/com/iluwatar/value/object/App.java
+++ b/value-object/src/main/java/com/iluwatar/value/object/App.java
@@ -43,7 +43,7 @@ import lombok.extern.slf4j.Slf4j;
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) {
var statA = HeroStat.valueOf(10, 5, 0);
@@ -51,6 +51,8 @@ public class App {
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));
diff --git a/value-object/src/main/java/com/iluwatar/value/object/HeroStat.java b/value-object/src/main/java/com/iluwatar/value/object/HeroStat.java
index 4d060ee0f..4620b4e4a 100644
--- a/value-object/src/main/java/com/iluwatar/value/object/HeroStat.java
+++ b/value-object/src/main/java/com/iluwatar/value/object/HeroStat.java
@@ -23,10 +23,7 @@
package com.iluwatar.value.object;
-import lombok.EqualsAndHashCode;
-import lombok.Getter;
-import lombok.RequiredArgsConstructor;
-import lombok.ToString;
+import lombok.Value;
/**
* 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
*
*/
-@Getter
-@ToString
-@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.
+@Value(staticConstructor = "valueOf")
+class HeroStat {
+ int strength;
+ int intelligence;
+ int luck;
}