Compare commits
8 Commits
all-contri
...
all-contri
Author | SHA1 | Date | |
---|---|---|---|
e29a18860a | |||
08caf6ddb0 | |||
e498c25675 | |||
122e6edb38 | |||
ea3c9d955e | |||
f1feb3f6a0 | |||
1388e38744 | |||
1dd26289e5 |
@ -1505,6 +1505,24 @@
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "EdisonE3",
|
||||
"name": "EdisonE3",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/52118917?v=4",
|
||||
"profile": "https://github.com/EdisonE3",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "tao-sun2",
|
||||
"name": "Tao",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/66189688?v=4",
|
||||
"profile": "https://github.com/tao-sun2",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 4,
|
||||
|
@ -10,7 +10,7 @@
|
||||
[](https://sonarcloud.io/dashboard?id=iluwatar_java-design-patterns)
|
||||
[](https://gitter.im/iluwatar/java-design-patterns?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
[](#contributors-)
|
||||
[](#contributors-)
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||
|
||||
<br/>
|
||||
@ -323,6 +323,8 @@ This project is licensed under the terms of the MIT license.
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/JackieNim"><img src="https://avatars.githubusercontent.com/u/4138836?v=4?s=100" width="100px;" alt=""/><br /><sub><b>JackieNim</b></sub></a><br /><a href="https://github.com/iluwatar/java-design-patterns/commits?author=JackieNim" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/EdisonE3"><img src="https://avatars.githubusercontent.com/u/52118917?v=4?s=100" width="100px;" alt=""/><br /><sub><b>EdisonE3</b></sub></a><br /><a href="https://github.com/iluwatar/java-design-patterns/commits?author=EdisonE3" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/tao-sun2"><img src="https://avatars.githubusercontent.com/u/66189688?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Tao</b></sub></a><br /><a href="https://github.com/iluwatar/java-design-patterns/commits?author=tao-sun2" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
285
lockable-object/README.md
Normal file
285
lockable-object/README.md
Normal 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
|
||||
|
||||

|
||||
|
||||
|
||||
## Credits
|
||||
|
||||
* [Lockable Object - Chapter 10.3, J2EE Design Patterns, O'Reilly](http://ommolketab.ir/aaf-lib/axkwht7wxrhvgs2aqkxse8hihyu9zv.pdf)
|
BIN
lockable-object/etc/lockable-object.urm.png
Normal file
BIN
lockable-object/etc/lockable-object.urm.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 113 KiB |
94
lockable-object/etc/lockable-object.urm.puml
Normal file
94
lockable-object/etc/lockable-object.urm.puml
Normal 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
60
lockable-object/pom.xml
Normal 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>
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package com.iluwatar.lockableobject.domain;
|
||||
|
||||
/** Constants of supported creatures. */
|
||||
public enum CreatureType {
|
||||
ORC,
|
||||
HUMAN,
|
||||
ELF
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
3
pom.xml
3
pom.xml
@ -225,6 +225,9 @@
|
||||
<module>active-object</module>
|
||||
<module>model-view-viewmodel</module>
|
||||
<module>composite-entity</module>
|
||||
<module>table-module</module>
|
||||
<module>presentation</module>
|
||||
<module>lockable-object</module>
|
||||
</modules>
|
||||
|
||||
<repositories>
|
||||
|
193
presentation/README.md
Normal file
193
presentation/README.md
Normal file
@ -0,0 +1,193 @@
|
||||
---
|
||||
layout: pattern
|
||||
title: Presentation
|
||||
folder: presentation
|
||||
permalink: /patterns/presentation/
|
||||
categories: Behavioral
|
||||
tags:
|
||||
- Decoupling
|
||||
---
|
||||
## Also known as
|
||||
Application Model
|
||||
|
||||
## Intent
|
||||
Presentation Model pulls the state and behavior of the view out into a model class that is part of the presentation.
|
||||
|
||||
## Explanation
|
||||
|
||||
Real world example
|
||||
|
||||
> When we need to write a program with GUI, there is no need for us to put all presentation behavior in the view class. Because it will test become harder. So we can use Presentation Model Pattern to separate the behavior and view. The view only need to load the data and states from other class and show these data on the screen according to the states.
|
||||
|
||||
In plain words
|
||||
|
||||
> a pattern that used to divide the presentation and controlling.
|
||||
|
||||
Code Example
|
||||
|
||||
Class `view` is the GUI of albums. Methods `saveToPMod` and `loadFromPMod` are used to achieve synchronization.
|
||||
|
||||
```java
|
||||
public class View {
|
||||
/**
|
||||
* the model that controls this view.
|
||||
*/
|
||||
private final PresentationModel model;
|
||||
|
||||
private TextField txtTitle;
|
||||
private TextField txtArtist;
|
||||
private JCheckBox chkClassical;
|
||||
private TextField txtComposer;
|
||||
private JList<String> albumList;
|
||||
private JButton apply;
|
||||
private JButton cancel;
|
||||
|
||||
public View() {
|
||||
model = new PresentationModel(PresentationModel.albumDataSet());
|
||||
}
|
||||
|
||||
/**
|
||||
* save the data to PresentationModel.
|
||||
*/
|
||||
public void saveToPMod() {
|
||||
LOGGER.info("Save data to PresentationModel");
|
||||
model.setArtist(txtArtist.getText());
|
||||
model.setTitle(txtTitle.getText());
|
||||
model.setIsClassical(chkClassical.isSelected());
|
||||
model.setComposer(txtComposer.getText());
|
||||
}
|
||||
|
||||
/**
|
||||
* load the data from PresentationModel.
|
||||
*/
|
||||
public void loadFromPMod() {
|
||||
LOGGER.info("Load data from PresentationModel");
|
||||
txtArtist.setText(model.getArtist());
|
||||
txtTitle.setText(model.getTitle());
|
||||
chkClassical.setSelected(model.getIsClassical());
|
||||
txtComposer.setEditable(model.getIsClassical());
|
||||
txtComposer.setText(model.getComposer());
|
||||
}
|
||||
|
||||
public void createView() {
|
||||
// the detail of GUI information like size, listenser and so on.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Class `Album` is to store information of a album.
|
||||
|
||||
```java
|
||||
public class Album {
|
||||
|
||||
private String title;
|
||||
private String artist;
|
||||
private boolean isClassical;
|
||||
/**
|
||||
* only when the album is classical,
|
||||
* composer can have content.
|
||||
*/
|
||||
private String composer;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Class `DisplatedAlbums` is store the information of all the albums that will be displayed on GUI.
|
||||
|
||||
```java
|
||||
public class DisplayedAlbums {
|
||||
private final List<Album> albums;
|
||||
|
||||
public DisplayedAlbums() {
|
||||
this.albums = new ArrayList<>();
|
||||
}
|
||||
|
||||
public void addAlbums(final String title,
|
||||
final String artist, final boolean isClassical,
|
||||
final String composer) {
|
||||
if (isClassical) {
|
||||
this.albums.add(new Album(title, artist, true, composer));
|
||||
} else {
|
||||
this.albums.add(new Album(title, artist, false, ""));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Class `PresentationMod` is used to control all the action of GUI.
|
||||
|
||||
```java
|
||||
public class PresentationModel {
|
||||
private final DisplayedAlbums data;
|
||||
|
||||
private int selectedAlbumNumber;
|
||||
private Album selectedAlbum;
|
||||
|
||||
public PresentationModel(final DisplayedAlbums dataOfAlbums) {
|
||||
this.data = dataOfAlbums;
|
||||
this.selectedAlbumNumber = 1;
|
||||
this.selectedAlbum = this.data.getAlbums().get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the value of selectedAlbumNumber.
|
||||
*
|
||||
* @param albumNumber the number of album which is shown on the view.
|
||||
*/
|
||||
public void setSelectedAlbumNumber(final int albumNumber) {
|
||||
LOGGER.info("Change select number from {} to {}",
|
||||
this.selectedAlbumNumber, albumNumber);
|
||||
this.selectedAlbumNumber = albumNumber;
|
||||
this.selectedAlbum = data.getAlbums().get(this.selectedAlbumNumber - 1);
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return selectedAlbum.getTitle();
|
||||
}
|
||||
// other get methods are like this, which are used to get information of selected album.
|
||||
|
||||
public void setTitle(final String value) {
|
||||
LOGGER.info("Change album title from {} to {}",
|
||||
selectedAlbum.getTitle(), value);
|
||||
selectedAlbum.setTitle(value);
|
||||
}
|
||||
// other set methods are like this, which are used to get information of selected album.
|
||||
|
||||
/**
|
||||
* Gets a list of albums.
|
||||
*
|
||||
* @return the names of all the albums.
|
||||
*/
|
||||
public String[] getAlbumList() {
|
||||
var result = new String[data.getAlbums().size()];
|
||||
for (var i = 0; i < result.length; i++) {
|
||||
result[i] = data.getAlbums().get(i).getTitle();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
We can run class `App` to start this demo. the checkbox is the album classical; the first text field is the name of album artist; the second is the name of album title; the last one is the name of the composer:
|
||||
|
||||

|
||||
|
||||
|
||||
## Class diagram
|
||||

|
||||
|
||||
## Applicability
|
||||
Use the Presentation Model Pattern when
|
||||
|
||||
* Testing a presentation through a GUI window is often awkward, and in some cases impossible.
|
||||
* Do not determine which GUI will be used.
|
||||
|
||||
## Related patterns
|
||||
|
||||
- [Supervising Controller](https://martinfowler.com/eaaDev/SupervisingPresenter.html)
|
||||
- [Passive View](https://martinfowler.com/eaaDev/PassiveScreen.html)
|
||||
|
||||
## Credits
|
||||
|
||||
* [Presentation Model Patterns](https://martinfowler.com/eaaDev/PresentationModel.html)
|
||||
|
BIN
presentation/etc/presentation.urm.png
Normal file
BIN
presentation/etc/presentation.urm.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 121 KiB |
59
presentation/etc/presentation.urm.puml
Normal file
59
presentation/etc/presentation.urm.puml
Normal file
@ -0,0 +1,59 @@
|
||||
@startuml
|
||||
package com.iluwatar.presentation {
|
||||
class Album {
|
||||
~ artist : String
|
||||
~ composer : String
|
||||
~ isClassical : boolean
|
||||
~ rowId : int
|
||||
~ title : String
|
||||
+ Album(rowId : int, title : String, artist : String, isClassical : boolean, composer : String)
|
||||
}
|
||||
class App {
|
||||
+ App()
|
||||
+ main(args : String[]) {static}
|
||||
}
|
||||
class DsAlbum {
|
||||
+ albums : List<Album>
|
||||
+ albumsCache : List<Album>
|
||||
+ DsAlbum()
|
||||
+ acceptChanges()
|
||||
+ addAlbums(rowId : int, title : String, artist : String, isClassical : boolean, composer : String)
|
||||
}
|
||||
class PresentationMod {
|
||||
- data : DsAlbum
|
||||
- selectedAlbum : Album
|
||||
- selectedAlbumNumber : int
|
||||
+ PresentationMod(data : DsAlbum)
|
||||
+ albumDataSet() : DsAlbum {static}
|
||||
+ getAlbumList() : String[]
|
||||
+ getArtist() : String
|
||||
+ getComposer() : String
|
||||
+ getIsClassical() : boolean
|
||||
+ getTitle() : String
|
||||
+ setArtist(value : String)
|
||||
+ setComposer(value : String)
|
||||
+ setIsClassical(value : boolean)
|
||||
+ setSelectedAlbumNumber(selectedAlbumNumber : int)
|
||||
+ setTitle(value : String)
|
||||
}
|
||||
class View {
|
||||
~ albumList : JList<String>
|
||||
~ apply : JButton
|
||||
~ cancel : JButton
|
||||
~ chkClassical : JCheckBox
|
||||
~ model : PresentationMod
|
||||
~ notLoadView : boolean
|
||||
~ txtArtist : TextField
|
||||
~ txtComposer : TextField
|
||||
~ txtTitle : TextField
|
||||
+ View()
|
||||
+ createView()
|
||||
+ loadFromPMod()
|
||||
+ saveToPMod()
|
||||
}
|
||||
}
|
||||
PresentationMod --> "-selectedAlbum" Album
|
||||
View --> "-model" PresentationMod
|
||||
DsAlbum --> "-albums" Album
|
||||
PresentationMod --> "-data" DsAlbum
|
||||
@enduml
|
BIN
presentation/etc/result.png
Normal file
BIN
presentation/etc/result.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
63
presentation/pom.xml
Normal file
63
presentation/pom.xml
Normal file
@ -0,0 +1,63 @@
|
||||
<!--
|
||||
|
||||
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 xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.iluwatar</groupId>
|
||||
<artifactId>java-design-patterns</artifactId>
|
||||
<version>1.25.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>presentation</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.presentation.App</mainClass>
|
||||
</manifest>
|
||||
</archive>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
@ -0,0 +1,31 @@
|
||||
package com.iluwatar.presentation;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
*A class used to store the information of album.
|
||||
*/
|
||||
@Setter
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public class Album {
|
||||
/**
|
||||
* the title of the album.
|
||||
*/
|
||||
private String title;
|
||||
/**
|
||||
* the artist name of the album.
|
||||
*/
|
||||
private String artist;
|
||||
/**
|
||||
* is the album classical, true or false.
|
||||
*/
|
||||
private boolean isClassical;
|
||||
/**
|
||||
* only when the album is classical,
|
||||
* composer can have content.
|
||||
*/
|
||||
private String composer;
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package com.iluwatar.presentation;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* The Presentation model pattern is used to divide the presentation and controlling.
|
||||
* This demo is a used to information of some albums with GUI.
|
||||
*/
|
||||
@Slf4j
|
||||
public final class App {
|
||||
/**
|
||||
* the constructor.
|
||||
*/
|
||||
private App() {
|
||||
}
|
||||
|
||||
/**
|
||||
* main method.
|
||||
*
|
||||
* @param args args
|
||||
*/
|
||||
public static void main(final String[] args) {
|
||||
var view = new View();
|
||||
view.createView();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,46 @@
|
||||
package com.iluwatar.presentation;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* a class used to deal with albums.
|
||||
*
|
||||
*/
|
||||
@Slf4j
|
||||
@Getter
|
||||
public class DisplayedAlbums {
|
||||
/**
|
||||
* albums a list of albums.
|
||||
*/
|
||||
private final List<Album> albums;
|
||||
|
||||
/**
|
||||
* a constructor method.
|
||||
*/
|
||||
public DisplayedAlbums() {
|
||||
this.albums = new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* a method used to add a new album to album list.
|
||||
*
|
||||
* @param title the title of the album.
|
||||
* @param artist the artist name of the album.
|
||||
* @param isClassical is the album classical, true or false.
|
||||
* @param composer only when the album is classical,
|
||||
* composer can have content.
|
||||
*/
|
||||
public void addAlbums(final String title,
|
||||
final String artist, final boolean isClassical,
|
||||
final String composer) {
|
||||
if (isClassical) {
|
||||
this.albums.add(new Album(title, artist, true, composer));
|
||||
} else {
|
||||
this.albums.add(new Album(title, artist, false, ""));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,163 @@
|
||||
package com.iluwatar.presentation;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* The class between view and albums, it is used to control the data.
|
||||
*/
|
||||
@Slf4j
|
||||
public class PresentationModel {
|
||||
/**
|
||||
* the data of all albums that will be shown.
|
||||
*/
|
||||
private final DisplayedAlbums data;
|
||||
/**
|
||||
* the no of selected album.
|
||||
*/
|
||||
private int selectedAlbumNumber;
|
||||
/**
|
||||
* the selected album.
|
||||
*/
|
||||
private Album selectedAlbum;
|
||||
|
||||
/**
|
||||
* Generates a set of data for testing.
|
||||
*
|
||||
* @return a instance of DsAlbum which store the data.
|
||||
*/
|
||||
public static DisplayedAlbums albumDataSet() {
|
||||
var titleList = new String[]{"HQ", "The Rough Dancer and Cyclical Night",
|
||||
"The Black Light", "Symphony No.5"};
|
||||
var artistList = new String[]{"Roy Harper", "Astor Piazzola",
|
||||
"The Black Light", "CBSO"};
|
||||
var isClassicalList = new boolean[]{false, false, false, true};
|
||||
var composerList = new String[]{null, null, null, "Sibelius"};
|
||||
|
||||
var result = new DisplayedAlbums();
|
||||
for (var i = 1; i <= titleList.length; i++) {
|
||||
result.addAlbums(titleList[i - 1], artistList[i - 1],
|
||||
isClassicalList[i - 1], composerList[i - 1]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* constructor method.
|
||||
*
|
||||
* @param dataOfAlbums the data of all the albums
|
||||
*/
|
||||
public PresentationModel(final DisplayedAlbums dataOfAlbums) {
|
||||
this.data = dataOfAlbums;
|
||||
this.selectedAlbumNumber = 1;
|
||||
this.selectedAlbum = this.data.getAlbums().get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the value of selectedAlbumNumber.
|
||||
*
|
||||
* @param albumNumber the number of album which is shown on the view.
|
||||
*/
|
||||
public void setSelectedAlbumNumber(final int albumNumber) {
|
||||
LOGGER.info("Change select number from {} to {}",
|
||||
this.selectedAlbumNumber, albumNumber);
|
||||
this.selectedAlbumNumber = albumNumber;
|
||||
this.selectedAlbum = data.getAlbums().get(this.selectedAlbumNumber - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* get the title of selected album.
|
||||
*
|
||||
* @return the tile of selected album.
|
||||
*/
|
||||
public String getTitle() {
|
||||
return selectedAlbum.getTitle();
|
||||
}
|
||||
|
||||
/**
|
||||
* set the title of selected album.
|
||||
*
|
||||
* @param value the title which user want to user.
|
||||
*/
|
||||
public void setTitle(final String value) {
|
||||
LOGGER.info("Change album title from {} to {}",
|
||||
selectedAlbum.getTitle(), value);
|
||||
selectedAlbum.setTitle(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* get the artist of selected album.
|
||||
*
|
||||
* @return the artist of selected album.
|
||||
*/
|
||||
public String getArtist() {
|
||||
return selectedAlbum.getArtist();
|
||||
}
|
||||
|
||||
/**
|
||||
* set the name of artist.
|
||||
*
|
||||
* @param value the name want artist to be.
|
||||
*/
|
||||
public void setArtist(final String value) {
|
||||
LOGGER.info("Change album artist from {} to {}",
|
||||
selectedAlbum.getArtist(), value);
|
||||
selectedAlbum.setArtist(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a boolean value which represents whether the album is classical.
|
||||
*
|
||||
* @return is the album classical.
|
||||
*/
|
||||
public boolean getIsClassical() {
|
||||
return selectedAlbum.isClassical();
|
||||
}
|
||||
|
||||
/**
|
||||
* set the isClassical of album.
|
||||
*
|
||||
* @param value is the album classical.
|
||||
*/
|
||||
public void setIsClassical(final boolean value) {
|
||||
LOGGER.info("Change album isClassical from {} to {}",
|
||||
selectedAlbum.isClassical(), value);
|
||||
selectedAlbum.setClassical(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* get is classical of the selected album.
|
||||
*
|
||||
* @return is the album classical.
|
||||
*/
|
||||
public String getComposer() {
|
||||
return selectedAlbum.isClassical() ? selectedAlbum.getComposer() : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the name of composer when the album is classical.
|
||||
*
|
||||
* @param value the name of composer.
|
||||
*/
|
||||
public void setComposer(final String value) {
|
||||
if (selectedAlbum.isClassical()) {
|
||||
LOGGER.info("Change album composer from {} to {}",
|
||||
selectedAlbum.getComposer(), value);
|
||||
selectedAlbum.setComposer(value);
|
||||
} else {
|
||||
LOGGER.info("Composer can not be changed");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of albums.
|
||||
*
|
||||
* @return the names of all the albums.
|
||||
*/
|
||||
public String[] getAlbumList() {
|
||||
var result = new String[data.getAlbums().size()];
|
||||
for (var i = 0; i < result.length; i++) {
|
||||
result[i] = data.getAlbums().get(i).getTitle();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
167
presentation/src/main/java/com/iluwatar/presentation/View.java
Normal file
167
presentation/src/main/java/com/iluwatar/presentation/View.java
Normal file
@ -0,0 +1,167 @@
|
||||
package com.iluwatar.presentation;
|
||||
|
||||
import java.awt.TextField;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import javax.swing.Box;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JCheckBox;
|
||||
import javax.swing.JFrame;
|
||||
import javax.swing.JList;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Generates the GUI of albums.
|
||||
*/
|
||||
@Getter
|
||||
@Slf4j
|
||||
public class View {
|
||||
/**
|
||||
* the model that controls this view.
|
||||
*/
|
||||
private final PresentationModel model;
|
||||
|
||||
/**
|
||||
* the filed to show and modify title.
|
||||
*/
|
||||
private TextField txtTitle;
|
||||
/**
|
||||
* the filed to show and modify the name of artist.
|
||||
*/
|
||||
private TextField txtArtist;
|
||||
/**
|
||||
* the checkbox for is classical.
|
||||
*/
|
||||
private JCheckBox chkClassical;
|
||||
/**
|
||||
* the filed to show and modify composer.
|
||||
*/
|
||||
private TextField txtComposer;
|
||||
/**
|
||||
* a list to show all the name of album.
|
||||
*/
|
||||
private JList<String> albumList;
|
||||
/**
|
||||
* a button to apply of all the change.
|
||||
*/
|
||||
private JButton apply;
|
||||
/**
|
||||
* roll back the change.
|
||||
*/
|
||||
private JButton cancel;
|
||||
|
||||
/**
|
||||
* the value of the text field size.
|
||||
*/
|
||||
static final int WIDTH_TXT = 200;
|
||||
static final int HEIGHT_TXT = 50;
|
||||
|
||||
/**
|
||||
* the value of the GUI size and location.
|
||||
*/
|
||||
static final int LOCATION_X = 200;
|
||||
static final int LOCATION_Y = 200;
|
||||
static final int WIDTH = 500;
|
||||
static final int HEIGHT = 300;
|
||||
|
||||
/**
|
||||
* constructor method.
|
||||
*/
|
||||
public View() {
|
||||
model = new PresentationModel(PresentationModel.albumDataSet());
|
||||
}
|
||||
|
||||
/**
|
||||
* save the data to PresentationModel.
|
||||
*/
|
||||
public void saveToPMod() {
|
||||
LOGGER.info("Save data to PresentationModel");
|
||||
model.setArtist(txtArtist.getText());
|
||||
model.setTitle(txtTitle.getText());
|
||||
model.setIsClassical(chkClassical.isSelected());
|
||||
model.setComposer(txtComposer.getText());
|
||||
}
|
||||
|
||||
/**
|
||||
* load the data from PresentationModel.
|
||||
*/
|
||||
public void loadFromPMod() {
|
||||
LOGGER.info("Load data from PresentationModel");
|
||||
txtArtist.setText(model.getArtist());
|
||||
txtTitle.setText(model.getTitle());
|
||||
chkClassical.setSelected(model.getIsClassical());
|
||||
txtComposer.setEditable(model.getIsClassical());
|
||||
txtComposer.setText(model.getComposer());
|
||||
}
|
||||
|
||||
/**
|
||||
* initialize the GUI.
|
||||
*/
|
||||
public void createView() {
|
||||
var frame = new JFrame("Album");
|
||||
var b1 = Box.createHorizontalBox();
|
||||
|
||||
frame.add(b1);
|
||||
albumList = new JList<>(model.getAlbumList());
|
||||
albumList.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(final MouseEvent e) {
|
||||
model.setSelectedAlbumNumber(albumList.getSelectedIndex() + 1);
|
||||
loadFromPMod();
|
||||
}
|
||||
});
|
||||
b1.add(albumList);
|
||||
|
||||
var b2 = Box.createVerticalBox();
|
||||
b1.add(b2);
|
||||
|
||||
txtArtist = new TextField();
|
||||
txtTitle = new TextField();
|
||||
|
||||
txtArtist.setSize(WIDTH_TXT, HEIGHT_TXT);
|
||||
txtTitle.setSize(WIDTH_TXT, HEIGHT_TXT);
|
||||
|
||||
chkClassical = new JCheckBox();
|
||||
txtComposer = new TextField();
|
||||
chkClassical.addActionListener(itemEvent -> {
|
||||
txtComposer.setEditable(chkClassical.isSelected());
|
||||
if (!chkClassical.isSelected()) {
|
||||
txtComposer.setText("");
|
||||
}
|
||||
});
|
||||
txtComposer.setSize(WIDTH_TXT, HEIGHT_TXT);
|
||||
txtComposer.setEditable(model.getIsClassical());
|
||||
|
||||
apply = new JButton("Apply");
|
||||
apply.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(final MouseEvent e) {
|
||||
saveToPMod();
|
||||
loadFromPMod();
|
||||
}
|
||||
});
|
||||
cancel = new JButton("Cancel");
|
||||
cancel.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(final MouseEvent e) {
|
||||
loadFromPMod();
|
||||
}
|
||||
});
|
||||
|
||||
b2.add(txtArtist);
|
||||
b2.add(txtTitle);
|
||||
|
||||
b2.add(chkClassical);
|
||||
b2.add(txtComposer);
|
||||
|
||||
b2.add(apply);
|
||||
b2.add(cancel);
|
||||
|
||||
frame.setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
|
||||
frame.setBounds(LOCATION_X, LOCATION_Y, WIDTH, HEIGHT);
|
||||
frame.setVisible(true);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
package com.iluwatar.presentation;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class AlbumTest {
|
||||
@Test
|
||||
void testSetTitle(){
|
||||
Album album = new Album("a", "b", false, "");
|
||||
album.setTitle("b");
|
||||
assertEquals("b", album.getTitle());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetArtist(){
|
||||
Album album = new Album("a", "b", false, "");
|
||||
album.setArtist("c");
|
||||
assertEquals("c", album.getArtist());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetClassical(){
|
||||
Album album = new Album("a", "b", false, "");
|
||||
album.setClassical(true);
|
||||
assertTrue(album.isClassical());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetComposer(){
|
||||
Album album = new Album("a", "b", false, "");
|
||||
album.setClassical(true);
|
||||
album.setComposer("w");
|
||||
assertEquals("w", album.getComposer());
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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.presentation;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
|
||||
/**
|
||||
* Issue: Add at least one assertion to this test case.
|
||||
*
|
||||
* Solution: Inserted assertion to check whether the execution of the main method in {@link App}
|
||||
* throws an exception.
|
||||
*/
|
||||
class AppTest {
|
||||
|
||||
@Test
|
||||
void shouldExecuteApplicationWithoutException() {
|
||||
assertDoesNotThrow(() -> App.main(new String[]{}));
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package com.iluwatar.presentation;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class DisplayedAlbumsTest {
|
||||
@Test
|
||||
void testAdd_true(){
|
||||
DisplayedAlbums displayedAlbums = new DisplayedAlbums();
|
||||
displayedAlbums.addAlbums("title", "artist", true, "composer");
|
||||
assertEquals("composer", displayedAlbums.getAlbums().get(0).getComposer());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAdd_false(){
|
||||
DisplayedAlbums displayedAlbums = new DisplayedAlbums();
|
||||
displayedAlbums.addAlbums("title", "artist", false, "composer");
|
||||
assertEquals("", displayedAlbums.getAlbums().get(0).getComposer());
|
||||
}
|
||||
}
|
@ -0,0 +1,117 @@
|
||||
/*
|
||||
* 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.presentation;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class PresentationTest {
|
||||
String[] albumList = {"HQ", "The Rough Dancer and Cyclical Night", "The Black Light", "Symphony No.5"};
|
||||
|
||||
@Test
|
||||
void testCreateAlbumList() {
|
||||
PresentationModel model = new PresentationModel(PresentationModel.albumDataSet());
|
||||
String[] list = model.getAlbumList();
|
||||
assertEquals(Arrays.toString(albumList), Arrays.toString(list));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetSelectedAlbumNumber_1() {
|
||||
PresentationModel model = new PresentationModel(PresentationModel.albumDataSet());
|
||||
final int selectId = 2;
|
||||
model.setSelectedAlbumNumber(selectId);
|
||||
assertEquals(albumList[selectId - 1], model.getTitle());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetSelectedAlbumNumber_2() {
|
||||
PresentationModel model = new PresentationModel(PresentationModel.albumDataSet());
|
||||
final int selectId = 4;
|
||||
model.setSelectedAlbumNumber(selectId);
|
||||
assertEquals(albumList[selectId - 1], model.getTitle());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetTitle_1() {
|
||||
PresentationModel model = new PresentationModel(PresentationModel.albumDataSet());
|
||||
String testTitle = "TestTile";
|
||||
model.setTitle(testTitle);
|
||||
assertEquals(testTitle, model.getTitle());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetTitle_2() {
|
||||
PresentationModel model = new PresentationModel(PresentationModel.albumDataSet());
|
||||
String testTitle = "";
|
||||
model.setTitle(testTitle);
|
||||
assertEquals(testTitle, model.getTitle());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetArtist_1() {
|
||||
PresentationModel model = new PresentationModel(PresentationModel.albumDataSet());
|
||||
String testArtist = "TestArtist";
|
||||
model.setArtist(testArtist);
|
||||
assertEquals(testArtist, model.getArtist());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetArtist_2() {
|
||||
PresentationModel model = new PresentationModel(PresentationModel.albumDataSet());
|
||||
String testArtist = "";
|
||||
model.setArtist(testArtist);
|
||||
assertEquals(testArtist, model.getArtist());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetIsClassical() {
|
||||
PresentationModel model = new PresentationModel(PresentationModel.albumDataSet());
|
||||
model.setIsClassical(true);
|
||||
assertTrue(model.getIsClassical());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetComposer_false() {
|
||||
PresentationModel model = new PresentationModel(PresentationModel.albumDataSet());
|
||||
String testComposer = "TestComposer";
|
||||
|
||||
model.setIsClassical(false);
|
||||
model.setComposer(testComposer);
|
||||
assertEquals("", model.getComposer());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetComposer_true() {
|
||||
PresentationModel model = new PresentationModel(PresentationModel.albumDataSet());
|
||||
String testComposer = "TestComposer";
|
||||
|
||||
model.setIsClassical(true);
|
||||
model.setComposer(testComposer);
|
||||
assertEquals(testComposer, model.getComposer());
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
package com.iluwatar.presentation;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class ViewTest {
|
||||
String[] albumList = {"HQ", "The Rough Dancer and Cyclical Night", "The Black Light", "Symphony No.5"};
|
||||
|
||||
@Test
|
||||
void testSave_setArtistAndTitle(){
|
||||
View view = new View();
|
||||
view.createView();
|
||||
String testTitle = "testTitle";
|
||||
String testArtist = "testArtist";
|
||||
view.getTxtArtist().setText(testArtist);
|
||||
view.getTxtTitle().setText(testTitle);
|
||||
view.saveToPMod();
|
||||
view.loadFromPMod();
|
||||
assertEquals(testTitle, view.getModel().getTitle());
|
||||
assertEquals(testArtist, view.getModel().getArtist());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSave_setClassicalAndComposer(){
|
||||
View view = new View();
|
||||
view.createView();
|
||||
boolean isClassical = true;
|
||||
String testComposer = "testComposer";
|
||||
view.getChkClassical().setSelected(isClassical);
|
||||
view.getTxtComposer().setText(testComposer);
|
||||
view.saveToPMod();
|
||||
view.loadFromPMod();
|
||||
assertTrue(view.getModel().getIsClassical());
|
||||
assertEquals(testComposer, view.getModel().getComposer());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLoad_1(){
|
||||
View view = new View();
|
||||
view.createView();
|
||||
view.getModel().setSelectedAlbumNumber(2);
|
||||
view.loadFromPMod();
|
||||
assertEquals(albumList[1], view.getModel().getTitle());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLoad_2(){
|
||||
View view = new View();
|
||||
view.createView();
|
||||
view.getModel().setSelectedAlbumNumber(4);
|
||||
view.loadFromPMod();
|
||||
assertEquals(albumList[3], view.getModel().getTitle());
|
||||
}
|
||||
}
|
134
table-module/README.md
Normal file
134
table-module/README.md
Normal file
@ -0,0 +1,134 @@
|
||||
---
|
||||
layout: pattern
|
||||
title: Table Module
|
||||
folder: table-module
|
||||
permalink: /patterns/table-module/
|
||||
categories: Structural
|
||||
tags:
|
||||
- Data access
|
||||
---
|
||||
## Intent
|
||||
Table Module organizes domain logic with one class per table in the database, and a single instance of a class contains the various procedures that will act on the data.
|
||||
|
||||
## Explanation
|
||||
|
||||
Real world example
|
||||
|
||||
> When dealing with a user system, we need some operations on the user table. We can use the table module pattern in this scenario. We can create a class named UserTableModule and initialize a instance of that class to handle the business logic for all rows in the user table.
|
||||
|
||||
In plain words
|
||||
|
||||
> A single instance that handles the business logic for all rows in a database table or view.
|
||||
|
||||
Programmatic Example
|
||||
|
||||
In the example of the user system, we need to deal with the domain logic of user login and user registration. We can use the table module pattern and create an instance of the class `UserTableModule` to handle the business logic for all rows in the user table.
|
||||
|
||||
Here is the basic `User` entity.
|
||||
|
||||
```java
|
||||
@Setter
|
||||
@Getter
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
@AllArgsConstructor
|
||||
public class User {
|
||||
private int id;
|
||||
private String username;
|
||||
private String password;
|
||||
}
|
||||
```
|
||||
|
||||
Here is the `UserTableModule` class.
|
||||
|
||||
```java
|
||||
public class UserTableModule {
|
||||
private final DataSource dataSource;
|
||||
private Connection connection = null;
|
||||
private ResultSet resultSet = null;
|
||||
private PreparedStatement preparedStatement = null;
|
||||
|
||||
public UserTableModule(final DataSource userDataSource) {
|
||||
this.dataSource = userDataSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Login using username and password.
|
||||
*
|
||||
* @param username the username of a user
|
||||
* @param password the password of a user
|
||||
* @return the execution result of the method
|
||||
* @throws SQLException if any error
|
||||
*/
|
||||
public int login(final String username, final String password) throws SQLException {
|
||||
// Method implementation.
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new user.
|
||||
*
|
||||
* @param user a user instance
|
||||
* @return the execution result of the method
|
||||
* @throws SQLException if any error
|
||||
*/
|
||||
public int registerUser(final User user) throws SQLException {
|
||||
// Method implementation.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In the class `App`, we use an instance of the `UserTableModule` to handle user login and registration.
|
||||
|
||||
```java
|
||||
// Create data source and create the user table.
|
||||
final var dataSource = createDataSource();
|
||||
createSchema(dataSource);
|
||||
userTableModule = new UserTableModule(dataSource);
|
||||
|
||||
//Initialize two users.
|
||||
var user1 = new User(1, "123456", "123456");
|
||||
var user2 = new User(2, "test", "password");
|
||||
|
||||
//Login and register using the instance of userTableModule.
|
||||
userTableModule.registerUser(user1);
|
||||
userTableModule.login(user1.getUsername(), user1.getPassword());
|
||||
userTableModule.login(user2.getUsername(), user2.getPassword());
|
||||
userTableModule.registerUser(user2);
|
||||
userTableModule.login(user2.getUsername(), user2.getPassword());
|
||||
|
||||
deleteSchema(dataSource);
|
||||
```
|
||||
|
||||
The program output:
|
||||
|
||||
```java
|
||||
12:22:13.095 [main] INFO com.iluwatar.tablemodule.UserTableModule - Register successfully!
|
||||
12:22:13.117 [main] INFO com.iluwatar.tablemodule.UserTableModule - Login successfully!
|
||||
12:22:13.128 [main] INFO com.iluwatar.tablemodule.UserTableModule - Fail to login!
|
||||
12:22:13.136 [main] INFO com.iluwatar.tablemodule.UserTableModule - Register successfully!
|
||||
12:22:13.144 [main] INFO com.iluwatar.tablemodule.UserTableModule - Login successfully!
|
||||
```
|
||||
|
||||
## Class diagram
|
||||
|
||||

|
||||
|
||||
## Applicability
|
||||
|
||||
Use the Table Module Pattern when
|
||||
|
||||
- Domain logic is simple and data is in tabular form.
|
||||
- The application only uses a few shared common table-oriented data structures.
|
||||
|
||||
## Related patterns
|
||||
|
||||
- [Transaction Script](https://java-design-patterns.com/patterns/transaction-script/)
|
||||
|
||||
- Domain Model
|
||||
|
||||
## Credits
|
||||
|
||||
* [Table Module Pattern](http://wiki3.cosc.canterbury.ac.nz/index.php/Table_module_pattern)
|
||||
* [Patterns of Enterprise Application Architecture](https://www.amazon.com/gp/product/0321127420/ref=as_li_qf_asin_il_tl?ie=UTF8&tag=javadesignpat-20&creative=9325&linkCode=as2&creativeASIN=0321127420&linkId=18acc13ba60d66690009505577c45c04)
|
||||
* [Architecture patterns: domain model and friends](https://inviqa.com/blog/architecture-patterns-domain-model-and-friends)
|
BIN
table-module/etc/table-module.urm.png
Normal file
BIN
table-module/etc/table-module.urm.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 76 KiB |
38
table-module/etc/table-module.urm.puml
Normal file
38
table-module/etc/table-module.urm.puml
Normal file
@ -0,0 +1,38 @@
|
||||
@startuml
|
||||
package com.iluwatar.tablemodule {
|
||||
class App {
|
||||
- DB_URL : String {static}
|
||||
- LOGGER : Logger {static}
|
||||
- App()
|
||||
- createDataSource() : DataSource {static}
|
||||
- createSchema(dataSource : DataSource) {static}
|
||||
- deleteSchema(dataSource : DataSource) {static}
|
||||
+ main(args : String[]) {static}
|
||||
}
|
||||
class User {
|
||||
- id : int
|
||||
- password : String
|
||||
- username : String
|
||||
+ User(id : int, username : String, password : String)
|
||||
# canEqual(other : Object) : boolean
|
||||
+ equals(o : Object) : boolean
|
||||
+ getId() : int
|
||||
+ getPassword() : String
|
||||
+ getUsername() : String
|
||||
+ hashCode() : int
|
||||
+ setId(id : int)
|
||||
+ setPassword(password : String)
|
||||
+ setUsername(username : String)
|
||||
+ toString() : String
|
||||
}
|
||||
class UserTableModule {
|
||||
+ CREATE_SCHEMA_SQL : String {static}
|
||||
+ DELETE_SCHEMA_SQL : String {static}
|
||||
- LOGGER : Logger {static}
|
||||
- dataSource : DataSource
|
||||
+ UserTableModule(userDataSource : DataSource)
|
||||
+ login(username : String, password : String) : int
|
||||
+ registerUser(user : User) : int
|
||||
}
|
||||
}
|
||||
@enduml
|
73
table-module/pom.xml
Normal file
73
table-module/pom.xml
Normal file
@ -0,0 +1,73 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
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 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>java-design-patterns</artifactId>
|
||||
<groupId>com.iluwatar</groupId>
|
||||
<version>1.25.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>table-module</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifest>
|
||||
<mainClass>com.iluwatar.tablemodule.App</mainClass>
|
||||
</manifest>
|
||||
</archive>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
81
table-module/src/main/java/com/iluwatar/tablemodule/App.java
Normal file
81
table-module/src/main/java/com/iluwatar/tablemodule/App.java
Normal file
@ -0,0 +1,81 @@
|
||||
package com.iluwatar.tablemodule;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.h2.jdbcx.JdbcDataSource;
|
||||
|
||||
|
||||
/**
|
||||
* Table Module pattern is a domain logic pattern.
|
||||
* In Table Module a single class encapsulates all the domain logic for all
|
||||
* records stored in a table or view. It's important to note that there is no
|
||||
* translation of data between objects and rows, as it happens in Domain Model,
|
||||
* hence implementation is relatively simple when compared to the Domain
|
||||
* Model pattern.
|
||||
*
|
||||
* <p>In this example we will use the Table Module pattern to implement register
|
||||
* and login methods for the records stored in the user table. The main
|
||||
* method will initialise an instance of {@link UserTableModule} and use it to
|
||||
* handle the domain logic for the user table.</p>
|
||||
*/
|
||||
@Slf4j
|
||||
public final class App {
|
||||
private static final String DB_URL = "jdbc:h2:~/test";
|
||||
|
||||
/**
|
||||
* Private constructor.
|
||||
*/
|
||||
private App() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Program entry point.
|
||||
*
|
||||
* @param args command line args.
|
||||
* @throws SQLException if any error occurs.
|
||||
*/
|
||||
public static void main(final String[] args) throws SQLException {
|
||||
// Create data source and create the user table.
|
||||
final var dataSource = createDataSource();
|
||||
createSchema(dataSource);
|
||||
var userTableModule = new UserTableModule(dataSource);
|
||||
|
||||
// Initialize two users.
|
||||
var user1 = new User(1, "123456", "123456");
|
||||
var user2 = new User(2, "test", "password");
|
||||
|
||||
// Login and register using the instance of userTableModule.
|
||||
userTableModule.registerUser(user1);
|
||||
userTableModule.login(user1.getUsername(), user1.getPassword());
|
||||
userTableModule.login(user2.getUsername(), user2.getPassword());
|
||||
userTableModule.registerUser(user2);
|
||||
userTableModule.login(user2.getUsername(), user2.getPassword());
|
||||
|
||||
deleteSchema(dataSource);
|
||||
}
|
||||
|
||||
private static void deleteSchema(final DataSource dataSource)
|
||||
throws SQLException {
|
||||
try (var connection = dataSource.getConnection();
|
||||
var statement = connection.createStatement()) {
|
||||
statement.execute(UserTableModule.DELETE_SCHEMA_SQL);
|
||||
}
|
||||
}
|
||||
|
||||
private static void createSchema(final DataSource dataSource)
|
||||
throws SQLException {
|
||||
try (var connection = dataSource.getConnection();
|
||||
var statement = connection.createStatement()) {
|
||||
statement.execute(UserTableModule.CREATE_SCHEMA_SQL);
|
||||
}
|
||||
}
|
||||
|
||||
private static DataSource createDataSource() {
|
||||
var dataSource = new JdbcDataSource();
|
||||
dataSource.setURL(DB_URL);
|
||||
return dataSource;
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package com.iluwatar.tablemodule;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
|
||||
/**
|
||||
* A user POJO that represents the data that will be read from the data source.
|
||||
*/
|
||||
@Setter
|
||||
@Getter
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
@AllArgsConstructor
|
||||
public class User {
|
||||
private int id;
|
||||
private String username;
|
||||
private String password;
|
||||
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
package com.iluwatar.tablemodule;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
|
||||
/**
|
||||
* This class organizes domain logic with the user table in the
|
||||
* database. A single instance of this class contains the various
|
||||
* procedures that will act on the data.
|
||||
*/
|
||||
@Slf4j
|
||||
public class UserTableModule {
|
||||
/**
|
||||
* Public element for creating schema.
|
||||
*/
|
||||
public static final String CREATE_SCHEMA_SQL =
|
||||
"CREATE TABLE IF NOT EXISTS USERS (ID NUMBER, USERNAME VARCHAR(30) "
|
||||
+ "UNIQUE,PASSWORD VARCHAR(30))";
|
||||
/**
|
||||
* Public element for deleting schema.
|
||||
*/
|
||||
public static final String DELETE_SCHEMA_SQL = "DROP TABLE USERS IF EXISTS";
|
||||
private final DataSource dataSource;
|
||||
|
||||
|
||||
/**
|
||||
* Public constructor.
|
||||
*
|
||||
* @param userDataSource the data source in the database
|
||||
*/
|
||||
public UserTableModule(final DataSource userDataSource) {
|
||||
this.dataSource = userDataSource;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Login using username and password.
|
||||
*
|
||||
* @param username the username of a user
|
||||
* @param password the password of a user
|
||||
* @return the execution result of the method
|
||||
* @throws SQLException if any error
|
||||
*/
|
||||
public int login(final String username, final String password)
|
||||
throws SQLException {
|
||||
var sql = "select count(*) from USERS where username=? and password=?";
|
||||
ResultSet resultSet = null;
|
||||
try (var connection = dataSource.getConnection();
|
||||
var preparedStatement =
|
||||
connection.prepareStatement(sql)
|
||||
) {
|
||||
var result = 0;
|
||||
preparedStatement.setString(1, username);
|
||||
preparedStatement.setString(2, password);
|
||||
resultSet = preparedStatement.executeQuery();
|
||||
while (resultSet.next()) {
|
||||
result = resultSet.getInt(1);
|
||||
}
|
||||
if (result == 1) {
|
||||
LOGGER.info("Login successfully!");
|
||||
} else {
|
||||
LOGGER.info("Fail to login!");
|
||||
}
|
||||
return result;
|
||||
} finally {
|
||||
if (resultSet != null) {
|
||||
resultSet.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new user.
|
||||
*
|
||||
* @param user a user instance
|
||||
* @return the execution result of the method
|
||||
* @throws SQLException if any error
|
||||
*/
|
||||
public int registerUser(final User user) throws SQLException {
|
||||
var sql = "insert into USERS (username, password) values (?,?)";
|
||||
try (var connection = dataSource.getConnection();
|
||||
var preparedStatement =
|
||||
connection.prepareStatement(sql)
|
||||
) {
|
||||
preparedStatement.setString(1, user.getUsername());
|
||||
preparedStatement.setString(2, user.getPassword());
|
||||
var result = preparedStatement.executeUpdate();
|
||||
LOGGER.info("Register successfully!");
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package com.iluwatar.tablemodule;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
|
||||
/**
|
||||
* Tests that the table module example runs without errors.
|
||||
*/
|
||||
class AppTest {
|
||||
|
||||
@Test
|
||||
void shouldExecuteWithoutException() {
|
||||
assertDoesNotThrow(() -> App.main(new String[]{}));
|
||||
}
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
package com.iluwatar.tablemodule;
|
||||
|
||||
import org.h2.jdbcx.JdbcDataSource;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.SQLException;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
class UserTableModuleTest {
|
||||
private static final String DB_URL = "jdbc:h2:~/test";
|
||||
|
||||
private static DataSource createDataSource() {
|
||||
var dataSource = new JdbcDataSource();
|
||||
dataSource.setURL(DB_URL);
|
||||
return dataSource;
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws SQLException {
|
||||
try (var connection = DriverManager.getConnection(DB_URL);
|
||||
var statement = connection.createStatement()) {
|
||||
statement.execute(UserTableModule.DELETE_SCHEMA_SQL);
|
||||
statement.execute(UserTableModule.CREATE_SCHEMA_SQL);
|
||||
}
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() throws SQLException {
|
||||
try (var connection = DriverManager.getConnection(DB_URL);
|
||||
var statement = connection.createStatement()) {
|
||||
statement.execute(UserTableModule.DELETE_SCHEMA_SQL);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void loginShouldFail() throws SQLException {
|
||||
var dataSource = createDataSource();
|
||||
var userTableModule = new UserTableModule(dataSource);
|
||||
var user = new User(1, "123456", "123456");
|
||||
assertEquals(0, userTableModule.login(user.getUsername(),
|
||||
user.getPassword()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void loginShouldSucceed() throws SQLException {
|
||||
var dataSource = createDataSource();
|
||||
var userTableModule = new UserTableModule(dataSource);
|
||||
var user = new User(1, "123456", "123456");
|
||||
userTableModule.registerUser(user);
|
||||
assertEquals(1, userTableModule.login(user.getUsername(),
|
||||
user.getPassword()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void registerShouldFail() throws SQLException {
|
||||
var dataSource = createDataSource();
|
||||
var userTableModule = new UserTableModule(dataSource);
|
||||
var user = new User(1, "123456", "123456");
|
||||
userTableModule.registerUser(user);
|
||||
assertThrows(SQLException.class, () -> {
|
||||
userTableModule.registerUser(user);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void registerShouldSucceed() throws SQLException {
|
||||
var dataSource = createDataSource();
|
||||
var userTableModule = new UserTableModule(dataSource);
|
||||
var user = new User(1, "123456", "123456");
|
||||
assertEquals(1, userTableModule.registerUser(user));
|
||||
}
|
||||
}
|
@ -0,0 +1,131 @@
|
||||
package com.iluwatar.tablemodule;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class UserTest {
|
||||
@Test
|
||||
void testCanEqual() {
|
||||
assertFalse((new User(1, "janedoe", "iloveyou"))
|
||||
.canEqual("Other"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCanEqual2() {
|
||||
var user = new User(1, "janedoe", "iloveyou");
|
||||
assertTrue(user.canEqual(new User(1, "janedoe",
|
||||
"iloveyou")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEquals1() {
|
||||
var user = new User(1, "janedoe", "iloveyou");
|
||||
assertNotEquals("42", user);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEquals2() {
|
||||
var user = new User(1, "janedoe", "iloveyou");
|
||||
assertEquals(user, new User(1, "janedoe",
|
||||
"iloveyou"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEquals3() {
|
||||
var user = new User(123, "janedoe", "iloveyou");
|
||||
assertNotEquals(user, new User(1, "janedoe",
|
||||
"iloveyou"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEquals4() {
|
||||
var user = new User(1, null, "iloveyou");
|
||||
assertNotEquals(user, new User(1, "janedoe",
|
||||
"iloveyou"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEquals5() {
|
||||
var user = new User(1, "iloveyou", "iloveyou");
|
||||
assertNotEquals(user, new User(1, "janedoe",
|
||||
"iloveyou"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEquals6() {
|
||||
var user = new User(1, "janedoe", "janedoe");
|
||||
assertNotEquals(user, new User(1, "janedoe",
|
||||
"iloveyou"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEquals7() {
|
||||
var user = new User(1, "janedoe", null);
|
||||
assertNotEquals(user, new User(1, "janedoe",
|
||||
"iloveyou"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEquals8() {
|
||||
var user = new User(1, null, "iloveyou");
|
||||
assertEquals(user, new User(1, null,
|
||||
"iloveyou"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEquals9() {
|
||||
var user = new User(1, "janedoe", null);
|
||||
assertEquals(user, new User(1, "janedoe",
|
||||
null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHashCode1() {
|
||||
assertEquals(-1758941372, (new User(1, "janedoe",
|
||||
"iloveyou")).hashCode());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHashCode2() {
|
||||
assertEquals(-1332207447, (new User(1, null,
|
||||
"iloveyou")).hashCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHashCode3() {
|
||||
assertEquals(-426522485, (new User(1, "janedoe",
|
||||
null)).hashCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetId() {
|
||||
var user = new User(1, "janedoe", "iloveyou");
|
||||
user.setId(2);
|
||||
assertEquals(2, user.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetPassword() {
|
||||
var user = new User(1, "janedoe", "tmp");
|
||||
user.setPassword("iloveyou");
|
||||
assertEquals("iloveyou", user.getPassword());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetUsername() {
|
||||
var user = new User(1, "tmp", "iloveyou");
|
||||
user.setUsername("janedoe");
|
||||
assertEquals("janedoe", user.getUsername());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testToString() {
|
||||
var user = new User(1, "janedoe", "iloveyou");
|
||||
assertEquals(String.format("User(id=%s, username=%s, password=%s)",
|
||||
user.getId(), user.getUsername(), user.getPassword()),
|
||||
user.toString());
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user