feature: #2151 Feature/mvi pattern (#2177)

* #2151 add module and pom.xml

* #2151 add Calculator Actions

* #2151 add remaining mvi pattern classes (actions already implemented)

* #2151 add Main and unit tests

* add README.md and class diagrams

* #2151 add module and pom.xml

* #2151 add Calculator Actions

* #2151 add remaining mvi pattern classes (actions already implemented)

* #2151 add Main and unit tests

* add README.md and class diagrams

* fixes for lint errors

* #2151 add module and pom.xml

* #2151 add Calculator Actions

* #2151 add remaining mvi pattern classes (actions already implemented)

* #2151 add Main and unit tests

* add README.md and class diagrams

* fixes for lint errors

* use Lombok @Data decorator and decouple View from ViewModel

* add comments and documentation

* fix checkstyle, the smart switch syntax was breaking checkstyle, so I had to change it back
This commit is contained in:
JanFidor
2023-01-21 10:45:07 +01:00
committed by GitHub
parent 6b0a90ee61
commit fb86ca1156
19 changed files with 943 additions and 0 deletions
@@ -0,0 +1,87 @@
/*
* This project is licensed under the MIT license.
* Module model-view-viewmodel is using ZK framework licensed under LGPL
* (see lgpl-3.0.txt).
*
* The MIT License
* Copyright © 2014-2022 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.model.view.intent;
/**
* Model-View-Intent is a pattern for implementing user interfaces.
* Its main advantage over MVVM which it closely mirrors is a
* minimal public api with which user events can be exposed to the ViewModel.
* In case of the MVI every event is exposed by using a single method
* with 1 argument which implements UserEvent interface.
* Specific parameters can be expressed as its parameters. In this case,
* we'll be using MVI to implement a simple calculator
* with +, -, /, * operations and the ability to set the variable.
* It's important to note, that every user action happens through the
* view, we never interact with the ViewModel directly.
*/
public final class App {
/**
* To avoid magic value lint error.
*/
private static final double RANDOM_VARIABLE = 10.0;
/**
* Program entry point.
*
* @param args command line args
*/
public static void main(final String[] args) {
// create model, view and controller
// initialize calculator view, output and variable = 0
var view = new CalculatorView(new CalculatorViewModel());
var variable1 = RANDOM_VARIABLE;
// calculator variable = RANDOM_VARIABLE -> 10.0
view.setVariable(variable1);
// add calculator variable to output -> calculator output = 10.0
view.add();
view.displayTotal(); // display output
variable1 = 2.0;
view.setVariable(variable1); // calculator variable = 2.0
// subtract calculator variable from output -> calculator output = 8
view.subtract();
// divide calculator output by variable -> calculator output = 4.0
view.divide();
// multiply calculator output by variable -> calculator output = 8.0
view.multiply();
view.displayTotal();
}
/**
* Avoid default constructor lint error.
*/
private App() {
}
}
@@ -0,0 +1,23 @@
package com.iluwatar.model.view.intent;
import lombok.Data;
import lombok.Getter;
/**
* Current state of calculator.
*/
@Data
public class CalculatorModel {
/**
* Current calculator variable used for operations.
**/
@Getter
private final Double variable;
/**
* Current calculator output -> is affected by operations.
**/
@Getter
private final Double output;
}
@@ -0,0 +1,74 @@
package com.iluwatar.model.view.intent;
import com.iluwatar.model.view.intent.actions.AdditionCalculatorAction;
import com.iluwatar.model.view.intent.actions.DivisionCalculatorAction;
import com.iluwatar.model.view.intent.actions.MultiplicationCalculatorAction;
import com.iluwatar.model.view.intent.actions.SetVariableCalculatorAction;
import com.iluwatar.model.view.intent.actions.SubtractionCalculatorAction;
import lombok.Data;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
/**
* Exposes changes to the state of calculator
* to {@link CalculatorViewModel} through
* {@link com.iluwatar.model.view.intent.actions.CalculatorAction}
* and displays its updated {@link CalculatorModel}.
*/
@Slf4j
@Data
public class CalculatorView {
/**
* View model param handling the operations.
*/
@Getter
private final CalculatorViewModel viewModel;
/**
* Display current view model output with logger.
*/
void displayTotal() {
LOGGER.info(
"Total value = {}",
viewModel.getCalculatorModel().getOutput().toString()
);
}
/**
* Handle addition action.
*/
void add() {
viewModel.handleAction(new AdditionCalculatorAction());
}
/**
* Handle subtraction action.
*/
void subtract() {
viewModel.handleAction(new SubtractionCalculatorAction());
}
/**
* Handle multiplication action.
*/
void multiply() {
viewModel.handleAction(new MultiplicationCalculatorAction());
}
/**
* Handle division action.
*/
void divide() {
viewModel.handleAction(new DivisionCalculatorAction());
}
/**
* Handle setting new variable action.
*
* @param value -> new calculator variable.
*/
void setVariable(final Double value) {
viewModel.handleAction(new SetVariableCalculatorAction(value));
}
}
@@ -0,0 +1,116 @@
package com.iluwatar.model.view.intent;
import com.iluwatar.model.view.intent.actions.AdditionCalculatorAction;
import com.iluwatar.model.view.intent.actions.CalculatorAction;
import com.iluwatar.model.view.intent.actions.DivisionCalculatorAction;
import com.iluwatar.model.view.intent.actions.MultiplicationCalculatorAction;
import com.iluwatar.model.view.intent.actions.SetVariableCalculatorAction;
import com.iluwatar.model.view.intent.actions.SubtractionCalculatorAction;
/**
* Handle transformations to {@link CalculatorModel}
* based on intercepted {@link CalculatorAction}.
*/
public final class CalculatorViewModel {
/**
* Current calculator model (can be changed).
*/
private CalculatorModel model =
new CalculatorModel(0.0, 0.0);
/**
* Handle calculator action.
*
* @param action -> transforms calculator model.
*/
void handleAction(final CalculatorAction action) {
switch (action.tag()) {
case AdditionCalculatorAction.TAG:
add();
break;
case SubtractionCalculatorAction.TAG:
subtract();
break;
case MultiplicationCalculatorAction.TAG:
multiply();
break;
case DivisionCalculatorAction.TAG:
divide();
break;
case SetVariableCalculatorAction.TAG:
SetVariableCalculatorAction setVariableAction =
(SetVariableCalculatorAction) action;
setVariable(setVariableAction.getVariable());
break;
default:
break;
}
}
/**
* Getter.
*
* @return current calculator model.
*/
public CalculatorModel getCalculatorModel() {
return model;
}
/**
* Set new calculator model variable.
*
* @param variable -> value of new calculator model variable.
*/
private void setVariable(final Double variable) {
model = new CalculatorModel(
variable,
model.getOutput()
);
}
/**
* Add variable to model output.
*/
private void add() {
model = new CalculatorModel(
model.getVariable(),
model.getOutput() + model.getVariable()
);
}
/**
* Subtract variable from model output.
*/
private void subtract() {
model = new CalculatorModel(
model.getVariable(),
model.getOutput() - model.getVariable()
);
}
/**
* Multiply model output by variable.
*/
private void multiply() {
model = new CalculatorModel(
model.getVariable(),
model.getOutput() * model.getVariable()
);
}
/**
* Divide model output by variable.
*/
private void divide() {
model = new CalculatorModel(
model.getVariable(),
model.getOutput() / model.getVariable()
);
}
}
@@ -0,0 +1,19 @@
package com.iluwatar.model.view.intent.actions;
/**
* Addition {@link CalculatorAction}.
* */
public class AdditionCalculatorAction implements CalculatorAction {
/**
* Subclass tag.
* */
public static final String TAG = "ADDITION";
/**
* Makes checking subclass type trivial.
* */
@Override
public String tag() {
return TAG;
}
}
@@ -0,0 +1,15 @@
package com.iluwatar.model.view.intent.actions;
/**
* Defines what outside interactions can be consumed by view model.
* */
public interface CalculatorAction {
/**
* Makes identifying action trivial.
*
* @return subclass tag.
* */
String tag();
}
@@ -0,0 +1,19 @@
package com.iluwatar.model.view.intent.actions;
/**
* Division {@link CalculatorAction}.
* */
public class DivisionCalculatorAction implements CalculatorAction {
/**
* Subclass tag.
* */
public static final String TAG = "DIVISION";
/**
* Makes checking subclass type trivial.
* */
@Override
public String tag() {
return TAG;
}
}
@@ -0,0 +1,19 @@
package com.iluwatar.model.view.intent.actions;
/**
* Multiplication {@link CalculatorAction}.
* */
public class MultiplicationCalculatorAction implements CalculatorAction {
/**
* Subclass tag.
* */
public static final String TAG = "MULTIPLICATION";
/**
* Makes checking subclass type trivial.
* */
@Override
public String tag() {
return TAG;
}
}
@@ -0,0 +1,30 @@
package com.iluwatar.model.view.intent.actions;
import lombok.Data;
import lombok.Getter;
/**
* SetVariable {@link CalculatorAction}.
*/
@Data
public final class SetVariableCalculatorAction implements CalculatorAction {
/**
* Subclass tag.
*/
public static final String TAG = "SET_VARIABLE";
/**
* Used by {@link com.iluwatar.model.view.intent.CalculatorViewModel}.
*/
@Getter
private final Double variable;
/**
* Makes checking subclass type trivial.
*/
@Override
public String tag() {
return TAG;
}
}
@@ -0,0 +1,19 @@
package com.iluwatar.model.view.intent.actions;
/**
* Subtraction {@link CalculatorAction}.
* */
public class SubtractionCalculatorAction implements CalculatorAction {
/**
* Subclass tag.
* */
public static final String TAG = "SUBTRACTION";
/**
* Makes checking subclass type trivial.
* */
@Override
public String tag() {
return TAG;
}
}
@@ -0,0 +1,6 @@
/**
* Handle actions for {@link com.iluwatar.model.view.intent.CalculatorModel}
* defined by {@link com.iluwatar.model.view.intent.actions.CalculatorAction}.
*/
package com.iluwatar.model.view.intent.actions;
@@ -0,0 +1,6 @@
/**
* Define Model, View and ViewModel.
* Use them in {@link com.iluwatar.model.view.intent.App}
*/
package com.iluwatar.model.view.intent;
@@ -0,0 +1,40 @@
/*
* This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
*
* The MIT License
* Copyright © 2014-2022 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.model.view.intent;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
/**
* Application test
*/
class AppTest {
@Test
void shouldExecuteApplicationWithoutException() {
assertDoesNotThrow(() -> App.main(new String[]{}));
}
}
@@ -0,0 +1,83 @@
package com.iluwatar.model.view.intent;
import com.iluwatar.model.view.intent.actions.*;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.List;
public class CalculatorViewModelTest {
private CalculatorModel modelAfterExecutingActions(List<CalculatorAction> actions) {
CalculatorViewModel viewModel = new CalculatorViewModel();
for (CalculatorAction action : actions) {
viewModel.handleAction(action);
}
return viewModel.getCalculatorModel();
}
@Test
void testSetup() {
CalculatorModel model = modelAfterExecutingActions(new ArrayList<>());
assert model.getVariable() == 0 && model.getOutput() == 0;
}
@Test
void testSetVariable() {
List<CalculatorAction> actions = List.of(
new SetVariableCalculatorAction(10.0)
);
CalculatorModel model = modelAfterExecutingActions(actions);
assert model.getVariable() == 10.0 && model.getOutput() == 0;
}
@Test
void testAddition() {
List<CalculatorAction> actions = List.of(
new SetVariableCalculatorAction(2.0),
new AdditionCalculatorAction(),
new AdditionCalculatorAction(),
new SetVariableCalculatorAction(7.0),
new AdditionCalculatorAction()
);
CalculatorModel model = modelAfterExecutingActions(actions);
assert model.getVariable() == 7.0 && model.getOutput() == 11.0;
}
@Test
void testSubtraction() {
List<CalculatorAction> actions = List.of(
new SetVariableCalculatorAction(2.0),
new AdditionCalculatorAction(),
new AdditionCalculatorAction(),
new SubtractionCalculatorAction()
);
CalculatorModel model = modelAfterExecutingActions(actions);
assert model.getVariable() == 2.0 && model.getOutput() == 2.0;
}
@Test
void testMultiplication() {
List<CalculatorAction> actions = List.of(
new SetVariableCalculatorAction(2.0),
new AdditionCalculatorAction(),
new AdditionCalculatorAction(),
new MultiplicationCalculatorAction()
);
CalculatorModel model = modelAfterExecutingActions(actions);
assert model.getVariable() == 2.0 && model.getOutput() == 8.0;
}
@Test
void testDivision() {
List<CalculatorAction> actions = List.of(
new SetVariableCalculatorAction(2.0),
new AdditionCalculatorAction(),
new AdditionCalculatorAction(),
new SetVariableCalculatorAction(2.0),
new DivisionCalculatorAction()
);
CalculatorModel model = modelAfterExecutingActions(actions);
assert model.getVariable() == 2.0 && model.getOutput() == 2.0;
}
}