3
\$\begingroup\$

I was thinking of adding a very high-level implementation of the event/observer pattern to my project. The implementation would manage adding and removing observers as "needed". You can use an almost DLS-like API to define when an observer should be added or removed.

Here is an example of the API in use. We want to track mouse dragging along a path:

public void registerMousePathTracker(
        Mouse mouse,
        Consumer<List<Point>> callback
)
{
    final List<Point> points = new ArrayList<>();

    observe(mouse)
        .when(MouseDown.class)
        .Do(evt -> {
            points.clear();
            points.add(evt.getPoint());
        })
        .once()
        .laterWhen(MouseMove.class)
        .Do(evt -> {
            points.add(evt.getPoint());
        })
        .until(MouseUp.class)
        .thenDo(evt -> {
            points.add(evt.getPoint());
            callback.accept(points);
        })
        .once()
        .repeatIndefinitely()
    ;

}

This code defines 3 observers:

  • when the mouse is pressed down we clear the points list and add the point where the mouse was pressed. Then we remove this observer from mouse and add the next observer.
  • every time the mouse is being moved we add a point to the points list.
  • when the mouse is released (no longer pressed) we remove the second observer and add the third. The third observer adds the current mouse position to the points list and calls the callback that was passed as a parameter. Afterwards the third observer removes itself from the mouse.
  • After the third observer was removed the first observer is added again to create a full circle.

The observe-Method is defined by the ObserverManager interface:

public interface ObserverManager {

    public DefineEventType observe(EventSrc eventSource);

    @SuppressWarnings("unchecked")
    public default <X extends Event> Consumer<X> nothing() {
        return (Consumer<X>) DO_NOTHING;
    }

    public static final Consumer<Event> DO_NOTHING = e -> {};

    public static interface DefineEventType {

        public <X extends Event> DefineAction<X> when(Class<X> eventType);

    }

    public static interface DefineAction<X extends Event> {

        public DefineCondition<X> Do(Consumer<X> action);

    }

    public static interface DefineCondition<X extends Event> {

        public DefineChainedEvent once();

        public void always();

        public DefineChainedEvent While(Predicate<X> continueCondition);

        public DefineChainedEvent until(Predicate<X> stopCondition);

        public <Y extends Event> DefineFollowUpAction<Y> until(Class<Y> stopEventType);

    }

    public static interface DefineChainedEvent {

        public <X extends Event> DefineAction<X> laterWhen(Class<X> event);

        public void repeatIndefinitely();

    }

    public static interface DefineFollowUpAction<X extends Event> extends DefineChainedEvent {

        public DefineCondition<X> thenDo(Consumer<X> action);

    }

}

There is one implementation of this interface which could either be used as a delegate or as a super-class:

public class ObserverManagerImpl implements ObserverManager {

    @Override
    public DefineEventType observe(EventSrc eventSource) {
        return new ObserveBuilder(this, eventSource);
    }

    @SuppressWarnings("rawtypes")
    static class ObserveBuilder implements DefineEventType, DefineChainedEvent, DefineAction, DefineFollowUpAction, DefineCondition {

        private final ManagedObserver first;
        private ManagedObserver latest;
        private Class mostRecentStopEvent;

        ObserveBuilder(ObserverManager source, EventSrc target) {
            first = new ManagedObserver(source, target);
            latest = first;
        }

        private void registerIfNeeded() {
            if (latest == first) {
                first.register();
            }
        }

        @SuppressWarnings("unchecked") @Override
        public <X extends Event> DefineAction<X> when(Class<X> event) {
            latest.evtType = event;
            return this;
        }

        @SuppressWarnings("unchecked") @Override
        public <X extends Event> DefineAction<X> laterWhen(Class<X> event) {
            ManagedObserver prev = latest;
            latest = new ManagedObserver(prev.source, prev.target);
            latest.evtType = event;
            prev.followUp = latest;
            return this;
        }

        @SuppressWarnings("unchecked") @Override
        public DefineCondition Do(Consumer action) {
            latest.action = action;
            return this;
        }

