Merge pull request #1531 from ravening/command-functional

Refactor the command pattern to use lambda functions
This commit is contained in:
Ilkka Seppälä 2020-11-08 19:33:48 +02:00 committed by GitHub
commit 7c0fdad5a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 208 additions and 438 deletions

View File

@ -14,14 +14,14 @@ Action, Transaction
## Intent
Encapsulate a request as an object, thereby letting you parameterize clients with different
Encapsulate a request as an object, thereby letting you parameterize clients with different
requests, queue or log requests, and support undoable operations.
## Explanation
Real world example
> There is a wizard casting spells on a goblin. The spells are executed on the goblin one by one.
> The first spell shrinks the goblin and the second makes him invisible. Then the wizard reverses
> There is a wizard casting spells on a goblin. The spells are executed on the goblin one by one.
> The first spell shrinks the goblin and the second makes him invisible. Then the wizard reverses
> the spells one by one. Each spell here is a command object that can be undone.
In plain words
@ -30,8 +30,8 @@ In plain words
Wikipedia says
> In object-oriented programming, the command pattern is a behavioral design pattern in which an
> object is used to encapsulate all information needed to perform an action or trigger an event at
> In object-oriented programming, the command pattern is a behavioral design pattern in which an
> object is used to encapsulate all information needed to perform an action or trigger an event at
> a later time.
**Programmatic Example**
@ -48,18 +48,16 @@ public class Wizard {
public Wizard() {}
public void castSpell(Command command, Target target) {
LOGGER.info("{} casts {} at {}", this, command, target);
command.execute(target);
undoStack.offerLast(command);
public void castSpell(Runnable runnable) {
runnable.run();
undoStack.offerLast(runnable);
}
public void undoLastSpell() {
if (!undoStack.isEmpty()) {
var previousSpell = undoStack.pollLast();
redoStack.offerLast(previousSpell);
LOGGER.info("{} undoes {}", this, previousSpell);
previousSpell.undo();
previousSpell.run();
}
}
@ -67,8 +65,7 @@ public class Wizard {
if (!redoStack.isEmpty()) {
var previousSpell = redoStack.pollLast();
undoStack.offerLast(previousSpell);
LOGGER.info("{} redoes {}", this, previousSpell);
previousSpell.redo();
previousSpell.run();
}
}
@ -79,84 +76,7 @@ public class Wizard {
}
```
Next we present the spell hierarchy.
```java
public interface Command {
void execute(Target target);
void undo();
void redo();
String toString();
}
public class InvisibilitySpell implements Command {
private Target target;
@Override
public void execute(Target target) {
target.setVisibility(Visibility.INVISIBLE);
this.target = target;
}
@Override
public void undo() {
if (target != null) {
target.setVisibility(Visibility.VISIBLE);
}
}
@Override
public void redo() {
if (target != null) {
target.setVisibility(Visibility.INVISIBLE);
}
}
@Override
public String toString() {
return "Invisibility spell";
}
}
public class ShrinkSpell implements Command {
private Size oldSize;
private Target target;
@Override
public void execute(Target target) {
oldSize = target.getSize();
target.setSize(Size.SMALL);
this.target = target;
}
@Override
public void undo() {
if (oldSize != null && target != null) {
var temp = target.getSize();
target.setSize(oldSize);
oldSize = temp;
}
}
@Override
public void redo() {
undo();
}
@Override
public String toString() {
return "Shrink spell";
}
}
```
Finally, we have the goblin who's the target of the spells.
Next, we have the goblin who's the target of the spells.
```java
public abstract class Target {
@ -203,33 +123,73 @@ public class Goblin extends Target {
return "Goblin";
}
public void changeSize() {
var oldSize = getSize() == Size.NORMAL ? Size.SMALL : Size.NORMAL;
setSize(oldSize);
}
public void changeVisibility() {
var visible = getVisibility() == Visibility.INVISIBLE
? Visibility.VISIBLE : Visibility.INVISIBLE;
setVisibility(visible);
}
}
```
Finally we have the wizard in main function who casts spell
```java
public static void main(String[] args) {
var wizard = new Wizard();
var goblin = new Goblin();
// casts shrink/unshrink spell
wizard.castSpell(goblin::changeSize);
// casts visible/invisible spell
wizard.castSpell(goblin::changeVisibility);
// undo and redo casts
wizard.undoLastSpell();
wizard.redoLastSpell();
```
Here's the whole example in action.
```java
var wizard = new Wizard();
var goblin = new Goblin();
goblin.printStatus();
wizard.castSpell(new ShrinkSpell(), goblin);
wizard.castSpell(goblin::changeSize);
goblin.printStatus();
wizard.castSpell(new InvisibilitySpell(), goblin);
wizard.castSpell(goblin::changeVisibility);
goblin.printStatus();
wizard.undoLastSpell();
goblin.printStatus();
wizard.undoLastSpell();
goblin.printStatus();
wizard.redoLastSpell();
goblin.printStatus();
wizard.redoLastSpell();
goblin.printStatus();
```
Here's the program output:
```java
// Goblin, [size=normal] [visibility=visible]
// Wizard casts Shrink spell at Goblin
// Goblin, [size=small] [visibility=visible]
// Wizard casts Invisibility spell at Goblin
// Goblin, [size=small] [visibility=invisible]
// Wizard undoes Invisibility spell
// Goblin, [size=small] [visibility=visible]
Goblin, [size=normal] [visibility=visible]
Goblin, [size=small] [visibility=visible]
Goblin, [size=small] [visibility=invisible]
Goblin, [size=small] [visibility=visible]
Goblin, [size=normal] [visibility=visible]
Goblin, [size=small] [visibility=visible]
Goblin, [size=small] [visibility=invisible]
```
## Class diagram
@ -240,26 +200,26 @@ Here's the program output:
Use the Command pattern when you want to:
* Parameterize objects by an action to perform. You can express such parameterization in a
procedural language with a callback function, that is, a function that's registered somewhere to be
* Parameterize objects by an action to perform. You can express such parameterization in a
procedural language with a callback function, that is, a function that's registered somewhere to be
called at a later point. Commands are an object-oriented replacement for callbacks.
* Specify, queue, and execute requests at different times. A Command object can have a lifetime
independent of the original request. If the receiver of a request can be represented in an address
space-independent way, then you can transfer a command object for the request to a different process
* Specify, queue, and execute requests at different times. A Command object can have a lifetime
independent of the original request. If the receiver of a request can be represented in an address
space-independent way, then you can transfer a command object for the request to a different process
and fulfill the request there.
* Support undo. The Command's execute operation can store state for reversing its effects in the
command itself. The Command interface must have an added un-execute operation that reverses the
effects of a previous call to execute. The executed commands are stored in a history list.
Unlimited-level undo and redo is achieved by traversing this list backwards and forwards calling
* Support undo. The Command's execute operation can store state for reversing its effects in the
command itself. The Command interface must have an added un-execute operation that reverses the
effects of a previous call to execute. The executed commands are stored in a history list.
Unlimited-level undo and redo is achieved by traversing this list backwards and forwards calling
un-execute and execute, respectively.
* Support logging changes so that they can be reapplied in case of a system crash. By augmenting the
Command interface with load and store operations, you can keep a persistent log of changes.
Recovering from a crash involves reloading logged commands from disk and re-executing them with
* Support logging changes so that they can be reapplied in case of a system crash. By augmenting the
Command interface with load and store operations, you can keep a persistent log of changes.
Recovering from a crash involves reloading logged commands from disk and re-executing them with
the execute operation.
* Structure a system around high-level operations build on primitive operations. Such a structure is
common in information systems that support transactions. A transaction encapsulates a set of changes
to data. The Command pattern offers a way to model transactions. Commands have a common interface,
letting you invoke all transactions the same way. The pattern also makes it easy to extend the
* Structure a system around high-level operations build on primitive operations. Such a structure is
common in information systems that support transactions. A transaction encapsulates a set of changes
to data. The Command pattern offers a way to model transactions. Commands have a common interface,
letting you invoke all transactions the same way. The pattern also makes it easy to extend the
system with new transactions.
## Typical Use Case

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 50 KiB

View File

@ -1,116 +1,89 @@
<?xml version="1.0" encoding="UTF-8"?>
<class-diagram version="1.1.8" icons="true" automaticImage="PNG" always-add-relationships="false" generalizations="true"
realizations="true" associations="true" dependencies="false" nesting-relationships="true">
<class id="1" language="java" name="com.iluwatar.command.ShrinkSpell" project="command"
file="/command/src/main/java/com/iluwatar/command/ShrinkSpell.java" binary="false" corner="BOTTOM_RIGHT">
<position height="178" width="141" x="-30" y="681"/>
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
sort-features="false" accessors="true" visibility="true">
<attributes public="true" package="true" protected="true" private="true" static="true"/>
<operations public="true" package="true" protected="true" private="true" static="true"/>
</display>
</class>
<class id="2" language="java" name="com.iluwatar.command.Goblin" project="command"
file="/command/src/main/java/com/iluwatar/command/Goblin.java" binary="false" corner="BOTTOM_RIGHT">
<position height="-1" width="-1" x="129" y="1223"/>
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
sort-features="false" accessors="true" visibility="true">
<attributes public="true" package="true" protected="true" private="true" static="true"/>
<operations public="true" package="true" protected="true" private="true" static="true"/>
</display>
</class>
<class id="3" language="java" name="com.iluwatar.command.Wizard" project="command"
file="/command/src/main/java/com/iluwatar/command/Wizard.java" binary="false" corner="BOTTOM_RIGHT">
<position height="-1" width="-1" x="129" y="362"/>
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
sort-features="false" accessors="true" visibility="true">
<attributes public="true" package="true" protected="true" private="true" static="true"/>
<operations public="true" package="true" protected="true" private="true" static="true"/>
</display>
</class>
<class id="4" language="java" name="com.iluwatar.command.Command" project="command"
file="/command/src/main/java/com/iluwatar/command/Command.java" binary="false" corner="BOTTOM_RIGHT">
<position height="-1" width="-1" x="129" y="561"/>
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
sort-features="false" accessors="true" visibility="true">
<attributes public="true" package="true" protected="true" private="true" static="true"/>
<operations public="true" package="true" protected="true" private="true" static="true"/>
</display>
</class>
<class id="5" language="java" name="com.iluwatar.command.InvisibilitySpell" project="command"
file="/command/src/main/java/com/iluwatar/command/InvisibilitySpell.java" binary="false" corner="BOTTOM_RIGHT">
<position height="160" width="141" x="151" y="681"/>
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
sort-features="false" accessors="true" visibility="true">
<attributes public="true" package="true" protected="true" private="true" static="true"/>
<operations public="true" package="true" protected="true" private="true" static="true"/>
</display>
</class>
<class id="6" language="java" name="com.iluwatar.command.Target" project="command"
file="/command/src/main/java/com/iluwatar/command/Target.java" binary="false" corner="BOTTOM_RIGHT">
<position height="-1" width="-1" x="129" y="1014"/>
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
sort-features="false" accessors="true" visibility="true">
<attributes public="true" package="true" protected="true" private="true" static="true"/>
<operations public="true" package="true" protected="true" private="true" static="true"/>
</display>
</class>
<association id="7">
<end type="SOURCE" refId="3" navigable="false">
<attribute id="8" name="redoStack">
<position height="20" width="67" x="140" y="451"/>
</attribute>
<multiplicity id="9" minimum="0" maximum="2147483647">
<position height="18" width="25" x="221" y="452"/>
</multiplicity>
</end>
<end type="TARGET" refId="4" navigable="true"/>
<display labels="true" multiplicity="true"/>
</association>
<generalization id="10">
<end type="SOURCE" refId="2"/>
<end type="TARGET" refId="6"/>
</generalization>
<association id="11">
<end type="SOURCE" refId="1" navigable="false">
<attribute id="12" name="target"/>
<multiplicity id="13" minimum="0" maximum="1"/>
</end>
<end type="TARGET" refId="6" navigable="true"/>
<display labels="true" multiplicity="true"/>
</association>
<generalization id="14">
<end type="SOURCE" refId="1"/>
<end type="TARGET" refId="4"/>
</generalization>
<association id="15">
<end type="SOURCE" refId="3" navigable="false">
<attribute id="16" name="undoStack">
<position height="20" width="70" x="-17" y="451"/>
</attribute>
<multiplicity id="17" minimum="0" maximum="2147483647">
<position height="18" width="25" x="60" y="452"/>
</multiplicity>
</end>
<end type="TARGET" refId="4" navigable="true"/>
<display labels="true" multiplicity="true"/>
</association>
<generalization id="18">
<end type="SOURCE" refId="5"/>
<end type="TARGET" refId="4"/>
</generalization>
<association id="19">
<end type="SOURCE" refId="5" navigable="false">
<attribute id="20" name="target"/>
<multiplicity id="21" minimum="0" maximum="1"/>
</end>
<end type="TARGET" refId="6" navigable="true"/>
<display labels="true" multiplicity="true"/>
</association>
<classifier-display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
sort-features="false" accessors="true" visibility="true">
<attributes public="true" package="true" protected="true" private="true" static="true"/>
<operations public="true" package="true" protected="true" private="true" static="true"/>
</classifier-display>
<class-diagram version="1.1.8" icons="true" automaticImage="PNG" always-add-relationships="false" generalizations="true"
realizations="true" associations="true" dependencies="false" nesting-relationships="true">
<class id="2" language="java" name="com.iluwatar.command.Goblin" project="command"
file="/command/src/main/java/com/iluwatar/command/Goblin.java" binary="false" corner="BOTTOM_RIGHT">
<position height="-1" width="-1" x="129" y="1223"/>
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
sort-features="false" accessors="true" visibility="true">
<attributes public="true" package="true" protected="true" private="true" static="true"/>
<operations public="true" package="true" protected="true" private="true" static="true"/>
</display>
</class>
<class id="3" language="java" name="com.iluwatar.command.Wizard" project="command"
file="/command/src/main/java/com/iluwatar/command/Wizard.java" binary="false" corner="BOTTOM_RIGHT">
<position height="-1" width="-1" x="129" y="362"/>
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
sort-features="false" accessors="true" visibility="true">
<attributes public="true" package="true" protected="true" private="true" static="true"/>
<operations public="true" package="true" protected="true" private="true" static="true"/>
</display>
</class>
<class id="6" language="java" name="com.iluwatar.command.Target" project="command"
file="/command/src/main/java/com/iluwatar/command/Target.java" binary="false" corner="BOTTOM_RIGHT">
<position height="-1" width="-1" x="129" y="1014"/>
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
sort-features="false" accessors="true" visibility="true">
<attributes public="true" package="true" protected="true" private="true" static="true"/>
<operations public="true" package="true" protected="true" private="true" static="true"/>
</display>
</class>
<association id="7">
<end type="SOURCE" refId="3" navigable="false">
<attribute id="8" name="redoStack">
<position height="20" width="67" x="140" y="451"/>
</attribute>
<multiplicity id="9" minimum="0" maximum="2147483647">
<position height="18" width="25" x="221" y="452"/>
</multiplicity>
</end>
<end type="TARGET" refId="4" navigable="true"/>
<display labels="true" multiplicity="true"/>
</association>
<generalization id="10">
<end type="SOURCE" refId="2"/>
<end type="TARGET" refId="6"/>
</generalization>
<association id="11">
<end type="SOURCE" refId="1" navigable="false">
<attribute id="12" name="target"/>
<multiplicity id="13" minimum="0" maximum="1"/>
</end>
<end type="TARGET" refId="6" navigable="true"/>
<display labels="true" multiplicity="true"/>
</association>
<generalization id="14">
<end type="SOURCE" refId="1"/>
<end type="TARGET" refId="4"/>
</generalization>
<association id="15">
<end type="SOURCE" refId="3" navigable="false">
<attribute id="16" name="undoStack">
<position height="20" width="70" x="-17" y="451"/>
</attribute>
<multiplicity id="17" minimum="0" maximum="2147483647">
<position height="18" width="25" x="60" y="452"/>
</multiplicity>
</end>
<end type="TARGET" refId="4" navigable="true"/>
<display labels="true" multiplicity="true"/>
</association>
<generalization id="18">
<end type="SOURCE" refId="5"/>
<end type="TARGET" refId="4"/>
</generalization>
<association id="19">
<end type="SOURCE" refId="5" navigable="false">
<attribute id="20" name="target"/>
<multiplicity id="21" minimum="0" maximum="1"/>
</end>
<end type="TARGET" refId="6" navigable="true"/>
<display labels="true" multiplicity="true"/>
</association>
<classifier-display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
sort-features="false" accessors="true" visibility="true">
<attributes public="true" package="true" protected="true" private="true" static="true"/>
<operations public="true" package="true" protected="true" private="true" static="true"/>
</classifier-display>
<association-display labels="true" multiplicity="true"/>
</class-diagram>
</class-diagram>

View File

@ -4,33 +4,11 @@ package com.iluwatar.command {
+ App()
+ main(args : String[]) {static}
}
interface Command {
+ Command()
+ execute(Target) {abstract}
+ redo() {abstract}
+ toString() : String {abstract}
+ undo() {abstract}
}
class Goblin {
+ Goblin()
+ toString() : String
}
class InvisibilitySpell {
- target : Target
+ InvisibilitySpell()
+ execute(target : Target)
+ redo()
+ toString() : String
+ undo()
}
class ShrinkSpell {
- oldSize : Size
- target : Target
+ ShrinkSpell()
+ execute(target : Target)
+ redo()
+ toString() : String
+ undo()
+ changeSize()
+ changeVisibility()
}
enum Size {
+ NORMAL {static}
@ -62,22 +40,19 @@ package com.iluwatar.command {
}
class Wizard {
- LOGGER : Logger {static}
- redoStack : Deque<Command>
- undoStack : Deque<Command>
- redoStack : Deque<Runnable>
- undoStack : Deque<Runnable>
+ Wizard()
+ castSpell(command : Command, target : Target)
+ castSpell(Runnable : runnable)
+ redoLastSpell()
+ toString() : String
+ undoLastSpell()
}
}
Target --> "-size" Size
Wizard --> "-undoStack" Command
ShrinkSpell --> "-oldSize" Size
InvisibilitySpell --> "-target" Target
ShrinkSpell --> "-target" Target
Wizard --> "-changeSize" Goblin
Wizard --> "-changeVisibility" Goblin
Target --> "-visibility" Visibility
Goblin --|> Target
InvisibilitySpell ..|> Command
ShrinkSpell ..|> Command
App --> "castSpell" Wizard
@enduml

View File

@ -30,12 +30,10 @@ package com.iluwatar.command;
*
* <p>Four terms always associated with the command pattern are command, receiver, invoker and
* client. A command object (spell) knows about the receiver (target) and invokes a method of the
* receiver. Values for parameters of the receiver method are stored in the command. The receiver
* then does the work. An invoker object (wizard) knows how to execute a command, and optionally
* does bookkeeping about the command execution. The invoker does not know anything about a concrete
* command, it knows only about command interface. Both an invoker object and several command
* objects are held by a client object (app). The client decides which commands to execute at which
* points. To execute a command, it passes the command object to the invoker object.
* receiver. An invoker object (wizard) receives a reference to the command to be executed and
* optionally does bookkeeping about the command execution. The invoker does not know anything
* about how the command is executed. The client decides which commands to execute at which
* points. To execute a command, it passes a reference of the function to the invoker object.
*
* <p>In other words, in this example the wizard casts spells on the goblin. The wizard keeps track
* of the previous spells cast, so it is easy to undo them. In addition, the wizard keeps track of
@ -54,10 +52,10 @@ public class App {
goblin.printStatus();
wizard.castSpell(new ShrinkSpell(), goblin);
wizard.castSpell(goblin::changeSize);
goblin.printStatus();
wizard.castSpell(new InvisibilitySpell(), goblin);
wizard.castSpell(goblin::changeVisibility);
goblin.printStatus();
wizard.undoLastSpell();

View File

@ -1,37 +0,0 @@
/*
* The MIT License
* Copyright © 2014-2019 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.command;
/**
* Interface for Commands.
*/
public interface Command {
void execute(Target target);
void undo();
void redo();
String toString();
}

View File

@ -23,11 +23,16 @@
package com.iluwatar.command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Goblin is the target of the spells.
*/
public class Goblin extends Target {
private static final Logger LOGGER = LoggerFactory.getLogger(Goblin.class);
public Goblin() {
setSize(Size.NORMAL);
setVisibility(Visibility.VISIBLE);
@ -37,5 +42,4 @@ public class Goblin extends Target {
public String toString() {
return "Goblin";
}
}

View File

@ -1,57 +0,0 @@
/*
* The MIT License
* Copyright © 2014-2019 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.command;
/**
* InvisibilitySpell is a concrete command.
*/
public class InvisibilitySpell implements Command {
private Target target;
@Override
public void execute(Target target) {
target.setVisibility(Visibility.INVISIBLE);
this.target = target;
}
@Override
public void undo() {
if (target != null) {
target.setVisibility(Visibility.VISIBLE);
}
}
@Override
public void redo() {
if (target != null) {
target.setVisibility(Visibility.INVISIBLE);
}
}
@Override
public String toString() {
return "Invisibility spell";
}
}

View File

@ -1,59 +0,0 @@
/*
* The MIT License
* Copyright © 2014-2019 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.command;
/**
* ShrinkSpell is a concrete command.
*/
public class ShrinkSpell implements Command {
private Size oldSize;
private Target target;
@Override
public void execute(Target target) {
oldSize = target.getSize();
target.setSize(Size.SMALL);
this.target = target;
}
@Override
public void undo() {
if (oldSize != null && target != null) {
var temp = target.getSize();
target.setSize(oldSize);
oldSize = temp;
}
}
@Override
public void redo() {
undo();
}
@Override
public String toString() {
return "Shrink spell";
}
}

View File

@ -62,4 +62,21 @@ public abstract class Target {
public void printStatus() {
LOGGER.info("{}, [size={}] [visibility={}]", this, getSize(), getVisibility());
}
/**
* Changes the size of the target.
*/
public void changeSize() {
var oldSize = getSize() == Size.NORMAL ? Size.SMALL : Size.NORMAL;
setSize(oldSize);
}
/**
* Changes the visibility of the target.
*/
public void changeVisibility() {
var visible = getVisibility() == Visibility.INVISIBLE
? Visibility.VISIBLE : Visibility.INVISIBLE;
setVisibility(visible);
}
}

View File

@ -35,20 +35,18 @@ public class Wizard {
private static final Logger LOGGER = LoggerFactory.getLogger(Wizard.class);
private final Deque<Command> undoStack = new LinkedList<>();
private final Deque<Command> redoStack = new LinkedList<>();
private final Deque<Runnable> undoStack = new LinkedList<>();
private final Deque<Runnable> redoStack = new LinkedList<>();
public Wizard() {
// comment to ignore sonar issue: LEVEL critical
}
/**
* Cast spell.
*/
public void castSpell(Command command, Target target) {
LOGGER.info("{} casts {} at {}", this, command, target);
command.execute(target);
undoStack.offerLast(command);
public void castSpell(Runnable runnable) {
runnable.run();
undoStack.offerLast(runnable);
}
/**
@ -58,8 +56,7 @@ public class Wizard {
if (!undoStack.isEmpty()) {
var previousSpell = undoStack.pollLast();
redoStack.offerLast(previousSpell);
LOGGER.info("{} undoes {}", this, previousSpell);
previousSpell.undo();
previousSpell.run();
}
}
@ -70,8 +67,7 @@ public class Wizard {
if (!redoStack.isEmpty()) {
var previousSpell = redoStack.pollLast();
undoStack.offerLast(previousSpell);
LOGGER.info("{} redoes {}", this, previousSpell);
previousSpell.redo();
previousSpell.run();
}
}

View File

@ -56,10 +56,10 @@ public class CommandTest {
var wizard = new Wizard();
var goblin = new Goblin();
wizard.castSpell(new ShrinkSpell(), goblin);
wizard.castSpell(goblin::changeSize);
verifyGoblin(goblin, GOBLIN, Size.SMALL, Visibility.VISIBLE);
wizard.castSpell(new InvisibilitySpell(), goblin);
wizard.castSpell(goblin::changeVisibility);
verifyGoblin(goblin, GOBLIN, Size.SMALL, Visibility.INVISIBLE);
wizard.undoLastSpell();