Freitag, 19. Juni 2015

The whiteboard pattern in a Java 8 world

The Whiteboard Pattern is a design pattern used in OSGi applications. It has been published 2004 as an alternative to the Observer Pattern (see the whitepaper of the OSGi alliance.)

Since 2004 - the era of Java 1.4 - Java has received many improvements. So the implementation of the Whiteboard Pattern is much simpler nowadays.

The use case

Assume we have a class NumberProducer that calculates numbers (e.g. prime numbers or something similar).

NumberProducer is used in different environments with different interfaces to present the numbers to the user. It is also possible that there are multiple components available that can print numbers.

So an interface NumberPrinter is defined which specifies the interface the NumberProducer uses to send the calculated numbers to the printing class:

public interface NumberPrinter {
 String printNumber(long value);
 String getLabel();
}

Finding a implementation of NumberPrinter

But how does the NumberProducer find an implementation of NumberPrinter when it has numbers to be printed? This is exactly what the whiteboard pattern descibes.

In this example we have 3 actors:

  • The NumberProducer
  • Two different implementations of NumberPrinter
To use the whiteboard pattern we need an additional component: the whiteboard (which is implemented by the OSGi registry).
When a bundle that provides an implementation of NumberPrinter is activated, it registers this implementation at the OSGi registry. (This is comparable to sticking a note on the whiteboard.)
When the NumberProducer has numbers to print, it searches the OSGi registry for NumberPrinters ...
.. and calls the printNumber() method on each of them:

Implementation part 1: Implement and register a NumberPrinter

When using OSGi declarative services annotations it's quite simple to register a NumberPrinter at the OSGi registry:

Just implement the interface and add the @Component annotation to your class:

@Component
public class HexPrinter implements NumberPrinter {
 @Override
 public String printNumber(long value) {
  return Long.toHexString(value);
 }

 @Override
 public String getLabel() {
  return "Hex";
 }
}
Hint: DS annotations are meant to be processed by the IDE or building toolchain. Use e.g. Declarative Services Annotations Support Eclipse Plug-In or BndTools for this.

Implementation part 2: Let the NumberProducer find and use NumberPrinters

The OSGi Framework provides the BundleTracker that helps finding services that implement specific interfaces.

I decorated this class with a method that takes a closure and applies it to all matching services (see complete code at GitHub):

...
 public void forAllServices(Consumer<S> func) {
  S[] services = getServicesAsArray();
  for (S service : services) {
   func.accept(service);
  }
 }
...

Using this helper class makes the implementation of the NumberProducer simple (complete code):


public class NumberProducer {

 private void printNumberOnPrinters(long number,
   WhiteboardAccess<NumberPrinter> whiteboardAccess) {

  whiteboardAccess.forAllServices((srv) -> {
   System.out.println(srv.printNumber(number));
  });
 }
... 

That's it

As we saw, the implementation of the whiteboard pattern is fast, simple and nearly fail-safe when we're using modern technologies.

Links