A loosely coupled observer system implementation inspired by boost::signals/glibmm signals.
Observers are an important part of the arsenal for any software designer and provide extraction and loose coupling of side-effects from operations.
Typical observers need a lot of boilerplate code to register and handle observer calls. Missing safety measures can break host logic. Also, depending on use case, we might want to invoke observers in sync/async, fire-forget modes etc.
Signals provides a clean abstraction to implement observers with low overhead.
There are 3 basic steps involved in the process:
- Create a signal
- Connect signal handlers to signal
- Dispatch the signal when you want to trigger observers
Add the following dependency to pom.xml
:
<dependency>
<groupId>io.appform.signals</groupId>
<artifactId>signals</artifactId>
<version>1.4</version>
</dependency>
The following section is some random java code to show how it works. Usecase: trigger a handler when a button is clicked on a window.
@Value
public class Button {
String title;
//1. Create a signal
ConsumingFireForgetSignal<Button> onClick = new ConsumingFireForgetSignal<>();
public Button(final String title) {
this.title = title;
}
void click() {
//3. dispatch signal when you want to trigger observers
onClick.dispatch(this);
}
}
@Value
public class Window {
Button b = new Button("Submit");
public Window() {
//2. Connect signal handlers
b.getOnClick().connect(button -> System.out.println("Button clicked: " + button.getTitle()));
}
}
public class ApplicationTest {
@Test
void testW() {
val window = new Window();
window.getButton().click(); // Will print -> Button clicked: Submit
}
}
A signal is the global container/actor that remembers registered handlers. the dispatch
method can be used to trigger
a signal.
There are two types of Signal Handlers provided and used in the context of different type of signals (see below).
- SignalConsumer - A signal handler that does not return the result of computations performed on the incoming parameter.
Equivalent to
Consumer<T>
in core java. - SignalHandler - A signal handler that returns the results of computations performed on the provided parameter.
This is equivalent to the
Function<T,R>
functional type in core java. Both these types can be coded as lambdas during usage.
Response combiners can assimilate
data resulting from SignalHandler
calls and can be used to accumulate data using
the assimilate(param)
call and the final result is obtained form a final call to result()
. The library provides
two ResponseCombiner
types:
- ConsumingNoOpCombiner - does nothing with the response
- LastValueResponseCombiner - Called in a chain, this will store the last value it encounters
Error Handlers are used to handle exceptions (duh!!) thrown by the SignalHandler calls. The default consumer
called LoggingTaskErrorHandler
logs the error and suppresses it. As a result, it makes sure that even if one handler
in a chain fails, the rest of the chain continues to execute.
There are two basic type of signals:
- Consuming Signal
- Generating Signal
Both are implemented with various variants wrt how they call handlers. All classes provide a default constructor where defaults (as mentioned below in respective sections) are set. Please use the corresponding provided builders (created using build() static method call) to customise different aspects of the Signals.
Consuming signals accept a SignalConsumer
as handler and do not respond back with any responses. There are three types
of consuming signals depending on the mode of handler dispatch. This represents observer patterns more closely and should suffice for most use-cases.
- ConsumingSyncSignal - A Consuming
Signal
that fires handlers in the same thread waits for them to complete. This is the closest to implementing a simple observer chain in your class. The calling function will obviously get blocked till all handlers execute in series. Choose this when your handlers are lightweight. - ConsumingParallelSignal - A Consuming
Signal
that fires handlers in parallel and waits for them to complete. This will execute handlers in parallel. Execution order cannot be guaranteed. The calling function will block till all handlers have executed. Choose this if you need all handlers to complete and they are heavy thread-safe computations. - ConsumingFireForgetSignal - A Consuming
Signal
that fires handlers in parallel and does not wait for their response. All handlers will be called on a thread-pool (by default a single thread different from the calling thread). Use this when you do not need guarantees on execution completion of the handlers before moving on. - ScheduledSignal - A consuming
Signal
where the handler is called at specified intervals. All handlers will be called on a thread-pool (by default a single thread different from the calling thread). Use this to setup regular refresh jobs etc.
- Exception Handler: LoggingTaskErrorHandler
- Response Combiner: ConsumingNoOpCombiner
Generating signals accept handlers of type SignalHandler
that returns response of processing. These results are
accumulated using the ResponseCombiner
and the final result is returned to the caller as a return value of
the dispatch(data)
call. There are two types of Generating Signals based on the mode of execution of handlers.
-
GeneratingSyncSignal - A Generating
Signal
that fires handlers in the calling thread and waits for them to complete. It returns the response as obtained for a call toResponseCombiner.result()
. -
GeneratingParallelSignal - * A Generating
Signal
that fires handlers in parallel and waits for them to complete. It returns the response as obtained for a call toResponseCombiner.result()
.
Generating handlers can be used to implement decision points etc in complicated workflows where the main processing halts for side effects to complete and proceeds using the data generated by them
There are use cases, where you might want to register handlers to a signal and de-register them later when you are no longer interested in listening to dispatches.
To handle this, new methods connect([groupId], name, handler)
and disconnect([groupId], name)
methods have been introduced. Connect and disconnect is available on all signal types.
Java 8
Apache License 2.0