diff --git a/command/README.md b/command/README.md index 28c7e88e9..87dd48824 100644 --- a/command/README.md +++ b/command/README.md @@ -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 diff --git a/command/etc/command.png b/command/etc/command.png index 5564b0ec5..0f026464e 100644 Binary files a/command/etc/command.png and b/command/etc/command.png differ diff --git a/command/etc/command.ucls b/command/etc/command.ucls index f0e0857d2..afc7e1762 100644 --- a/command/etc/command.ucls +++ b/command/etc/command.ucls @@ -1,116 +1,89 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - \ No newline at end of file + diff --git a/command/etc/command.urm.puml b/command/etc/command.urm.puml index c9c172226..f85949c56 100644 --- a/command/etc/command.urm.puml +++ b/command/etc/command.urm.puml @@ -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 - - undoStack : Deque + - redoStack : Deque + - undoStack : Deque + 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 diff --git a/command/src/main/java/com/iluwatar/command/App.java b/command/src/main/java/com/iluwatar/command/App.java index b4e54fd97..1e4e78758 100644 --- a/command/src/main/java/com/iluwatar/command/App.java +++ b/command/src/main/java/com/iluwatar/command/App.java @@ -32,10 +32,10 @@ package com.iluwatar.command; * 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 + * does bookkeeping about the command execution. The invoker does not know anything about a is + * executed. 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. + * points. To execute a command, it passes a reference of the command object to the invoker object. * *

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 +54,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(); diff --git a/command/src/main/java/com/iluwatar/command/Command.java b/command/src/main/java/com/iluwatar/command/Command.java deleted file mode 100644 index 83010f160..000000000 --- a/command/src/main/java/com/iluwatar/command/Command.java +++ /dev/null @@ -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(); -} diff --git a/command/src/main/java/com/iluwatar/command/Goblin.java b/command/src/main/java/com/iluwatar/command/Goblin.java index 72ddc43b5..791bd94e3 100644 --- a/command/src/main/java/com/iluwatar/command/Goblin.java +++ b/command/src/main/java/com/iluwatar/command/Goblin.java @@ -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); @@ -38,4 +43,20 @@ public class Goblin extends Target { return "Goblin"; } + /** + * changeSize. + */ + public void changeSize() { + var oldSize = getSize() == Size.NORMAL ? Size.SMALL : Size.NORMAL; + setSize(oldSize); + } + + /** + * changeVisibility. + */ + public void changeVisibility() { + var visible = getVisibility() == Visibility.INVISIBLE + ? Visibility.VISIBLE : Visibility.INVISIBLE; + setVisibility(visible); + } } diff --git a/command/src/main/java/com/iluwatar/command/InvisibilitySpell.java b/command/src/main/java/com/iluwatar/command/InvisibilitySpell.java deleted file mode 100644 index 33e053cc2..000000000 --- a/command/src/main/java/com/iluwatar/command/InvisibilitySpell.java +++ /dev/null @@ -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"; - } -} diff --git a/command/src/main/java/com/iluwatar/command/ShrinkSpell.java b/command/src/main/java/com/iluwatar/command/ShrinkSpell.java deleted file mode 100644 index 3f21fc7c1..000000000 --- a/command/src/main/java/com/iluwatar/command/ShrinkSpell.java +++ /dev/null @@ -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"; - } -} diff --git a/command/src/main/java/com/iluwatar/command/Wizard.java b/command/src/main/java/com/iluwatar/command/Wizard.java index dd469d3c0..b4568eebf 100644 --- a/command/src/main/java/com/iluwatar/command/Wizard.java +++ b/command/src/main/java/com/iluwatar/command/Wizard.java @@ -35,20 +35,18 @@ public class Wizard { private static final Logger LOGGER = LoggerFactory.getLogger(Wizard.class); - private final Deque undoStack = new LinkedList<>(); - private final Deque redoStack = new LinkedList<>(); + private final Deque undoStack = new LinkedList<>(); + private final Deque 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(); } } diff --git a/command/src/test/java/com/iluwatar/command/CommandTest.java b/command/src/test/java/com/iluwatar/command/CommandTest.java index 81f556010..76b8c9e58 100644 --- a/command/src/test/java/com/iluwatar/command/CommandTest.java +++ b/command/src/test/java/com/iluwatar/command/CommandTest.java @@ -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();