Blog

Decoupling Vaadin components with the Observer Pattern

By  
Sven Ruppert
·
On Oct 23, 2019 4:30:00 PM
·

In this tutorial, we show you how to use the observer design pattern to decouple Vaadin components.

Who Belongs to Whom?

Connecting two components using constructor parameters is a common procedure. It can be clearly seen, for example, in the construction of graphic surfaces.

Take, for example, the following pseudo source code.

public class SubView {
private MainView mainView;
public SubView(MainView mainView) {
this.mainView = mainView;
}
public void buttonClicked(String input) {
mainView.setInputValue(input);
}
}

The subcomponent gets the surrounding main component via the constructor. The instance of the given component is only needed for a value transit. The value results from user interaction in the subcomponent and is consumed in the surrounding element.

This procedure leads to various challenges that can actually be avoided:

  • First, the subcomponent is hard-bound to the main element. This type of hard coupling makes no sense, because the bond is based purely on the use of the generated values.

  • Furthermore, testing the subcomponent is more difficult, because an instance of the main component, or a corresponding MOCK, needs to be used. Again, this is an additional requirement that merely adds complexity and at the same time reduces the abstraction of the individual components.

So how can we add this to a project, without addding another framework as a dependency?

Observer Pattern: The Classic

There is a straightforward design pattern that can help here. We’re talking about the Observer pattern.

public class Observable<KEY, VALUE> {
private final Map<KEY, Consumer<VALUE>> listeners = new ConcurrentHashMap<>();
public void register(KEY key, Consumer<VALUE> listener) {
listeners.put(key, listener);
}
public void unregister(KEY key) {
listeners.remove(key);
}
public void sentEvent(VALUE event) {
listeners.values()
.forEach(listener -> listener.accept(event));
}
}

The basic principle consists of three interactions:

  1. In a map, a consumer is stored for a given key. The consumer is the utilization unit for an event, or better an input type that is defined by the second type definition of the map. You can register with a key. If needed, the associated consumer can also later be removed from the map.

  2. When a data package is ready for processing, it is sent to all consumers who are registered at the time, using the method for posting events.

  3. In practice, all existing consumers in the map process the value once in an undefined order. The sent event must be immutable itself.

Registry

To simplify the process of registering and unsubscribing, you can modify the Observer design pattern a bit.

public interface Registration {
void remove();
}
public class Registry<KEY, VALUE> {
private final Map<KEY, Consumer<VALUE>> listeners = new ConcurrentHashMap<>();
public static <K, V> Registry<K, V> instance() {
return new Registry<>();
}
public Registration register(KEY key, Consumer<VALUE> listener) {
listeners.put(key, listener);
return () -> listeners.remove(key);
}
public void sentEvent(VALUE event) {
listeners.values()
.forEach(listener -> listener.accept(event));
}
}

For this, a functional interface named Registration is defined, which only provides the method to remove itself from the registry. This method implements the respective logout process. The instance of a Registration is the return value of a registration process itself. Processing of the event data happens in the same way as we defined in the implementation of Observable.

The practical use is shown below in a JUnit5 test.

final Registry<String, Event> eventBus = Registry.instance();

final String expected = "message 001";

final AtomicInteger counter = new AtomicInteger(0);
final String key01 = "Consumer-01";
final String key02 = "Consumer-02";

final Registration register01 = eventBus.register(key01, (event) -> {
assertEquals(expected, event.getMessage());
counter.incrementAndGet();
});
final Registration register02 = eventBus.register(key02, (event) -> {
assertEquals(expected, event.getMessage());
counter.incrementAndGet();
});

eventBus.sentEvent(new Event(expected, ""));
Assertions.assertEquals(2, counter.get());

register01.remove();
eventBus.sentEvent(new Event(expected, ""));
Assertions.assertEquals(3, counter.get());

Coupling of Components

Let’s start with the subcomponent. This element is no longer linked to the main component. In this example, we use a general static event bus of type Registry. Of course, you can also use your event bus instance per component, which further decouples the component.

public class SubView {
public void buttonClicked(String input) {
EVENT_BUS.sentEvent(new Event(input));
}
public Registration register(String key, Consumer<Event> listener) {
return EVENT_BUS.register(key, listener);
}
}

If another component wants to use the values of the fictitious user interaction, it can register with the instance of the subcomponent.

public class MainView {
//for demo public
public SubView subView = new SubView();
private Registration registration = subView.register("keyXYZ",
e -> inputValue = e.getValue());
private String inputValue;

public String getInputValue() {
return inputValue;
}

public void release() {
registration.remove();
}
}

The corresponding jUnit5 test looks like this.

