Java Signals and Effects
Observables done right To tee off this presentation, consider a TodosList that contains Todo items. You wish to be able to react to the following events. In any Todo item, when: the title is changed the completion status is toggled In the TodosList, when: a new item is added an existing item is removed The behaviors executed in response to these reactions are called effects, and they are set off when a signal, or more explicitly, an observed class property, is modified in the course of program execution. Approach 1 Dialing it back to begin from first principles, here is a basic representation of the respective domain classes: Domain class with basic fields @Data @AllArgsConstructor public class Todo { private UUID id; private String title; private Boolean completed; } Domain class with collection field public class TodosList { private final Collection todos = new ArrayList(); public Todo add(String title){ Todo todo = new Todo(UUID.randomUUID(), title, false); todos.add(todo); return todo; } public void update(String id, String title){ Todo todo = todos.stream().filter(t -> t.getId().toString().equals(id)).findFirst() .orElseThrow(() -> new RuntimeException("no task found with matching id")); todo.setTitle(title); } public void toggle(String id){ Todo todo = todos.stream().filter(t -> t.getId().toString().equals(id)).findFirst() .orElseThrow(() -> new RuntimeException("no task found with matching id")); todo.setCompleted(!todo.getCompleted()); } public void delete(String id){ Todo todo = todos.stream().filter(t -> t.getId().toString().equals(id)).findFirst() .orElseThrow(() -> new RuntimeException("no task found with matching id")); todos.remove(todo); } } Observing changes to the state of an object when the events of interest described above are triggered can be achieved through different techniques, which I will review here. A basic solution would require explicitly implementing some kind of observation mechanism, like a Listener. public interface Listener { void onTitleChanged(Todo todo); void onCompletionChanged(Todo todo); void onItemAdded(Todo entity, Collection todos); void onItemRemoved(Todo entity, Collection todos); } Then a concrete implementation of the listener would execute the business requirements necessary when the events of interest are fired. Different concrete classes of the listener interface would have to be implemented if different behavior becomes a necessity. Below is one such implementation which only acknowledges the event happening by printing its details to the console. public class BasicListener implements Listener { @Override public void onTitleChanged(Todo todo) { System.out.printf("Task title changed to %s\n", todo.getTitle()); } @Override public void onCompletionChanged(Todo todo) { System.out.printf("Task completion changed to %s\n", todo.getCompleted()); } @Override public void onItemAdded(Todo entity, Collection todos) { System.out.printf("Event: add, entity: %s\n", entity); todos.forEach(System.out::println); } @Override public void onItemRemoved(Todo entity, Collection todos) { System.out.printf("Event: remove, entity: %s\n", entity); todos.forEach(System.out::println); } } These two classes (observable and listener implementation) represent functionality that needs to be woven together in some way, to be able to react to state changes. The easiest (and unfortunately pretty invasive as well) is to add statements in the TodosList object to invoke methods in the BasicListener when the events of interest are happening. The updated TodosList would therefore look something like this. public class TodosList { private final Collection todos = new ArrayList(); private final Listener listener = new BasicListener(); // require listener to accept events public Todo add(String title){ Todo todo = new Todo(UUID.randomUUID(), title, false); todos.add(todo); listener.onItemAdded(todo, todos); // explicitly fire listener function return todo; } public void update(String id, String title){ Todo todo = todos.stream().filter(t -> t.getId().toString().equals(id)).findFirst() .orElseThrow(() -> new RuntimeException("no task found with matching id")); todo.setTitle(title); listener.onTitleChanged(todo); // explicitly fire listener function } public void toggle(String id){ Todo todo = todos.stream().filter(t -> t.getId().toString().equals(id)).findFirst() .orElseThrow(() -> new RuntimeException("no task found with matching id")); todo.setCompleted(!todo.getCompleted()); listener.onComple

Observables done right
To tee off this presentation, consider a TodosList that contains Todo items. You wish to be able to react to the following events.
In any Todo item, when:
- the title is changed
- the completion status is toggled
In the TodosList, when:
- a new item is added
- an existing item is removed
The behaviors
executed in response to these reactions are called effects
, and they are set off when a signal
, or more explicitly, an observed class property
, is modified in the course of program execution.
Approach 1
Dialing it back to begin from first principles, here is a basic representation of the respective domain classes:
Domain class with basic fields
@Data
@AllArgsConstructor
public class Todo {
private UUID id;
private String title;
private Boolean completed;
}
Domain class with collection field
public class TodosList {
private final Collection<Todo> todos = new ArrayList<>();
public Todo add(String title){
Todo todo = new Todo(UUID.randomUUID(), title, false);
todos.add(todo);
return todo;
}
public void update(String id, String title){
Todo todo = todos.stream().filter(t -> t.getId().toString().equals(id)).findFirst()
.orElseThrow(() -> new RuntimeException("no task found with matching id"));
todo.setTitle(title);
}
public void toggle(String id){
Todo todo = todos.stream().filter(t -> t.getId().toString().equals(id)).findFirst()
.orElseThrow(() -> new RuntimeException("no task found with matching id"));
todo.setCompleted(!todo.getCompleted());
}
public void delete(String id){
Todo todo = todos.stream().filter(t -> t.getId().toString().equals(id)).findFirst()
.orElseThrow(() -> new RuntimeException("no task found with matching id"));
todos.remove(todo);
}
}
Observing changes to the state of an object when the events of interest described above are triggered can be achieved through different techniques, which I will review here. A basic solution would require explicitly implementing some kind of observation mechanism, like a Listener.
public interface Listener {
void onTitleChanged(Todo todo);
void onCompletionChanged(Todo todo);
void onItemAdded(Todo entity, Collection<Todo> todos);
void onItemRemoved(Todo entity, Collection<Todo> todos);
}
Then a concrete implementation of the listener would execute the business requirements necessary when the events of interest are fired. Different concrete classes of the listener interface would have to be implemented if different behavior becomes a necessity. Below is one such implementation which only acknowledges the event happening by printing its details to the console.
public class BasicListener implements Listener {
@Override
public void onTitleChanged(Todo todo) {
System.out.printf("Task title changed to %s\n", todo.getTitle());
}
@Override
public void onCompletionChanged(Todo todo) {
System.out.printf("Task completion changed to %s\n", todo.getCompleted());
}
@Override
public void onItemAdded(Todo entity, Collection<Todo> todos) {
System.out.printf("Event: add, entity: %s\n", entity);
todos.forEach(System.out::println);
}
@Override
public void onItemRemoved(Todo entity, Collection<Todo> todos) {
System.out.printf("Event: remove, entity: %s\n", entity);
todos.forEach(System.out::println);
}
}
These two classes (observable and listener implementation) represent functionality that needs to be woven together in some way, to be able to react to state changes. The easiest (and unfortunately pretty invasive as well) is to add statements in the TodosList object to invoke methods in the BasicListener when the events of interest are happening. The updated TodosList would therefore look something like this.
public class TodosList {
private final Collection<Todo> todos = new ArrayList<>();
private final Listener listener = new BasicListener(); // require listener to accept events
public Todo add(String title){
Todo todo = new Todo(UUID.randomUUID(), title, false);
todos.add(todo);
listener.onItemAdded(todo, todos); // explicitly fire listener function
return todo;
}
public void update(String id, String title){
Todo todo = todos.stream().filter(t -> t.getId().toString().equals(id)).findFirst()
.orElseThrow(() -> new RuntimeException("no task found with matching id"));
todo.setTitle(title);
listener.onTitleChanged(todo); // explicitly fire listener function
}
public void toggle(String id){
Todo todo = todos.stream().filter(t -> t.getId().toString().equals(id)).findFirst()
.orElseThrow(() -> new RuntimeException("no task found with matching id"));
todo.setCompleted(!todo.getCompleted());
listener.onCompletionChanged(todo); // explicitly fire listener function
}
public void delete(String id){
Todo todo = todos.stream().filter(t -> t.getId().toString().equals(id)).findFirst()
.orElseThrow(() -> new RuntimeException("no task found with matching id"));
todos.remove(todo);
listener.onItemRemoved(todo, todos); // explicitly fire listener function
}
}
A main
class would then be used set off the orchestration of events as shown below
public class Main {
public static void main(String[] args) {
TodosList list = new TodosList();
Todo t1 = list.add("wake up");
Todo t2 = list.add("make breakfast");
Todo t3 = list.add("watch tv");
list.update(t2.getId().toString(), "work out");
list.toggle(t1.getId().toString());
list.delete(t3.getId().toString());
}
}
Putting it all together, the main class shown above would certainly do a decent job of capturing all the targeted events and executing the prescribed effects.
If multiple listener implementations need to be invoked when these state changes happen, then it would require having a collection of such listeners and calling them all sequentially to dispatch the events' data.
public class AnotherListener implements Listener {
@Override
public void onTitleChanged(Todo todo) {
System.out.printf("[**] Task title changed to %s\n", todo.getTitle());
}
@Override
public void onCompletionChanged(Todo todo) {
System.out.printf("[**] Task completion changed to %s\n", todo.getCompleted());
}
@Override
public void onItemAdded(Todo entity, Collection<Todo> todos) {
System.out.printf("[**] Event: add, entity: %s\n", entity);
todos.forEach(System.out::println);
}
@Override
public void onItemRemoved(Todo entity, Collection<Todo> todos) {
System.out.printf("[**] Event: remove, entity: %s\n", entity);
todos.forEach(System.out::println);
}
}
The TodosList would now need to save a reference to the subscribers in a collection, and invoke them all when an event is patched.
public class TodosList {
private final Collection<Todo> todos = new ArrayList<>();
private final Collection<Listener> listeners = new LinkedList<>(); // use a collection to hold multiple listeners
// convenience method to register listener
public void addListener(Listener listener) {
this.listeners.add(listener);
}
// convenience method to unregister listener
public void removeListener(Listener listener) {
this.listeners.remove(listener);
}
public Todo add(String title){
// omitted for brevity
listeners.forEach(l -> l.onItemAdded(todo, todos));
return todo;
}
public void update(String id, String title){
// omitted for brevity
listeners.forEach(l -> l.onTitleChanged(todo));
}
public void toggle(String id){
// omitted for brevity
listeners.forEach(l -> l.onCompletionChanged(todo));
}
public void delete(String id){
// omitted for brevity
listeners.forEach(l -> l.onItemRemoved(todo, todos));
}
}
Lastly, the main class would then be used to register (and perhaps even to unregister) listeners
public class Main {
public static void main(String[] args) {
TodosList list = new TodosList();
// register listeners
list.addListener(new BasicListener());
list.addListener(new AnotherListener());
// continue in the same way as before
Todo t1 = list.add("wake up");
Todo t2 = list.add("make breakfast");
Todo t3 = list.add("watch tv");
list.update(t2.getId().toString(), "work out");
list.toggle(t1.getId().toString());
list.delete(t3.getId().toString());
}
}
The main problem with this method is that the concerns of the listener (registering, unregistering and dispatching) must be manually woven into observable, which opens up a lot of opportunities for errors to creep in and therefore requires careful design and extensive testing. Even more importantly, however, the listener implementation is very tightly coupled to the Observable, and hence impossible to reuse in any other situation without major modifications.
Approach 2
A slightly more idiomatic approach would be to take advantage of Java's built-in Observer/Observable framework to offload much of the observing
concerns, registering and unregistering of listeners to the framework and instead focus more on the effects or more explicitly, focus on the corresponding behavior after events are triggered.
This method is similarly as intrusive as the approach 1
implemented previously, and has actually been deprecated since java 9, and as a result, I would not even encourage anyone to use it.
@Getter
@AllArgsConstructor
public class Todo extends Observable {
@Setter
private UUID id;
private String title;
private Boolean completed;
public void setTitle(String title) {
this.title = title;
setChanged(); // mark the field as dirty
notifyObservers(this); // inform listeners to do their thing
}
public void setCompleted(Boolean completed) {
this.completed = completed;
setChanged(); // mark the field as dirty
notifyObservers(this); // inform listeners to do their thing
}
}
The setters in an Observable need to be instrumented to notify observers of some change in state. The existing Listener
implementations can be repurposed into Observers
by implementing Java's own Observer interface.
public class BasicListener implements Listener, Observer {
@Override
public void onTitleChanged(Todo todo) {
System.out.printf("Task title changed to %s\n", todo.getTitle());
}
@Override
public void onCompletionChanged(Todo todo) {
System.out.printf("Task completion changed to %s\n", todo.getCompleted());
}
@Override
public void onItemAdded(Todo entity, Collection<Todo> todos) {
System.out.printf("Event: add, entity: %s\n", entity);
todos.forEach(System.out::println);
}
@Override
public void onItemRemoved(Todo entity, Collection<Todo> todos) {
System.out.printf("Event: remove, entity: %s\n", entity);
todos.forEach(System.out::println);
}
@Override
public void update(Observable obj, Object arg) {
if (obj instanceof Todo todo) {
System.out.println("[Observer] received event -> todo: " + todo);
}
if (obj instanceof TodosList list) {
System.out.println("[Observer] received event -> todos: " + list);
}
}
}
The second Observer
would take similar modifications to the ones made in the first one.
public class AnotherListener implements Listener, Observer {
@Override
public void onTitleChanged(Todo todo) {
System.out.printf("[**] Task title changed to %s\n", todo.getTitle());
}
@Override
public void onCompletionChanged(Todo todo) {
System.out.printf("[**] Task completion changed to %s\n", todo.getCompleted());
}
@Override
public void onItemAdded(Todo entity, Collection<Todo> todos) {
System.out.printf("[**] Event: add, entity: %s\n", entity);
todos.forEach(System.out::println);
}
@Override
public void onItemRemoved(Todo entity, Collection<Todo> todos) {
System.out.printf("[**] Event: remove, entity: %s\n", entity);
todos.forEach(System.out::println);
}
@Override
public void update(Observable obj, Object arg) {
if (obj instanceof Todo todo) {
System.out.println("[**Observer**] received event -> todo: " + todo);
}
if (obj instanceof TodosList list) {
System.out.println("[**Observer**] received event -> todos: " + list);
}
}
}
The fact that the notifyObservers(obj) in the Observable takes just one argument of Object
type makes it difficult to be expressive when using this method. It becomes tricky to detect what attributes
changed in which Observable
when the event is received over on the Listener
side.
public class TodosList extends Observable {
private final Collection<Todo> todos = new ArrayList<>();
public Todo add(String title){
Todo todo = new Todo(UUID.randomUUID(), title, false);
todos.add(todo);
setChanged(); // mark the field as dirty
notifyObservers(todos); // inform listeners to do their thing
return todo;
}
public void update(String id, String title){
Todo todo = todos.stream().filter(t -> t.getId().toString().equals(id)).findFirst()
.orElseThrow(() -> new RuntimeException("no task found with matching id"));
todo.setTitle(title);
}
public void toggle(String id){
Todo todo = todos.stream().filter(t -> t.getId().toString().equals(id)).findFirst()
.orElseThrow(() -> new RuntimeException("no task found with matching id"));
todo.setCompleted(!todo.getCompleted());
}
public void delete(String id){
Todo todo = todos.stream().filter(t -> t.getId().toString().equals(id)).findFirst()
.orElseThrow(() -> new RuntimeException("no task found with matching id"));
todos.remove(todo);
setChanged(); // mark the field as dirty
notifyObservers(this); // inform listeners to do their thing
}
}
The main
class now changes pretty dramatically since the Observers need to be registered with each Observable party.
public class Main {
public static void main(String[] args) {
TodosList list = new TodosList();
BasicListener basic = new BasicListener();
AnotherListener another = new AnotherListener();
// register listeners
list.addObserver(basic);
list.addObserver(another);
Todo t1 = list.add("wake up");
// register listeners
list.addObserver(basic);
list.addObserver(another);
Todo t2 = list.add("make breakfast");
// register listeners
list.addObserver(basic);
list.addObserver(another);
Todo t3 = list.add("watch tv");
// register listeners
list.addObserver(basic);
list.addObserver(another);
// proceed in the usual manner
list.update(t2.getId().toString(), "work out");
list.toggle(t1.getId().toString());
list.delete(t3.getId().toString());
}
}
As mentioned earlier, this approach may have been cutting-edge in its heydays, but in today's technology landscape, those glory days are far in the rearview mirror now. It's certainly an improvement over the approach 1
in that the observing responsibility is delegated to the underlying framework, but it lacks the critical versatility of
reusability, since it's not easy to use without a lot of customization, and hence any solution in which it is a part of is not easily reusable without major refactoring.
I have skipped the details of demultiplexing events arriving in the void update(Observable obj, Objects arg)
methods of the Observers
because it can get unnecessarily complex detecting what attributes changed, so that the routing may be dispatched to the correct Listener methods.
Approach 3
So what else is out there that will perform the same role as Observer/Observable
, but without the difficulty of use associated with approaches 1 & 2
illustrated before? Enter Signals
. This is a concept that I have used extensively in the JavaScript ecosystem, and its non-existence in the Java universe is pretty saddening.
This is an attempt to narrow that gap.
# using maven
com.akilisha.oss
signals
0.0.1
# using gradle
implementation("com.akilisha.oss:signals:0.0.1")
Signals
uses the concept of instrumenting attributes that need to be observed and the registration of listeners is then implicitly achieved in the construction phase of an Observable
by literary accessing these observed attributes.
Allow me illustrate because that explanation was certainly not exhaustive. The Todo
class in this case clearly shows which attributes are candidates for observation.
@Getter
@AllArgsConstructor
public class Todo {
private final Signal<String> title = Signals.signal("");
private final Signal<Boolean> completed = Signals.signal(false);
@Setter
private UUID id;
public Todo(String title) {
this(UUID.randomUUID(), title, false);
}
public Todo(UUID id, String title, Boolean completed) {
this.id = id;
this.title.value(title);
this.completed.value(completed);
}
@Override
public String toString() {
return "Todo{" +
"title=" + title.value() +
", completed=" + completed.value() +
", id=" + id +
'}';
}
}
It's always convenient in the majority of cases to work with data carriers implemented as Java's Records (or Class) to complement domain classes instrumented with Signal
attributes. Although not used in this presentation, TodoItem is nonetheless an example of such a data carrier object. They are a totally optional but very convenient.
public record TodoItem (UUID id, String title, Boolean completed){
public TodoItem(String title){
this(UUID.randomUUID(), title, false);
}
}
Now instead of explicitly implementing Listener
interfaces, the effects
of changes to the title
and completed
attributes of a Todo class can be captured during construction of the Todo objects in a factory method. Each call to the .observe()
method will return a Subscription object which can be stored and then used later on to cancel the captured effect from getting invoked again (similar to unsubscribing a listener). In this presentation, I will not be using the Subscription object so that I may focus on effects.
@Getter
@AllArgsConstructor
public class Todo {
// code omitted from brevity
public static Todo from(String title){
Todo todo = new Todo(title);
// observe title attribute - multiple Observer effects can be captured here
Signals.observe(() -> System.out.printf("Task title changed to %s\n", todo.getTitle().get()));
// observe completed attribute - multiple Observer effects can be captured here
Signals.observe(() -> System.out.printf("Task completion changed to %s\n", todo.getCompleted().get()));
return todo;
}
@Override
public String toString() {
// omitted for brevity
}
}
When observing a Collection or a Map attributes, values are wrapped in the SignalCollection and SignalDictionary classes respectively because they have unique distinguishing characteristics that need to be handled differently.
In this case, the TodosList needs the todos Collection to be Observable too.
public class TodosList {
private final SignalCollection<Todo> todos = Signals.signal(new ArrayList<>());
public Todo add(String title){
// using factory method to create Todo object
Todo todo = Todo.from(title);
todos.add(todo);
return todo;
}
public void update(String id, String title){
Todo todo = todos.value().stream().filter(t -> t.getId().toString().equals(id)).findFirst()
.orElseThrow(() -> new RuntimeException("no task found with matching id"));
todo.getTitle().set(title);
}
public void toggle(String id){
Todo todo = todos.value().stream().filter(t -> t.getId().toString().equals(id)).findFirst()
.orElseThrow(() -> new RuntimeException("no task found with matching id"));
todo.getCompleted().set(!todo.getCompleted().value());
}
public void delete(String id){
Todo todo = todos.value().stream().filter(t -> t.getId().toString().equals(id)).findFirst()
.orElseThrow(() -> new RuntimeException("no task found with matching id"));
todos.remove(todo);
}
}
The magic sauce is in the choice of methods in the Signal
object used to access the underlying Signal values. There are three categories of values that are Observable.
- Scalar (anything that is neither a Collection nor a Map)
- Collection (Lists, Sets, etc)
- Dictionary (Map)
For all Scalar
values the .value()
and .value(arg)
methods are used to access and set respectively the underlying values without triggering effects. The .get()
and .set(arg)
methods however will register and trigger effects respectively. An effect is the behavior triggered when an Observable attribute is changed.
For Collection
values, the .value()
method is used to access the underlying Collection value without triggering effects. The .get()
, .forEach()
and .iterator()
methods will register effects. The .set(arg)
and .value(arg)
methods don't exists since they serve no useful purpose in this case. So to trigger effects, only three method are currently instrumented for that purpose in the SignalCollection - add
, addAll
and remove
.
For Dictionary
values, the .value()
method is equally used to access the underlying Map value without triggering effects. The .get()
, .forEach()
and .iterator()
methods will register effects. The .set(arg)
and .value(arg)
methods don't exists since they serve no useful purpose in this case. So to trigger effects, only three method are currently instrumented for that purpose in the SignalDictionary - put
, putAll
and remove
.
When observing a Collection
or a Map
, the Signals.observe()
method takes different arguments to accommodate the differences in these categories of classes. In this case, the TodosList registers an effect because todos.get()
method got accessed in the constructor, and the registered handler receives an event name
and the affected entity
as parameters. The event name
represents the name of the method
which triggered the effect.
public class TodosList {
private final SignalCollection<Todo> todos = Signals.signal(new ArrayList<>());
public TodosList() {
Signals.observe((event, entity) -> {
switch (event) {
case "add", "remove" -> System.out.printf("Event: %s, entity: %s\n", event, entity);
default -> System.out.printf("todos size: %d\n", todos.get().size());
}
});
}
// code omitted for brevity
}
To reiterate what just happened, accessing todos.get()
causes the registration of an effect function to happen, and this effect will subsequently be triggered by any add()
or remove()
invocations on the SignalCollection.
The main class will now look vastly cleaner than the previous times.
public class Main {
public static void main(String[] args) {
TodosList list = new TodosList();
// continue as normal
Todo t1 = list.add("wake up");
Todo t2 = list.add("make breakfast");
Todo t3 = list.add("watch tv");
list.update(t2.getId().toString(), "work out");
list.toggle(t1.getId().toString());
list.delete(t3.getId().toString());
}
}
And the output produced will tell the whole story of what is happening when the TodosList and TodoItems are getting updated in the main method above.
todos size: 0
Task title changed to wake up
Task completion changed to false
Event: add, entity: Todo{title=wake up, completed=false, id=4b2e720e-5510-4f35-bd13-4925ff6c6f57}
Task title changed to make breakfast
Task completion changed to false
Event: add, entity: Todo{title=make breakfast, completed=false, id=8be14779-0ec9-44c4-aa94-572d2d21aac0}
Task title changed to watch tv
Task completion changed to false
Event: add, entity: Todo{title=watch tv, completed=false, id=bd665225-8dba-421c-91d6-0b6fb78f5f75}
Task title changed to work out
Task completion changed to true
Event: remove, entity: Todo{title=watch tv, completed=false, id=bd665225-8dba-421c-91d6-0b6fb78f5f75}
The source code for the above example can be viewed in this gitlab repository for convenience.
The source code to this presentation can be found in this repo. A more detailed and non-trivial example is also presented in the same repo.