feature: resolve #1282 for Lockable Object pattern. (#1702)

* Added Lockable-Object pattern. Closes #1282.

* Refactor method name.

* Refactor sonar lint bugs.

* Added tests and enum Constants.

* Increase coverage.

* Changed @Data to Getters and Setters.

* Iluwatar's comment on pull request #1702.

* Fixed codes mells.

* Incremented wait time to 3 seconds.

* Reduced wait time to 2 seconds.

* Cleaned Code Smells.

* Incremented wait time, removed cool down.

* Refactored README.md file.

Co-authored-by: Subhrodip Mohanta <subhrodipmohanta@gmail.com>
This commit is contained in:
Noam Greenshtain 2021-05-14 18:56:41 +03:00 committed by GitHub
parent ea3c9d955e
commit 122e6edb38
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1118 additions and 0 deletions

285
lockable-object/README.md Normal file
View File

@ -0,0 +1,285 @@
---
layout: pattern
title: Lockable Object
folder: lockable-object
permalink: /patterns/lockable-object/
categories: Concurrency
tags:
- Performance
---
## Intent
The lockable object design pattern ensures that there is only one user using the target object. Compared to the built-in synchronization mechanisms such as using the `synchronized` keyword, this pattern can lock objects for an undetermined time and is not tied to the duration of the request.
## Explanation
Real-world example
>The Sword Of Aragorn is a legendary object that only one creature can possess at the time.
>Every creature in the middle earth wants to possess is, so as long as it's not locked, every creature will fight for it.
Under the hood
>In this particular module, the SwordOfAragorn.java is a class that implements the Lockable interface.
It reaches the goal of the Lockable-Object pattern by implementing unlock() and unlock() methods using
thread-safety logic. The thread-safety logic is implemented with the built-in monitor mechanism of Java.
The SwordOfAaragorn.java has an Object property called "synchronizer". In every crucial concurrency code block,
it's synchronizing the block by using the synchronizer.
**Programmatic Example**
```java
/** This interface describes the methods to be supported by a lockable object. */
public interface Lockable {
/**
* Checks if the object is locked.
*
* @return true if it is locked.
*/
boolean isLocked();
/**
* locks the object with the creature as the locker.
*
* @param creature as the locker.
* @return true if the object was locked successfully.
*/
boolean lock(Creature creature);
/**
* Unlocks the object.
*
* @param creature as the locker.
*/
void unlock(Creature creature);
/**
* Gets the locker.
*
* @return the Creature that holds the object. Returns null if no one is locking.
*/
Creature getLocker();
/**
* Returns the name of the object.
*
* @return the name of the object.
*/
String getName();
}
```
We have defined that according to our context, the object must implement the Lockable interface.
For example, the SwordOfAragorn class:
```java
public class SwordOfAragorn implements Lockable {
private Creature locker;
private final Object synchronizer;
private static final String NAME = "The Sword of Aragorn";
public SwordOfAragorn() {
this.locker = null;
this.synchronizer = new Object();
}
@Override
public boolean isLocked() {
return this.locker != null;
}
@Override
public boolean lock(@NonNull Creature creature) {
synchronized (synchronizer) {
LOGGER.info("{} is now trying to acquire {}!", creature.getName(), this.getName());
if (!isLocked()) {
locker = creature;
return true;
} else {
if (!locker.getName().equals(creature.getName())) {
return false;
}
}
}
return false;
}
@Override
public void unlock(@NonNull Creature creature) {
synchronized (synchronizer) {
if (locker != null && locker.getName().equals(creature.getName())) {
locker = null;
LOGGER.info("{} is now free!", this.getName());
}
if (locker != null) {
throw new LockingException("You cannot unlock an object you are not the owner of.");
}
}
}
@Override
public Creature getLocker() {
return this.locker;
}
@Override
public String getName() {
return NAME;
}
}
```
According to our context, there are creatures that are looking for the sword, so must define the parent class:
```java
public abstract class Creature {
private String name;
private CreatureType type;
private int health;
private int damage;
Set<Lockable> instruments;
protected Creature(@NonNull String name) {
this.name = name;
this.instruments = new HashSet<>();
}
/**
* Reaches for the Lockable and tried to hold it.
*
* @param lockable as the Lockable to lock.
* @return true of Lockable was locked by this creature.
*/
public boolean acquire(@NonNull Lockable lockable) {
if (lockable.lock(this)) {
instruments.add(lockable);
return true;
}
return false;
}
/** Terminates the Creature and unlocks all of the Lockable that it posses. */
public synchronized void kill() {
LOGGER.info("{} {} has been slayed!", type, name);
for (Lockable lockable : instruments) {
lockable.unlock(this);
}
this.instruments.clear();
}
/**
* Attacks a foe.
*
* @param creature as the foe to be attacked.
*/
public synchronized void attack(@NonNull Creature creature) {
creature.hit(getDamage());
}
/**
* When a creature gets hit it removed the amount of damage from the creature's life.
*
* @param damage as the damage that was taken.
*/
public synchronized void hit(int damage) {
if (damage < 0) {
throw new IllegalArgumentException("Damage cannot be a negative number");
}
if (isAlive()) {
setHealth(getHealth() - damage);
if (!isAlive()) {
kill();
}
}
}
/**
* Checks if the creature is still alive.
*
* @return true of creature is alive.
*/
public synchronized boolean isAlive() {
return getHealth() > 0;
}
}
```
As mentioned before, we have classes that extend the Creature class, such as Elf, Orc, and Human.
Finally, the following program will simulate a battle for the sword:
```java
public class App implements Runnable {
private static final int WAIT_TIME = 3;
private static final int WORKERS = 2;
private static final int MULTIPLICATION_FACTOR = 3;
/**
* main method.
*
* @param args as arguments for the main method.
*/
public static void main(String[] args) {
var app = new App();
app.run();
}
@Override
public void run() {
// The target object for this example.
var sword = new SwordOfAragorn();
// Creation of creatures.
List<Creature> creatures = new ArrayList<>();
for (var i = 0; i < WORKERS; i++) {
creatures.add(new Elf(String.format("Elf %s", i)));
creatures.add(new Orc(String.format("Orc %s", i)));
creatures.add(new Human(String.format("Human %s", i)));
}
int totalFiends = WORKERS * MULTIPLICATION_FACTOR;
ExecutorService service = Executors.newFixedThreadPool(totalFiends);
// Attach every creature and the sword is a Fiend to fight for the sword.
for (var i = 0; i < totalFiends; i = i + MULTIPLICATION_FACTOR) {
service.submit(new Feind(creatures.get(i), sword));
service.submit(new Feind(creatures.get(i + 1), sword));
service.submit(new Feind(creatures.get(i + 2), sword));
}
// Wait for program to terminate.
try {
if (!service.awaitTermination(WAIT_TIME, TimeUnit.SECONDS)) {
LOGGER.info("The master of the sword is now {}.", sword.getLocker().getName());
}
} catch (InterruptedException e) {
LOGGER.error(e.getMessage());
Thread.currentThread().interrupt();
} finally {
service.shutdown();
}
}
}
```
## Applicability
The Lockable Object pattern is ideal for non distributed applications, that needs to be thread-safe
and keeping their domain models in memory(in contrast to persisted models such as databases).
## Class diagram
![alt text](./etc/lockable-object.urm.png "Lockable Object class diagram")
## Credits
* [Lockable Object - Chapter 10.3, J2EE Design Patterns, O'Reilly](http://ommolketab.ir/aaf-lib/axkwht7wxrhvgs2aqkxse8hihyu9zv.pdf)

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

View File

@ -0,0 +1,94 @@
@startuml
package com.iluwatar.lockableobject.domain {
abstract class Creature {
- LOGGER : Logger {static}
- damage : int
- health : int
~ instruments : Set<Lockable>
- name : String
- type : CreatureType
+ Creature(name : String)
+ acquire(lockable : Lockable) : boolean
+ attack(creature : Creature)
# canEqual(other : Object) : boolean
+ equals(o : Object) : boolean
+ getDamage() : int
+ getHealth() : int
+ getInstruments() : Set<Lockable>
+ getName() : String
+ getType() : CreatureType
+ hashCode() : int
+ hit(damage : int)
+ isAlive() : boolean
+ kill()
+ setDamage(damage : int)
+ setHealth(health : int)
+ setInstruments(instruments : Set<Lockable>)
+ setName(name : String)
+ setType(type : CreatureType)
+ toString() : String
}
enum CreatureType {
+ ELF {static}
+ HUMAN {static}
+ ORC {static}
+ valueOf(name : String) : CreatureType {static}
+ values() : CreatureType[] {static}
}
class Elf {
+ Elf(name : String)
}
class Feind {
- LOGGER : Logger {static}
- feind : Creature
- target : Lockable
+ Feind(feind : Creature, target : Lockable)
- fightForTheSword(reacher : Creature, holder : Creature, sword : Lockable)
+ run()
}
class Human {
+ Human(name : String)
}
class Orc {
+ Orc(name : String)
}
}
package com.iluwatar.lockableobject {
class App {
- LOGGER : Logger {static}
- MULTIPLICATION_FACTOR : int {static}
- WAIT_TIME : int {static}
- WORKERS : int {static}
+ App()
+ main(args : String[]) {static}
}
interface Lockable {
+ getLocker() : Creature {abstract}
+ getName() : String {abstract}
+ isLocked() : boolean {abstract}
+ lock(Creature) : boolean {abstract}
+ unlock(Creature) {abstract}
}
class SwordOfAragorn {
- LOGGER : Logger {static}
- NAME : String {static}
- locker : Creature
- synchronizer : Object
+ SwordOfAragorn()
+ getLocker() : Creature
+ getName() : String
+ isLocked() : boolean
+ lock(creature : Creature) : boolean
+ unlock(creature : Creature)
}
}
Creature --> "-type" CreatureType
Creature --> "-instruments" Lockable
Feind --> "-feind" Creature
Feind --> "-target" Lockable
SwordOfAragorn --> "-locker" Creature
SwordOfAragorn ..|> Lockable
Elf --|> Creature
Human --|> Creature
Orc --|> Creature
@enduml

60
lockable-object/pom.xml Normal file
View File

@ -0,0 +1,60 @@
<?xml version="1.0"?>
<!--
The MIT License
Copyright © 2014-2021 Ilkka Seppälä
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->
<project
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.iluwatar</groupId>
<artifactId>java-design-patterns</artifactId>
<version>1.25.0-SNAPSHOT</version>
</parent>
<artifactId>lockable-object</artifactId>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Maven assembly plugin is invoked with default setting which we have
in parent pom and specifying the class having main method -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<configuration>
<archive>
<manifest>
<mainClass>com.iluwatar.lockableobject.App</mainClass>
</manifest>
</archive>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,77 @@
package com.iluwatar.lockableobject;
import com.iluwatar.lockableobject.domain.Creature;
import com.iluwatar.lockableobject.domain.Elf;
import com.iluwatar.lockableobject.domain.Feind;
import com.iluwatar.lockableobject.domain.Human;
import com.iluwatar.lockableobject.domain.Orc;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
/**
* The Lockable Object pattern is a concurrency pattern. Instead of using the "synchronized" word
* upon the methods to be synchronized, the object which implements the Lockable interface handles
* the request.
*
* <p>In this example, we create a new Lockable object with the SwordOfAragorn implementation of it.
* Afterwards we create 6 Creatures with the Elf, Orc and Human implementations and assign them each
* to a Fiend object and the Sword is the target object. Because there is only one Sword and it uses
* the Lockable Object pattern, only one creature can hold the sword at a given time. When the sword
* is locked, any other alive Fiends will try to lock, which will result in a race to lock the
* sword.
*
* @author Noam Greenshtain
*/
@Slf4j
public class App implements Runnable {
private static final int WAIT_TIME = 3;
private static final int WORKERS = 2;
private static final int MULTIPLICATION_FACTOR = 3;
/**
* main method.
*
* @param args as arguments for the main method.
*/
public static void main(String[] args) {
var app = new App();
app.run();
}
@Override
public void run() {
// The target object for this example.
var sword = new SwordOfAragorn();
// Creation of creatures.
List<Creature> creatures = new ArrayList<>();
for (var i = 0; i < WORKERS; i++) {
creatures.add(new Elf(String.format("Elf %s", i)));
creatures.add(new Orc(String.format("Orc %s", i)));
creatures.add(new Human(String.format("Human %s", i)));
}
int totalFiends = WORKERS * MULTIPLICATION_FACTOR;
ExecutorService service = Executors.newFixedThreadPool(totalFiends);
// Attach every creature and the sword is a Fiend to fight for the sword.
for (var i = 0; i < totalFiends; i = i + MULTIPLICATION_FACTOR) {
service.submit(new Feind(creatures.get(i), sword));
service.submit(new Feind(creatures.get(i + 1), sword));
service.submit(new Feind(creatures.get(i + 2), sword));
}
// Wait for program to terminate.
try {
if (!service.awaitTermination(WAIT_TIME, TimeUnit.SECONDS)) {
LOGGER.info("The master of the sword is now {}.", sword.getLocker().getName());
}
} catch (InterruptedException e) {
LOGGER.error(e.getMessage());
Thread.currentThread().interrupt();
} finally {
service.shutdown();
}
}
}

View File

@ -0,0 +1,43 @@
package com.iluwatar.lockableobject;
import com.iluwatar.lockableobject.domain.Creature;
/** This interface describes the methods to be supported by a lockable-object. */
public interface Lockable {
/**
* Checks if the object is locked.
*
* @return true if it is locked.
*/
boolean isLocked();
/**
* locks the object with the creature as the locker.
*
* @param creature as the locker.
* @return true if the object was locked successfully.
*/
boolean lock(Creature creature);
/**
* Unlocks the object.
*
* @param creature as the locker.
*/
void unlock(Creature creature);
/**
* Gets the locker.
*
* @return the Creature that holds the object. Returns null if no one is locking.
*/
Creature getLocker();
/**
* Returns the name of the object.
*
* @return the name of the object.
*/
String getName();
}

View File

@ -0,0 +1,14 @@
package com.iluwatar.lockableobject;
/**
* An exception regarding the locking process of a Lockable object.
*/
public class LockingException extends RuntimeException {
private static final long serialVersionUID = 8556381044865867037L;
public LockingException(String message) {
super(message);
}
}

View File

@ -0,0 +1,66 @@
package com.iluwatar.lockableobject;
import com.iluwatar.lockableobject.domain.Creature;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
/**
* An implementation of a Lockable object. This is the the Sword of Aragorn and every creature wants
* to posses it!
*/
@Slf4j
public class SwordOfAragorn implements Lockable {
private Creature locker;
private final Object synchronizer;
private static final String NAME = "The Sword of Aragorn";
public SwordOfAragorn() {
this.locker = null;
this.synchronizer = new Object();
}
@Override
public boolean isLocked() {
return this.locker != null;
}
@Override
public boolean lock(@NonNull Creature creature) {
synchronized (synchronizer) {
LOGGER.info("{} is now trying to acquire {}!", creature.getName(), this.getName());
if (!isLocked()) {
locker = creature;
return true;
} else {
if (!locker.getName().equals(creature.getName())) {
return false;
}
}
}
return false;
}
@Override
public void unlock(@NonNull Creature creature) {
synchronized (synchronizer) {
if (locker != null && locker.getName().equals(creature.getName())) {
locker = null;
LOGGER.info("{} is now free!", this.getName());
}
if (locker != null) {
throw new LockingException("You cannot unlock an object you are not the owner of.");
}
}
}
@Override
public Creature getLocker() {
return this.locker;
}
@Override
public String getName() {
return NAME;
}
}

View File

@ -0,0 +1,89 @@
package com.iluwatar.lockableobject.domain;
import com.iluwatar.lockableobject.Lockable;
import java.util.HashSet;
import java.util.Set;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
/**
* An abstract class of a creature that wanders across the wasteland. It can attack, get hit and
* acquire a Lockable object.
*/
@Getter
@Setter
@Slf4j
public abstract class Creature {
private String name;
private CreatureType type;
private int health;
private int damage;
Set<Lockable> instruments;
protected Creature(@NonNull String name) {
this.name = name;
this.instruments = new HashSet<>();
}
/**
* Reaches for the Lockable and tried to hold it.
*
* @param lockable as the Lockable to lock.
* @return true of Lockable was locked by this creature.
*/
public boolean acquire(@NonNull Lockable lockable) {
if (lockable.lock(this)) {
instruments.add(lockable);
return true;
}
return false;
}
/** Terminates the Creature and unlocks all of the Lockable that it posses. */
public synchronized void kill() {
LOGGER.info("{} {} has been slayed!", type, name);
for (Lockable lockable : instruments) {
lockable.unlock(this);
}
this.instruments.clear();
}
/**
* Attacks a foe.
*
* @param creature as the foe to be attacked.
*/
public synchronized void attack(@NonNull Creature creature) {
creature.hit(getDamage());
}
/**
* When a creature gets hit it removed the amount of damage from the creature's life.
*
* @param damage as the damage that was taken.
*/
public synchronized void hit(int damage) {
if (damage < 0) {
throw new IllegalArgumentException("Damage cannot be a negative number");
}
if (isAlive()) {
setHealth(getHealth() - damage);
if (!isAlive()) {
kill();
}
}
}
/**
* Checks if the creature is still alive.
*
* @return true of creature is alive.
*/
public synchronized boolean isAlive() {
return getHealth() > 0;
}
}

View File

@ -0,0 +1,21 @@
package com.iluwatar.lockableobject.domain;
/** Attribute constants of each Creature implementation. */
public enum CreatureStats {
ELF_HEALTH(90),
ELF_DAMAGE(40),
ORC_HEALTH(70),
ORC_DAMAGE(50),
HUMAN_HEALTH(60),
HUMAN_DAMAGE(60);
int value;
private CreatureStats(int value) {
this.value = value;
}
public int getValue() {
return this.value;
}
}

View File

@ -0,0 +1,8 @@
package com.iluwatar.lockableobject.domain;
/** Constants of supported creatures. */
public enum CreatureType {
ORC,
HUMAN,
ELF
}

View File

@ -0,0 +1,17 @@
package com.iluwatar.lockableobject.domain;
/** An Elf implementation of a Creature. */
public class Elf extends Creature {
/**
* A constructor that initializes the attributes of an elf.
*
* @param name as the name of the creature.
*/
public Elf(String name) {
super(name);
setType(CreatureType.ELF);
setDamage(CreatureStats.ELF_DAMAGE.getValue());
setHealth(CreatureStats.ELF_HEALTH.getValue());
}
}

View File

@ -0,0 +1,71 @@
package com.iluwatar.lockableobject.domain;
import com.iluwatar.lockableobject.Lockable;
import java.security.SecureRandom;
import lombok.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** A Feind is a creature that all it wants is to posses a Lockable object. */
public class Feind implements Runnable {
private final Creature creature;
private final Lockable target;
private final SecureRandom random;
private static final Logger LOGGER = LoggerFactory.getLogger(Feind.class.getName());
/**
* public constructor.
*
* @param feind as the creature to lock to he lockable.
* @param target as the target object.
*/
public Feind(@NonNull Creature feind, @NonNull Lockable target) {
this.creature = feind;
this.target = target;
this.random = new SecureRandom();
}
@Override
public void run() {
if (!creature.acquire(target)) {
try {
fightForTheSword(creature, target.getLocker(), target);
} catch (InterruptedException e) {
LOGGER.error(e.getMessage());
Thread.currentThread().interrupt();
}
} else {
LOGGER.info("{} has acquired the sword!", target.getLocker().getName());
}
}
/**
* Keeps on fighting until the Lockable is possessed.
*
* @param reacher as the source creature.
* @param holder as the foe.
* @param sword as the Lockable to posses.
* @throws InterruptedException in case of interruption.
*/
private void fightForTheSword(Creature reacher, @NonNull Creature holder, Lockable sword)
throws InterruptedException {
LOGGER.info("A duel between {} and {} has been started!", reacher.getName(), holder.getName());
boolean randBool;
while (this.target.isLocked() && reacher.isAlive() && holder.isAlive()) {
randBool = random.nextBoolean();
if (randBool) {
reacher.attack(holder);
} else {
holder.attack(reacher);
}
}
if (reacher.isAlive()) {
if (!reacher.acquire(sword)) {
fightForTheSword(reacher, sword.getLocker(), sword);
} else {
LOGGER.info("{} has acquired the sword!", reacher.getName());
}
}
}
}

View File

@ -0,0 +1,17 @@
package com.iluwatar.lockableobject.domain;
/** A human implementation of a Creature. */
public class Human extends Creature {
/**
* A constructor that initializes the attributes of an human.
*
* @param name as the name of the creature.
*/
public Human(String name) {
super(name);
setType(CreatureType.HUMAN);
setDamage(CreatureStats.HUMAN_DAMAGE.getValue());
setHealth(CreatureStats.HUMAN_HEALTH.getValue());
}
}

View File

@ -0,0 +1,16 @@
package com.iluwatar.lockableobject.domain;
/** A Orc implementation of a Creature. */
public class Orc extends Creature {
/**
* A constructor that initializes the attributes of an orc.
*
* @param name as the name of the creature.
*/
public Orc(String name) {
super(name);
setType(CreatureType.ORC);
setDamage(CreatureStats.ORC_DAMAGE.getValue());
setHealth(CreatureStats.ORC_HEALTH.getValue());
}
}

View File

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

View File

@ -0,0 +1,79 @@
package com.iluwatar.lockableobject;
import com.iluwatar.lockableobject.domain.Creature;
import com.iluwatar.lockableobject.domain.CreatureStats;
import com.iluwatar.lockableobject.domain.CreatureType;
import com.iluwatar.lockableobject.domain.Elf;
import com.iluwatar.lockableobject.domain.Orc;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class CreatureTest {
private Creature orc;
private Creature elf;
private Lockable sword;
@BeforeEach
void init() {
elf = new Elf("Elf test");
orc = new Orc("Orc test");
sword = new SwordOfAragorn();
}
@Test
void baseTest() {
Assertions.assertEquals("Elf test", elf.getName());
Assertions.assertEquals(CreatureType.ELF, elf.getType());
Assertions.assertThrows(NullPointerException.class, () -> new Elf(null));
Assertions.assertThrows(NullPointerException.class, () -> elf.acquire(null));
Assertions.assertThrows(NullPointerException.class, () -> elf.attack(null));
Assertions.assertThrows(IllegalArgumentException.class, () -> elf.hit(-10));
}
@Test
void hitTest() {
elf.hit(CreatureStats.ELF_HEALTH.getValue() / 2);
Assertions.assertEquals(CreatureStats.ELF_HEALTH.getValue() / 2, elf.getHealth());
elf.hit(CreatureStats.ELF_HEALTH.getValue() / 2);
Assertions.assertFalse(elf.isAlive());
Assertions.assertEquals(0, orc.getInstruments().size());
Assertions.assertTrue(orc.acquire(sword));
Assertions.assertEquals(1, orc.getInstruments().size());
orc.kill();
Assertions.assertEquals(0, orc.getInstruments().size());
}
@Test
void testFight() throws InterruptedException {
killCreature(elf, orc);
Assertions.assertTrue(elf.isAlive());
Assertions.assertFalse(orc.isAlive());
Assertions.assertTrue(elf.getHealth() > 0);
Assertions.assertTrue(orc.getHealth() <= 0);
}
@Test
void testAcqusition() throws InterruptedException {
Assertions.assertTrue(elf.acquire(sword));
Assertions.assertEquals(elf.getName(), sword.getLocker().getName());
Assertions.assertTrue(elf.getInstruments().contains(sword));
Assertions.assertFalse(orc.acquire(sword));
killCreature(orc, elf);
Assertions.assertTrue(orc.acquire(sword));
Assertions.assertEquals(orc, sword.getLocker());
}
void killCreature(Creature source, Creature target) throws InterruptedException {
while (target.isAlive()) {
source.attack(target);
}
}
@Test
void invalidDamageTest(){
Assertions.assertThrows(IllegalArgumentException.class, () -> elf.hit(-50));
}
}

View File

@ -0,0 +1,21 @@
package com.iluwatar.lockableobject;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
class ExceptionsTest {
private String msg = "test";
@Test
void testException(){
Exception e;
try{
throw new LockingException(msg);
}
catch(LockingException ex){
e = ex;
}
Assertions.assertEquals(msg, e.getMessage());
}
}

View File

@ -0,0 +1,47 @@
package com.iluwatar.lockableobject;
import com.iluwatar.lockableobject.domain.Creature;
import com.iluwatar.lockableobject.domain.Elf;
import com.iluwatar.lockableobject.domain.Feind;
import com.iluwatar.lockableobject.domain.Orc;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class FeindTest {
private Creature elf;
private Creature orc;
private Lockable sword;
@BeforeEach
void init(){
elf = new Elf("Nagdil");
orc = new Orc("Ghandar");
sword = new SwordOfAragorn();
}
@Test
void nullTests(){
Assertions.assertThrows(NullPointerException.class, () -> new Feind(null, null));
Assertions.assertThrows(NullPointerException.class, () -> new Feind(elf, null));
Assertions.assertThrows(NullPointerException.class, () -> new Feind(null, sword));
}
@Test
void testBaseCase() throws InterruptedException {
var base = new Thread(new Feind(orc, sword));
Assertions.assertNull(sword.getLocker());
base.start();
base.join();
Assertions.assertEquals(orc, sword.getLocker());
var extend = new Thread(new Feind(elf, sword));
extend.start();
extend.join();
Assertions.assertTrue(sword.isLocked());
sword.unlock(elf.isAlive() ? elf : orc);
Assertions.assertNull(sword.getLocker());
}
}

View File

@ -0,0 +1,24 @@
package com.iluwatar.lockableobject;
import com.iluwatar.lockableobject.domain.CreatureStats;
import com.iluwatar.lockableobject.domain.Elf;
import com.iluwatar.lockableobject.domain.Human;
import com.iluwatar.lockableobject.domain.Orc;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
class SubCreaturesTests {
@Test
void statsTest(){
var elf = new Elf("Limbar");
var orc = new Orc("Dargal");
var human = new Human("Jerry");
Assertions.assertEquals(CreatureStats.ELF_HEALTH.getValue(), elf.getHealth());
Assertions.assertEquals(CreatureStats.ELF_DAMAGE.getValue(), elf.getDamage());
Assertions.assertEquals(CreatureStats.ORC_DAMAGE.getValue(), orc.getDamage());
Assertions.assertEquals(CreatureStats.ORC_HEALTH.getValue(), orc.getHealth());
Assertions.assertEquals(CreatureStats.HUMAN_DAMAGE.getValue(), human.getDamage());
Assertions.assertEquals(CreatureStats.HUMAN_HEALTH.getValue(), human.getHealth());
}
}

View File

@ -0,0 +1,27 @@
package com.iluwatar.lockableobject;
import com.iluwatar.lockableobject.domain.Human;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
class TheSwordOfAragornTest {
@Test
void basicSwordTest() {
var sword = new SwordOfAragorn();
Assertions.assertNotNull(sword.getName());
Assertions.assertNull(sword.getLocker());
Assertions.assertFalse(sword.isLocked());
var human = new Human("Tupac");
Assertions.assertTrue(human.acquire(sword));
Assertions.assertEquals(human, sword.getLocker());
Assertions.assertTrue(sword.isLocked());
}
@Test
void invalidLockerTest(){
var sword = new SwordOfAragorn();
Assertions.assertThrows(NullPointerException.class, () -> sword.lock(null));
Assertions.assertThrows(NullPointerException.class, () -> sword.unlock(null));
}
}

View File

@ -226,6 +226,7 @@
<module>model-view-viewmodel</module>
<module>composite-entity</module>
<module>presentation</module>
<module>lockable-object</module>
</modules>
<repositories>