Compare commits

..

1 Commits

Author SHA1 Message Date
2823166c91 refactor unit of work 2022-01-01 21:44:58 +02:00
9 changed files with 215 additions and 313 deletions

View File

@ -10,115 +10,19 @@ 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
* The factory class can't anticipate the types of objects it must create * a class can't anticipate the class of objects it must create
* A new instance of a custom builder is needed instead of a global one * you just want a new instance of a custom builder instead of the global one
* The types of objects that the factory can build need to be defined outside the class * you explicitly want to define types of objects, that factory can build
* The builder and creator interfaces need to be separated * you want a separated builder and creator interface
## 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,16 +23,14 @@
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 that defines a factory of immutable content with separated * Factory-kit is a creational pattern which 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
@ -54,11 +52,7 @@ 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 list = new ArrayList<Weapon>(); var axe = factory.create(WeaponType.AXE);
list.add(factory.create(WeaponType.AXE)); LOGGER.info(axe.toString());
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

@ -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
@RequiredArgsConstructor
public class Weapon {
private final Integer id; private final Integer id;
private final String name; 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;
}
} }
``` ```
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 @Override
public void registerNew(Student student) { public void registerModified(Weapon weapon) {
LOGGER.info("Registering {} for insert in context.", student.getName()); LOGGER.info("Registering {} for modify in context.", weapon.getName());
register(student, IUnitOfWork.INSERT); register(weapon, UnitActions.MODIFY.getActionValue());
}
@Override
public void registerModified(Student student) {
LOGGER.info("Registering {} for modify in context.", student.getName());
register(student, IUnitOfWork.MODIFY);
} }
@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, IUnitOfWork.DELETE); 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);
} }
/**
* All UnitOfWork operations are batched and executed together on commit only.
*/
@Override @Override
public void commit() { public void commit() {
if (context == null || context.size() == 0) { if (context == null || context.size() == 0) {
return; return;
} }
LOGGER.info("Commit started"); LOGGER.info("Commit started");
if (context.containsKey(IUnitOfWork.INSERT)) { if (context.containsKey(UnitActions.INSERT.getActionValue())) {
commitInsert(); commitInsert();
} }
if (context.containsKey(IUnitOfWork.MODIFY)) { if (context.containsKey(UnitActions.MODIFY.getActionValue())) {
commitModify(); commitModify();
} }
if (context.containsKey(IUnitOfWork.DELETE)) { if (context.containsKey(UnitActions.DELETE.getActionValue())) {
commitDelete(); commitDelete();
} }
LOGGER.info("Commit finished."); LOGGER.info("Commit finished.");
} }
private void commitInsert() { private void commitInsert() {
var studentsToBeInserted = context.get(IUnitOfWork.INSERT); 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(IUnitOfWork.MODIFY); 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(IUnitOfWork.DELETE); 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);
} }
} }
} }
``` ```
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);
} }
} }