final MainView mainView = new MainView();
final String inputValue = "inputValue";
//subview is public for demo
mainView.subView.buttonClicked(inputValue);

Assertions.assertEquals(inputValue, mainView.getInputValue());

How to Use This with Vaadin?

In Vaadin, the Registration interface already exits. And, we can even improve the given implementation of the class of type Registry. Up until now, we needed a key for registration, but this is not necessary. By switching the internal data structure from a Map to a Set , we can use the consumer for the registration and de-registration. Don´t forget to use thread safe data structures to avoid concurrency issues.

public class Registry<VALUE> {

private final Set<Consumer<VALUE>> listeners = ConcurrentHashMap.newKeySet();

public Registration register(Consumer<VALUE> listener) {
listeners.add(listener);
return () -> listeners.remove(listener);
}

public void sentEvent(VALUE event) {
listeners.forEach(listener -> listener.accept(event));
}
}

If we want to build a component-specific Registry for more type-safety, we have to extend the generic class and add the event-type itself.

public class DemoComponentRegistry
extends Registry<DemoComponentRegistry.ValueEvent> {

public static class ValueEvent
extends Pair<String, String> {

public ValueEvent(String id, String value) {
super(id, value);
}

public String id() {
return getT1();
}

public String value() {
return getT2();
}
}
}

To demonstrate the use of this class of type DemoComponentRegistry, we create a class named DemoComponent. The component contains a few attributes to receive and send events. The basic idea is the following:

  • The component can send a message with the content provided by the user. In technical terms, the input value from the instance of type TextField is wrapped into an instance of an event and sent to the registry when the user clicks the button.

  • Additionally, the component can presend event data received from the registry. The id, as well as the value from the event itself, is shown in the two text fields with the "event" prefix in their name.

public class DemoComponent
extends Composite<FormLayout>
implements HasLogger {

private final Checkbox active = new Checkbox(false);
private final TextField input = new TextField();
private final Button sendBtn = new Button();
private final TextField eventID = new TextField("ID:");
private final TextField eventMessage = new TextField("MSG:");

private Result<Registration> registrationResult = Result.failure("not registered");

//SNIP code here
}

The implementation to send an event is shown below. In the ClickListener, the instance of type ValueEvent is created and filled with the component id itself, together with the value from the input field. Afterwards, the freshly created event is sent to all components that are interested in this information.

    sendBtn.setText("send event");
sendBtn.addClickListener(e -> {
final String value = input.getValue();
final String id = DemoComponent.this.getId()
.orElse("");
final ValueEvent valueEvent = new ValueEvent(id, value);

fireCustomEvent(valueEvent);
});
  private void fireCustomEvent(ValueEvent valueEvent) {
UI.getCurrent()
.getSession()
.getAttribute(DemoComponentRegistry.class)
.sentEvent(valueEvent);
}

The instance of the registry itself, is stored is the VaadinSession. With this approach, every user has an instance of an event-bus. If events need to be shared between users, you can use a JVM static instance.

The missing piece is now the registration at the event-bus itself. To make it a bit more dynamic, a checkbox (named active) is used to register and de-register the component itself.

    active.setLabel("receiving events");
active.addValueChangeListener(e -> {
final Boolean isActive = e.getValue();
if (isActive) registrationResult = Result.ofNullable(registerForEvents());
else {
registrationResult.ifPresent(Registration::remove);
registrationResult = Result.failure("not registered");
eventID.setValue("");
eventMessage.setValue("");
}
});
  private Registration registerForEvents() {
return UI.getCurrent()
.getSession()
.getAttribute(DemoComponentRegistry.class)
.register(valueEvent -> {
if (nonNull(valueEvent.id()) && !valueEvent.id()
.equals(getId().orElse(""))) {
eventID.setValue(valueEvent.id());
eventMessage.setValue(valueEvent.value());
}
});
}

In the same way as before, the main view is created and also includes a few instances of type DemoComponent. All together, the result looks like this.

To see this in action, watch this YouTube video or or try it yourself on heroku.

Conclusion

With a few lines of source code, we have not only decoupled the components in a far better way, but also simplified individual element testing. No mocks are needed anymore. The increased abstraction also allows more than one component to register on the subcomponent shown here. Of course, one should not forget at this point that logoff from a registry needs to be taken into consideration to allow the garbage collector to function correctly.

Source code on GitHub.

Happy Coding!

Sven Ruppert
Sven Ruppert has been coding Java since 1996 and is working as Developer Advocate at Vaadin. He is regularly speaking at Conferences like JavaOne/Jfokus/Devoxx/JavaZone/JavaLand and many more and contributes to IT periodicals, as well as tech portals.
Other posts by Sven Ruppert