Hower there is bunch of patterns addressing presentation layer issues, implementing clean presentation layer in Java desktop application is tricky task. Main problem for me is a lot of mess inside. I'd like to present my approach to take control of presentation layer, which is combination of existing ideas such:
Supervising Controller, Passive View,
Presentation Model,
Publish-Subscribe,
Observer.
This will be an approach, not a pattern. The reason is a pattern is something widely used and this will be just set of tips. So you won't be able to apply it by just repetition some steps, but by interpretation the approach in the current context and
SOLID analysis. But if you are not familiar with patterns listed above and presentation layer issues of desktop application, try
Mediator pattern or SmartUI approach first. You will have some mess in the code but still under control.
So, if you want to take a look on risky implementation of a presentation layer, check out
this example. As far I remember some folks call this SmartUI anti-pattern.
Well, the UI is really smart in the example. It contains child UI components, implements listeners and does both UI and business processing. This GUI is simple but continuing this approach leads to code which is hard to understand, hard to test, hard to develop.
The idea
Let's list challenges for the desktop applications with visual presentation layer:
- communication between controls
- managing application state - there are three types of it: screen state, session state and record state
- type conversion between view and rest of the application
- handling user events
- keeping GUI responsive - typically with threads
- testability - Humble Dialog aka MVP is a solution
Take look on the figure.
In the typical application all controls are grouped by their business purpose and UX rules. Let's treat the controls arrangement as being not so important for the application architect. What is important that we have many groups of controls, each group has its business purpose and groups need to communicate with others. Let's call these groups components.
However components might be visually arranged in a hierarchy (depends on UX designer), from the source code side they are totally equivalent. Component to communicate with other component use an event bus. When an event comes to a component it updates its state (what mostly means a user view is changed) according to the subscribed event.
Using event bus gives all benefits of the
Publish-Subscribe pattern. You may use topics or even two or more event buses for really complex GUIs.
This was an idea. Now let's take a look how a component is designed inside.
Structure of a component
Five parts works together as a component.
part | responsibility | out of responsibility |
Form | * is coupled with a particular GUI technology
* builds itself
* has private helper methods to working with embedded controls
* calculates a control arrangement | * business processing
* calling calling services
* validation
event publishing |
FormEventHandler | * implements all technology-specific event listeners
* might be decomposed into specialized handlers e.g. DragAndDropHandler | * subscribing events other than technology-specific
* business processing
* calling calling services
* validation
* event publishing |
View | * exposes methods for getting and Form state (see Nice Move with ValueSetter)
* typically is implemented by Form classes | * event publishing
* event subscribing |
ViewObserver | * notifies that something happened on a Form
* typically is implemented by the Presenter class | * event subscribing |
Presenter | * calls business services
* publishes events addressed other components
* subscribes on events from other components | * business processing (except of very rarely cases) |
So how you see, a component talks to other components via event bus, but asynchronous communication inside the component is implemented via
Observer Patterns. There is nothing wrong with using event bus inside the component also. But in my opinion the solution with event bus outside- and Observer inside- is more clear and easier to test.
and how it works?
Here you have a diagram how a user request is processed by a component.
and also how components talk to others:
ViewModel
- what is for?
The problem is how to deal with screen state (keeping
Form
as stupid as possible), especially when you work with the remote backend and you need to synchronize the client with the server.
Possible solution to this is introducing a session state which is in-memory copy of screen state.
Presentation Model pattern do that. The pattern
represent the state and behavior of the presentation independently of the GUI controls used in the user interface. It is possible to find
Presentation Model structure in this approach. These are:
View
interface and
Presenter.viewModel
property.
However implementing
Presentation Model is not the goal, I have borrowed
viewModel
part from the pattern. Why?
In regular MVP pattern
View
exposes methods to set and get state of
Form
controls. It's something like that:
public class DetailsPresenter implements DetailsViewObserver {
private DetailsView view;
public void handleRequest() {
String name = view.getName();
String name = view.getTitle();
Date dayOfBirth = toDate( view.getDayOfBirth());
//call services
view.setTitle( newTitle );
//...
}
}
This approach causes lots of
set/get
methods in the
View
interface. Because the fact that some controls states are needed to business processing the Presenter handler methods are full
set/get/add
invocations.
An alternative to this is full-featured
Data Binding. When something changes on the screen it is automatically reflected to the
viewModel
and vice versa. Really magic
Data Binding means lots of listeners, controllers and events going there and back again.
So, what instead? Map? Well, map might be a bag for everything, but idea is good. I use the apache-commons
DynaBean
as
viewModel
implementation.
ViewObserver
exposes the method to notify about changes of screen state.
public class DetailsPresenter implements DetailsViewObserver {
private DynaBean viewModel;
private DetailsView view;
public void buildViewModel() {
viewModel = new LazyDynaBean();
viewModel.set("name", "Jack");
viewModel.set("title", "Dr.");
viewModel.set( "dayOfBirth", new Date());
}
public void handleRequest() {
String name = viewModel.get("name");
String name = viewModel.get("title);
Date dayOfBirth = viewModel.get("dayOfBirth");
//call services
view.updateControl( "title", viewModel.get( newTitle) );
//or updateAllControls()
}
@Override
public void propertyChanged(String property, Object value) {
viewModel.set(property, value);
}
}
public class DetailsFormListener implements DocumentListener {
private DetailsViewObserver viewObserver;
public void changedUpdate(DocumentEvent e) {
viewObserver.propertyChanged("title", e.getDocument().getText());
}
//...
}
Implementation details
You may fetch examples from
my Github profile.
pl.bnsit.architecture
package
This is basic template of the approach.
pl.bnsit.flights
package
This is an example with coule windows, validation and so on. But I followed the typical llayered architecture.
pl.bnsit.onepieceflow
package
There I went a step ahead. I tried to write simple Kanban board. But I started thinking iin a component-like way. I asked myself
What are independent components on this GUI? and answered:
a card, lane containing set of cards and board containing set of lanes. Then I realized that the independency might be considered through all application layers, not only inside the presentation layer.
Continuing this way of thinking we come the architecture with small, well-defined components anchored in the business domain, which are working together to do a business task.
Type conversion
Notice that some type conversion must be done between
viewModel
and
Form
. I did this with TypeConverter interface:
public interface TypeConverter {
String toString( Object anObject );
Object fromString( String str);
Class type();
}
Conversion is done when the control state is updated:
public class AddingCustomerForm extends JFrame implements AddingCustomerView {
private Map converters;
public void updateComponent(String id, Object value) {
Object convertedValue = convert(value);
JComponent component = components.get(id);
ValueSetter.get(component.getClass()).set(component, converterValue);
}
private Object convert(Object value) {
Object converterValue = value;
if( converters.containsKey(value.getClass()) ) {
TypeConverter converter = converters.get(value.getClass());
converterValue = converter.toString(value);
}
return converterValue;
}
and back, when Listener notifies via
ViewObserver
that control state was changed.