diff --git a/lockable-object/README.md b/lockable-object/README.md new file mode 100644 index 000000000..f68ec4336 --- /dev/null +++ b/lockable-object/README.md @@ -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 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 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) diff --git a/lockable-object/etc/lockable-object.urm.png b/lockable-object/etc/lockable-object.urm.png new file mode 100644 index 000000000..431244f2b Binary files /dev/null and b/lockable-object/etc/lockable-object.urm.png differ diff --git a/lockable-object/etc/lockable-object.urm.puml b/lockable-object/etc/lockable-object.urm.puml new file mode 100644 index 000000000..646cc3bc5 --- /dev/null +++ b/lockable-object/etc/lockable-object.urm.puml @@ -0,0 +1,94 @@ +@startuml +package com.iluwatar.lockableobject.domain { + abstract class Creature { + - LOGGER : Logger {static} + - damage : int + - health : int + ~ instruments : Set + - 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 + + getName() : String + + getType() : CreatureType + + hashCode() : int + + hit(damage : int) + + isAlive() : boolean + + kill() + + setDamage(damage : int) + + setHealth(health : int) + + setInstruments(instruments : Set) + + 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 \ No newline at end of file diff --git a/lockable-object/pom.xml b/lockable-object/pom.xml new file mode 100644 index 000000000..a01299dc9 --- /dev/null +++ b/lockable-object/pom.xml @@ -0,0 +1,60 @@ + + + + 4.0.0 + + com.iluwatar + java-design-patterns + 1.25.0-SNAPSHOT + + lockable-object + + + org.junit.jupiter + junit-jupiter-engine + test + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + + + + com.iluwatar.lockableobject.App + + + + + + + + + \ No newline at end of file diff --git a/lockable-object/src/main/java/com/iluwatar/lockableobject/App.java b/lockable-object/src/main/java/com/iluwatar/lockableobject/App.java new file mode 100644 index 000000000..0429d2494 --- /dev/null +++ b/lockable-object/src/main/java/com/iluwatar/lockableobject/App.java @@ -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. + * + *

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 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(); + } + } +} diff --git a/lockable-object/src/main/java/com/iluwatar/lockableobject/Lockable.java b/lockable-object/src/main/java/com/iluwatar/lockableobject/Lockable.java new file mode 100644 index 000000000..b0257236b --- /dev/null +++ b/lockable-object/src/main/java/com/iluwatar/lockableobject/Lockable.java @@ -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(); +} diff --git a/lockable-object/src/main/java/com/iluwatar/lockableobject/LockingException.java b/lockable-object/src/main/java/com/iluwatar/lockableobject/LockingException.java new file mode 100644 index 000000000..2446ce6b0 --- /dev/null +++ b/lockable-object/src/main/java/com/iluwatar/lockableobject/LockingException.java @@ -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); + } + +} diff --git a/lockable-object/src/main/java/com/iluwatar/lockableobject/SwordOfAragorn.java b/lockable-object/src/main/java/com/iluwatar/lockableobject/SwordOfAragorn.java new file mode 100644 index 000000000..1840c3770 --- /dev/null +++ b/lockable-object/src/main/java/com/iluwatar/lockableobject/SwordOfAragorn.java @@ -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; + } +} diff --git a/lockable-object/src/main/java/com/iluwatar/lockableobject/domain/Creature.java b/lockable-object/src/main/java/com/iluwatar/lockableobject/domain/Creature.java new file mode 100644 index 000000000..736fd0e98 --- /dev/null +++ b/lockable-object/src/main/java/com/iluwatar/lockableobject/domain/Creature.java @@ -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 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; + } + +} diff --git a/lockable-object/src/main/java/com/iluwatar/lockableobject/domain/CreatureStats.java b/lockable-object/src/main/java/com/iluwatar/lockableobject/domain/CreatureStats.java new file mode 100644 index 000000000..dac74bd36 --- /dev/null +++ b/lockable-object/src/main/java/com/iluwatar/lockableobject/domain/CreatureStats.java @@ -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; + } +} diff --git a/lockable-object/src/main/java/com/iluwatar/lockableobject/domain/CreatureType.java b/lockable-object/src/main/java/com/iluwatar/lockableobject/domain/CreatureType.java new file mode 100644 index 000000000..5bb0734b1 --- /dev/null +++ b/lockable-object/src/main/java/com/iluwatar/lockableobject/domain/CreatureType.java @@ -0,0 +1,8 @@ +package com.iluwatar.lockableobject.domain; + +/** Constants of supported creatures. */ +public enum CreatureType { + ORC, + HUMAN, + ELF +} diff --git a/lockable-object/src/main/java/com/iluwatar/lockableobject/domain/Elf.java b/lockable-object/src/main/java/com/iluwatar/lockableobject/domain/Elf.java new file mode 100644 index 000000000..56b22ec67 --- /dev/null +++ b/lockable-object/src/main/java/com/iluwatar/lockableobject/domain/Elf.java @@ -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()); + } +} diff --git a/lockable-object/src/main/java/com/iluwatar/lockableobject/domain/Feind.java b/lockable-object/src/main/java/com/iluwatar/lockableobject/domain/Feind.java new file mode 100644 index 000000000..cf793b520 --- /dev/null +++ b/lockable-object/src/main/java/com/iluwatar/lockableobject/domain/Feind.java @@ -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()); + } + } + } +} diff --git a/lockable-object/src/main/java/com/iluwatar/lockableobject/domain/Human.java b/lockable-object/src/main/java/com/iluwatar/lockableobject/domain/Human.java new file mode 100644 index 000000000..6ff3c8bad --- /dev/null +++ b/lockable-object/src/main/java/com/iluwatar/lockableobject/domain/Human.java @@ -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()); + } +} diff --git a/lockable-object/src/main/java/com/iluwatar/lockableobject/domain/Orc.java b/lockable-object/src/main/java/com/iluwatar/lockableobject/domain/Orc.java new file mode 100644 index 000000000..a74637a44 --- /dev/null +++ b/lockable-object/src/main/java/com/iluwatar/lockableobject/domain/Orc.java @@ -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()); + } +} diff --git a/lockable-object/src/test/java/com/iluwatar/lockableobject/AppTest.java b/lockable-object/src/test/java/com/iluwatar/lockableobject/AppTest.java new file mode 100644 index 000000000..8198d3bee --- /dev/null +++ b/lockable-object/src/test/java/com/iluwatar/lockableobject/AppTest.java @@ -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()); + } +} diff --git a/lockable-object/src/test/java/com/iluwatar/lockableobject/CreatureTest.java b/lockable-object/src/test/java/com/iluwatar/lockableobject/CreatureTest.java new file mode 100644 index 000000000..9986e1951 --- /dev/null +++ b/lockable-object/src/test/java/com/iluwatar/lockableobject/CreatureTest.java @@ -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)); + } +} diff --git a/lockable-object/src/test/java/com/iluwatar/lockableobject/ExceptionsTest.java b/lockable-object/src/test/java/com/iluwatar/lockableobject/ExceptionsTest.java new file mode 100644 index 000000000..bb6d7a9dc --- /dev/null +++ b/lockable-object/src/test/java/com/iluwatar/lockableobject/ExceptionsTest.java @@ -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()); + } +} diff --git a/lockable-object/src/test/java/com/iluwatar/lockableobject/FeindTest.java b/lockable-object/src/test/java/com/iluwatar/lockableobject/FeindTest.java new file mode 100644 index 000000000..230ffbccb --- /dev/null +++ b/lockable-object/src/test/java/com/iluwatar/lockableobject/FeindTest.java @@ -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()); + } + +} diff --git a/lockable-object/src/test/java/com/iluwatar/lockableobject/SubCreaturesTests.java b/lockable-object/src/test/java/com/iluwatar/lockableobject/SubCreaturesTests.java new file mode 100644 index 000000000..72cedbc43 --- /dev/null +++ b/lockable-object/src/test/java/com/iluwatar/lockableobject/SubCreaturesTests.java @@ -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()); + } +} diff --git a/lockable-object/src/test/java/com/iluwatar/lockableobject/TheSwordOfAragornTest.java b/lockable-object/src/test/java/com/iluwatar/lockableobject/TheSwordOfAragornTest.java new file mode 100644 index 000000000..186e085ea --- /dev/null +++ b/lockable-object/src/test/java/com/iluwatar/lockableobject/TheSwordOfAragornTest.java @@ -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)); + } +} diff --git a/pom.xml b/pom.xml index 9cd6b6adb..1b8f30e92 100644 --- a/pom.xml +++ b/pom.xml @@ -226,6 +226,7 @@ model-view-viewmodel composite-entity presentation + lockable-object