        @SuppressWarnings("unchecked") @Override
        public DefineCondition thenDo(Consumer action) {
            ManagedObserver prev = latest;
            latest = new ManagedObserver(prev.source, prev.target);
            latest.evtType = mostRecentStopEvent;
            prev.followUp = latest;
            return Do(action);
        }

        @Override
        public DefineChainedEvent once() {
            Class<?> evtType = latest.evtType;
            latest.unregisterCondition = evt -> evtType.isInstance(evt);
            registerIfNeeded();
            return this;
        }

        @Override
        public void always() {
            latest.unregisterCondition = null;
            registerIfNeeded();
        }

        @Override
        public DefineFollowUpAction until(Class stopEvent) {
            mostRecentStopEvent = stopEvent;
            latest.unregisterCondition = evt -> stopEvent.isInstance(evt);
            registerIfNeeded();
            return this;
        }

        @SuppressWarnings("unchecked") @Override
        public DefineChainedEvent until(Predicate stopCondition) {
            latest.unregisterCondition = evt -> stopCondition.test(evt);
            registerIfNeeded();
            return this;
        }

        @SuppressWarnings("unchecked") @Override
        public DefineChainedEvent While(Predicate continueCondition) {
            latest.unregisterCondition = evt -> !continueCondition.test(evt);
            registerIfNeeded();
            return this;
        }

        @Override
        public void repeatIndefinitely() {
            latest.followUp = first;
        }
    }

}

The ManagedObserver class looks like this:

class ManagedObserver implements Observer {

    final ObserverManager source;
    final EventSrc target;
    Class<? extends Event> evtType;
    Consumer<Event> action;
    Predicate<Event> unregisterCondition;
    ManagedObserver followUp;

    ManagedObserver(ObserverManager source, EventSrc target) {
        this.source = source;
        this.target = target;
    }

    @Override
    public void onEvent(Event evt) {
        if (evtType.isInstance(evt)) {
            action.accept(evt);
        }
        if (unregisterCondition != null && unregisterCondition.test(evt)) {
            unregister();
            if (followUp != null) {
                followUp.register();
                followUp.onEvent(evt);
            }
        }
    }

    void register() {
        target.addObserver(this);
    }

    private void unregister() {
        target.removeObserver(this);
    }

}

My main concerns are:

  1. the naming of the methods in ObserverManager. The methods "Do" and "While" use capitalization in their names (because these words are java keywords) but in my opinion these names are the "perfect" fit for their purposes.
  2. feature completeness. Will I be able to use this throughout my application or will this become a huge investment for little return because it lacks the features I will need.
  3. I consider the ObserverManagerImpl class a rather "dirty" implementation. On the other hand, I also think that this may be okay in a situation like this where it is part of a closed system that is not supposed to be touched after it has been finished once.

What are your thoughts and opinions? All feedback and suggestions for improvement are welcome.


Edit:

I changed the ObserverManager interface & implementation in such a way to make sure both "until" methods work the same way. I guess it makes the implementation even more ugly. Advise is appreciated.

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$
  1. the naming of the methods in ObserverManager. The methods "Do" and "While" use capitalization in their names (because these words are java keywords) but in my opinion these names are the "perfect" fit for their purposes.

Synonyms. What about perform instead of Do and whilst or asLongAs (for the non-Brits) for while?

  1. feature completeness. Will I be able to use this throughout my application or will this become a huge investment for little return because it lacks the features I will need.

I'm always very wary of generic functionality like this. You have to make sure that it is reasonably well behaved because if it needs redesign or if it had bugs, you may end up having to update all the applications and libraries that use it. Furthermore, you'll be the only person that fully understands what's going on.

  1. I consider the ObserverManagerImpl class a rather "dirty" implementation. On the other hand, I also think that this may be okay in a situation like this where it is part of a closed system that is not supposed to be touched after it has been finished once.

Sure, it won't harm here, but if it is deployed elsewhere then you need to test it, document it and make sure it is flexible enough.


I'm slightly worried about the number of suppressed warnings - especially those for the generics. That's OK for some libraries if they are well designed, but it's a bit tricky for me to evaluate that.


All in all, it looks well designed, but it is rather complex for what it tries to accomplish as well.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.