1. Pattern Descriptions
The following section describe the four patterns that we’ll use to implement a trivial application
1.1. MVC: Model-View-Controller
Perhaps the most well known pattern, also the one that many will get wrong and implement in an even worse way. The MVC pattern arose as a solution to keep 3 concerns separate from each other: visuals (View), data (Model), and logic (Controller). The pattern is easy to understand but hard to implement given that its generic description: a triangle where all parts can communicate with each other. Problem is, the links between some parts may be passive, for example some claim the View may only read the Model but do not update it directly, where as others claim data flows equally both ways.
The following diagram presents one of the many ways in which this pattern may be implemented; the dotted lines represent passive links.
1.2. MVP: Model-View-Presenter
This pattern emerged as a counterpoint for MVC, trying to solve the triangle problem. In this pattern the View is passive and only reacts to data sent by the Presenter, which came from the Model. The View can route user events to the Presenter but does not know the Model. The Model in turn receives updates form the Presenter and notifies it of any state changes, resulting in an isolated Model. Only the Presenter knows the other two members of the pattern.
1.3. MVVM: Model-View-ViewModel
The MVVM pattern is a weird variation if you ask me, as it puts the logic between the View and the ViewModel. The ViewModel is responsible for abstracting all View input/outputs while providing behavior at the same time. This simplifies (in theory) testing of an application built with this pattern as the ViewModel has the lion’s share of data and behavior, and it’s separated from the View. Perhaps the most interesting feature coming form this pattern is the availability of a Binder, used to synchronize data between View and ViewModel.
1.4. PMVC: Presentation Model-View-Controller
The PMVC pattern is the ultimate variation of MVC, where we’re back to the triangle but this time we’ve got clear links between members. The PresentationModel is responsible for holding information used to display the application’s data and hints to how said data should be visualized, such as colors, fonts, etc. Similarly to MVVM, the View benefits from a Binder that can tie data from the PresentationModel to the View's UI elements. The Controller is now content to manipulate the PresentationModel directly, completely oblivious about a specific View. This leads to a much more testable outcome, as the PresentationModel and Controller are completely separate from the View.
2. Pattern Implementation
It’s time to get down with business. The following sections describe the implementation of each pattern using the Griffon framework. All applications use JavaFX as the toolkit of choice. Regardless of the pattern all UIs rely on FXML to describe the View in a declarative approach as much as possible. The following FXML file is thus shared by all applications
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.AnchorPane?>
<AnchorPane id="AnchorPane" maxHeight="-Infinity" maxWidth="-Infinity"
minHeight="-Infinity" minWidth="-Infinity"
prefHeight="80.0" prefWidth="384.0"
xmlns:fx="http://javafx.com/fxml"
fx:controller="org.example.SampleView">
<children>
<Label layoutX="14.0" layoutY="14.0" text="Please enter your name:"/>
<TextField fx:id="input" layoutX="172.0" layoutY="11.0"
prefWidth="200.0"/>
<Button layoutX="172.0" layoutY="45.0"
mnemonicParsing="false"
prefWidth="200.0"
text="Say hello!"
fx:id="sayHelloActionTarget"/>
<Label layoutX="14.0" layoutY="80.0" prefWidth="360.0" fx:id="output"/>
</children>
</AnchorPane>
Input data is transformed by a simple service class, also shared by all applications
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package org.example;
import griffon.core.artifact.GriffonService;
import griffon.core.i18n.MessageSource;
import griffon.metadata.ArtifactProviderFor;
import org.codehaus.griffon.runtime.core.artifact.AbstractGriffonService;
import static griffon.util.GriffonNameUtils.isBlank;
import static java.util.Arrays.asList;
@ArtifactProviderFor(GriffonService.class)
public class SampleService extends AbstractGriffonService {
public String sayHello(String input) {
MessageSource messageSource = getApplication().getMessageSource();
if (isBlank(input)) {
return messageSource.getMessage("greeting.default");
} else {
return messageSource.getMessage("greeting.parameterized", asList(input));
}
}
}
Alright, we can now have a look at each specific pattern.
2.1. MVC
Let’s begin with the View, as this is the pattern member the user will interact. The View in this case is pretty straight forward, as it only needs to load the FXML file that contains the descriptive UI. However it also exposes two UI elements (input and output) in order for the Controller and Model to read data and supply updates.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package org.example;
import griffon.core.artifact.GriffonView;
import griffon.metadata.ArtifactProviderFor;
import javafx.fxml.FXML;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import org.codehaus.griffon.runtime.javafx.artifact.AbstractJavaFXGriffonView;
import javax.annotation.Nonnull;
import java.util.Collections;
@ArtifactProviderFor(GriffonView.class)
public class SampleView extends AbstractJavaFXGriffonView {
private SampleController controller;
@FXML private TextField input;
@FXML private Label output;
public void setController(SampleController controller) {
this.controller = controller;
}
@Override
public void initUI() {
Stage stage = (Stage) getApplication()
.createApplicationContainer(Collections.<String, Object>emptyMap());
stage.setTitle(getApplication().getConfiguration().getAsString("application.title"));
stage.setWidth(400);
stage.setHeight(120);
stage.setScene(init());
getApplication().getWindowManager().attach("mainWindow", stage);
}
// build the UI
private Scene init() {
Scene scene = new Scene(new Group());
scene.setFill(Color.WHITE);
Node node = loadFromFXML();
((Group) scene.getRoot()).getChildren().addAll(node);
connectActions(node, controller); (1)
return scene;
}
@Nonnull
public TextField getInput() { (2)
return input;
}
@Nonnull
public Label getOutput() { (2)
return output;
}
}
-
Connects user events to Controller
-
Exposes UI elements
Next comes the Model, responsible for holding the data required by the View. Notice that this particular implementation notifies the View using proper threading concerns, that is, data is pushed to the View only inside the UI thread.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package org.example;
import griffon.core.artifact.GriffonModel;
import griffon.metadata.ArtifactProviderFor;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import org.codehaus.griffon.runtime.core.artifact.AbstractGriffonModel;
import javax.annotation.Nonnull;
import java.util.Map;
@ArtifactProviderFor(GriffonModel.class)
public class SampleModel extends AbstractGriffonModel {
private StringProperty output;
@Nonnull
public final StringProperty outputProperty() {
if (output == null) {
output = new SimpleStringProperty(this, "output");
}
return output;
}
public void setOutput(String output) {
outputProperty().set(output);
}
public String getOutput() {
return output == null ? null : outputProperty().get();
}
private SampleView view;
public void setView(SampleView view) {
this.view = view;
}
@Override
public void mvcGroupInit(@Nonnull Map<String, Object> args) {
outputProperty().addListener((observable, oldValue, newValue) -> {
runInsideUIAsync(() -> view.getOutput().setText(newValue)); (1)
});
}
}
-
Notifies View
Finally we get to the Controller, where we can see how it reacts to user events (the click on a button), reads data from
the View, transforms said data, then sends it to the Model. As we saw earlier, the Model closes the circuit by updating
the View. You may be wondering, how is the connection established between Controller and View? If you look back at
the FXML file you’ll notice that the button has an id equal to sayHelloActionTarget
. That name is really close to one
of the Controller's public methods. Also, in the View there’s a call to connectActions
that takes a controller
argument. Here Griffon expects a naming convention (the name of the action method plus the name of the button’s id), which
if followed correctly, will make the a connection between Controller and View.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package org.example;
import griffon.core.artifact.GriffonController;
import griffon.metadata.ArtifactProviderFor;
import griffon.transform.Threading;
import org.codehaus.griffon.runtime.core.artifact.AbstractGriffonController;
import javax.inject.Inject;
@ArtifactProviderFor(GriffonController.class)
public class SampleController extends AbstractGriffonController {
private SampleModel model;
private SampleView view;
@Inject
private SampleService sampleService;
public void setModel(SampleModel model) {
this.model = model;
}
public void setView(SampleView view) {
this.view = view;
}
@Threading(Threading.Policy.INSIDE_UITHREAD_ASYNC)
public void sayHello() {
String input = view.getInput().getText(); (1)
String output = sampleService.sayHello(input); (2)
model.setOutput(output); (3)
}
}
-
Read input
-
Transform data
-
Set as output
Given the trivial nature of this application you may be wondering if it’s worthwhile having a Model at all. And you may be right, however the Model is shown here to compare the different implementations that follow.
2.2. MVP
The View in this pattern is a passive one. Curiously enough we can implement it in the same way as we did with the MVC pattern. The resulting View turns out to be exactly the same as before.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package org.example;
import griffon.core.artifact.GriffonView;
import griffon.metadata.ArtifactProviderFor;
import javafx.fxml.FXML;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import org.codehaus.griffon.runtime.javafx.artifact.AbstractJavaFXGriffonView;
import javax.annotation.Nonnull;
import java.util.Collections;
@ArtifactProviderFor(GriffonView.class)
public class SampleView extends AbstractJavaFXGriffonView {
private SamplePresenter presenter;
@FXML private TextField input;
@FXML private Label output;
public void setPresenter(SamplePresenter presenter) {
this.presenter = presenter;
}
@Override
public void initUI() {
Stage stage = (Stage) getApplication()
.createApplicationContainer(Collections.<String, Object>emptyMap());
stage.setTitle(getApplication().getConfiguration().getAsString("application.title"));
stage.setWidth(400);
stage.setHeight(120);
stage.setScene(init());
getApplication().getWindowManager().attach("mainWindow", stage);
}
// build the UI
private Scene init() {
Scene scene = new Scene(new Group());
scene.setFill(Color.WHITE);
Node node = loadFromFXML();
((Group) scene.getRoot()).getChildren().addAll(node);
connectActions(node, presenter); (1)
return scene;
}
@Nonnull
public TextField getInput() { (2)
return input;
}
@Nonnull
public Label getOutput() { (2)
return output;
}
}
-
Connects user events to Presenter
-
Exposes UI elements
We’ll see some changes in the remaining pattern members. The Model is again just a data holder, with just a single property that takes care of the output to be displayed. There’s nothing more to it given that this class uses JavaFX properties, and as such we’ll use event listeners to let the Model notify the Presenter when a state change occurs.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package org.example;
import griffon.core.artifact.GriffonModel;
import griffon.metadata.ArtifactProviderFor;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import org.codehaus.griffon.runtime.core.artifact.AbstractGriffonModel;
import javax.annotation.Nonnull;
@ArtifactProviderFor(GriffonModel.class)
public class SampleModel extends AbstractGriffonModel {
private StringProperty output;
@Nonnull
public final StringProperty outputProperty() {
if (output == null) {
output = new SimpleStringProperty(this, "output");
}
return output;
}
public void setOutput(String output) {
outputProperty().set(output);
}
public String getOutput() {
return output == null ? null : outputProperty().get();
}
}
Next we get to the Presenter. Griffon allows MVC members to have any name you deem fit, however their
responsibilities must be clearly stated, using the @ArtifactProviderFor
annotation and a given MVC interface. At the
moment of writing there’s no GriffonPresenter
interface, and it’s likely there’ll never be, as this example shows
the current types are more than enough to provide the required behavior
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package org.example;
import griffon.core.artifact.GriffonController;
import griffon.metadata.ArtifactProviderFor;
import griffon.transform.Threading;
import org.codehaus.griffon.runtime.core.artifact.AbstractGriffonController;
import javax.annotation.Nonnull;
import javax.inject.Inject;
import java.util.Map;
@ArtifactProviderFor(GriffonController.class)
public class SamplePresenter extends AbstractGriffonController {
private SampleModel model;
private SampleView view;
@Inject
private SampleService sampleService;
public void setModel(SampleModel model) {
this.model = model;
}
public void setView(SampleView view) {
this.view = view;
}
@Override
public void mvcGroupInit(@Nonnull Map<String, Object> args) {
model.outputProperty().addListener((observable, oldValue, newValue) -> {
runInsideUIAsync(() -> view.getOutput().setText(newValue)); (4)
});
}
@Threading(Threading.Policy.INSIDE_UITHREAD_ASYNC)
public void sayHello() {
String input = view.getInput().getText(); (1)
String output = sampleService.sayHello(input); (2)
model.setOutput(output); (3)
}
}
-
Read input
-
Transform data
-
Set as output
-
Notifies View
Last but not least, we have to cover the application’s configuration in terms of MVC definitions, given that we’re using
presenter
instead of controller
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import griffon.util.AbstractMapResourceBundle;
import javax.annotation.Nonnull;
import java.util.Map;
import static java.util.Arrays.asList;
import static griffon.util.CollectionUtils.map;
public class Config extends AbstractMapResourceBundle {
@Override
protected void initialize(@Nonnull Map<String, Object> entries) {
map(entries)
.e("application", map()
.e("title", "sample")
.e("startupGroups", asList("sample"))
.e("autoShutdown", true)
)
.e("mvcGroups", map()
.e("sample", map()
.e("model", "org.example.SampleModel")
.e("view", "org.example.SampleView")
.e("presenter", "org.example.SamplePresenter")
)
);
}
}
The configuration is quite flexible to allow us a simple rename.
2.3. MVVM
On to the third pattern, MVVM. The View is again a passive one. A Binder makes it easy to keep data in sync between the View and the ViewModel. I opted to make use of the Binder in the View, rendering it almost exactly the same as in the previous two patterns.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package org.example;
import griffon.core.artifact.GriffonView;
import griffon.metadata.ArtifactProviderFor;
import javafx.fxml.FXML;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import org.codehaus.griffon.runtime.javafx.artifact.AbstractJavaFXGriffonView;
import javax.annotation.Nonnull;
import java.util.Collections;
@ArtifactProviderFor(GriffonView.class)
public class SampleView extends AbstractJavaFXGriffonView {
private SampleViewModel viewModel;
@FXML private TextField input;
@FXML private Label output;
public void setViewModel(SampleViewModel viewModel) {
this.viewModel = viewModel;
}
@Override
public void initUI() {
Stage stage = (Stage) getApplication()
.createApplicationContainer(Collections.<String, Object>emptyMap());
stage.setTitle(getApplication().getConfiguration().getAsString("application.title"));
stage.setWidth(400);
stage.setHeight(120);
stage.setScene(init());
getApplication().getWindowManager().attach("mainWindow", stage);
}
// build the UI
private Scene init() {
Scene scene = new Scene(new Group());
scene.setFill(Color.WHITE);
Node node = loadFromFXML();
((Group) scene.getRoot()).getChildren().addAll(node);
connectActions(node, viewModel); (1)
input.textProperty().bindBidirectional(viewModel.inputProperty()); (2)
output.textProperty().bindBidirectional(viewModel.outputProperty()); (2)
return scene;
}
}
-
Connects user events to ViewModel
-
Binds ViewModel and View
Now, the ViewModel contains data and behavior, essentially, a combination of Model and Controller/Presenter as seen in previous patterns
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package org.example;
import griffon.core.artifact.GriffonController;
import griffon.metadata.ArtifactProviderFor;
import griffon.transform.Threading;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import org.codehaus.griffon.runtime.core.artifact.AbstractGriffonController;
import javax.annotation.Nonnull;
import javax.inject.Inject;
@ArtifactProviderFor(GriffonController.class)
public class SampleViewModel extends AbstractGriffonController {
private SampleView view;
private StringProperty input;
private StringProperty output;
@Nonnull
public final StringProperty inputProperty() {
if (input == null) {
input = new SimpleStringProperty(this, "input");
}
return input;
}
public void setInput(String input) {
inputProperty().set(input);
}
public String getInput() {
return input == null ? null : inputProperty().get();
}
@Nonnull
public final StringProperty outputProperty() {
if (output == null) {
output = new SimpleStringProperty(this, "output");
}
return output;
}
public void setOutput(String output) {
outputProperty().set(output);
}
public String getOutput() {
return output == null ? null : outputProperty().get();
}
@Inject
private SampleService sampleService;
public void setView(SampleView view) {
this.view = view;
}
@Threading(Threading.Policy.INSIDE_UITHREAD_ASYNC)
public void sayHello() {
String input = getInput(); (1)
String output = sampleService.sayHello(input); (2)
setOutput(output); (3)
}
}
-
Read input
-
Transform data
-
Set as output
The configuration is shown next.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import griffon.util.AbstractMapResourceBundle;
import javax.annotation.Nonnull;
import java.util.Map;
import static java.util.Arrays.asList;
import static griffon.util.CollectionUtils.map;
public class Config extends AbstractMapResourceBundle {
@Override
protected void initialize(@Nonnull Map<String, Object> entries) {
map(entries)
.e("application", map()
.e("title", "sample")
.e("startupGroups", asList("sample"))
.e("autoShutdown", true)
)
.e("mvcGroups", map()
.e("sample", map()
.e("view", "org.example.SampleView")
.e("viewModel", "org.example.SampleViewModel")
)
);
}
}
In truth we could have had a Model member too, just like in the other pattern implementations. However MVVM prefers the Model to wrap a domain object or provide direct access to the data layer. As that’s not the case for this trivial application the Model disappears and the data properties are moved to the ViewModel. This also servers to showcase that MVC configurations can have less members than the stereotypical three.
2.4. PMVC
Finally we meet the last pattern. We begin again with the View.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package org.example;
import griffon.core.artifact.GriffonView;
import griffon.metadata.ArtifactProviderFor;
import javafx.fxml.FXML;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import org.codehaus.griffon.runtime.javafx.artifact.AbstractJavaFXGriffonView;
import java.util.Collections;
@ArtifactProviderFor(GriffonView.class)
public class SampleView extends AbstractJavaFXGriffonView {
private SampleController controller;
private SampleModel model;
@FXML private TextField input;
@FXML private Label output;
public void setController(SampleController controller) {
this.controller = controller;
}
public void setModel(SampleModel model) {
this.model = model;
}
@Override
public void initUI() {
Stage stage = (Stage) getApplication()
.createApplicationContainer(Collections.<String, Object>emptyMap());
stage.setTitle(getApplication().getConfiguration().getAsString("application.title"));
stage.setWidth(400);
stage.setHeight(120);
stage.setScene(init());
getApplication().getWindowManager().attach("mainWindow", stage);
}
// build the UI
private Scene init() {
Scene scene = new Scene(new Group());
scene.setFill(Color.WHITE);
Node node = loadFromFXML();
((Group) scene.getRoot()).getChildren().addAll(node);
model.inputProperty().bindBidirectional(input.textProperty()); (2)
model.outputProperty().bindBidirectional(output.textProperty()); (2)
connectActions(node, controller); (1)
return scene;
}
}
-
Connects user events to Controller
-
Binds Model and View
Opposed to the other implementations, this View makes direct use of the Binder, thus foregoing the need to expose UI elements to other MVC members. In this way, the View is self contained and all of its links are properly established.
The Model goes back to being a simple data holder, but this time it contains properties for both input and output, as those define all the required data points.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package org.example;
import griffon.core.artifact.GriffonModel;
import griffon.metadata.ArtifactProviderFor;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import org.codehaus.griffon.runtime.core.artifact.AbstractGriffonModel;
import javax.annotation.Nonnull;
@ArtifactProviderFor(GriffonModel.class)
public class SampleModel extends AbstractGriffonModel {
private StringProperty input;
private StringProperty output;
@Nonnull
public final StringProperty inputProperty() {
if (input == null) {
input = new SimpleStringProperty(this, "input");
}
return input;
}
public void setInput(String input) {
inputProperty().set(input);
}
public String getInput() {
return input == null ? null : inputProperty().get();
}
@Nonnull
public final StringProperty outputProperty() {
if (output == null) {
output = new SimpleStringProperty(this, "output");
}
return output;
}
public void setOutput(String output) {
outputProperty().set(output);
}
public String getOutput() {
return output == null ? null : outputProperty().get();
}
}
The last member we must discuss is the Controller itself.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package org.example;
import griffon.core.artifact.GriffonController;
import griffon.metadata.ArtifactProviderFor;
import griffon.transform.Threading;
import org.codehaus.griffon.runtime.core.artifact.AbstractGriffonController;
import javax.inject.Inject;
@ArtifactProviderFor(GriffonController.class)
public class SampleController extends AbstractGriffonController {
private SampleModel model;
@Inject
private SampleService sampleService;
public void setModel(SampleModel model) {
this.model = model;
}
@Threading(Threading.Policy.INSIDE_UITHREAD_ASYNC)
public void sayHello() {
String input = model.getInput(); (1)
String output = sampleService.sayHello(input); (2)
model.setOutput(output); (3)
}
}
-
Read input
-
Transform data
-
Set as output
This Controller has no need for a particular View, it only needs a PresentationModel that delivers the required inputs and outputs. This greatly simplifies testing such components.
3. Conclusions
As you can see, Griffon is flexible enough and can adapt itself to your preferred MVC pattern. There are other MVC variants, such as the Hierarchical model–view–controller, which is also automatically supported, given that Griffon enables hierarchical MVC groups and per-group contexts.
The preferred pattern is PMVC, as that’s the one suggested by the standard project templates, but as we’ve shown, it’s easy to migrate from one pattern to the next.
The code for all applications is readily available on GitHub at aalmiray/griffon-patterns.