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 betweenviewModel
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 Mapconverters; 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.
No comments:
Post a Comment