Skip to content

Conversation

@mikecdavis
Copy link
Contributor

@mikecdavis mikecdavis commented Apr 30, 2019

Summary

  • Define new Notification interface to mark supported notification models.
  • Define new generic NotificationHandler<T> interface
  • Define NotifcationManager<T> class to own specific class of notifications.
  • Refactor NotificationCenter to hold a map of NotificationManager instances.
  • Define explicit ActivateNotification class
  • Refactor ActivateNotificationListener to implement NotificationHandler<ActivateNotification>
  • Define explicit TrackNotification class
  • Refactor TrackNotificationListener to implement NotificationHandler<TrackNotification>

The current patterns surrounding the NotificationCenter did not extend well and relied on Object... array arguments into the NotificationListener interface. This resulted in the need for dedicated abstract classes, enums and helper methods to be implemented for each notification. Moving to a generic NotificationHandler<T> interface and NotifcationManager<T> means that new notifications can be defined simply by implementing a new Notification model which is type safe, discoverable and self describing.

Test plan

Unit and integration testing along with the compatibility suite.

@coveralls
Copy link

coveralls commented Apr 30, 2019

Pull Request Test Coverage Report for Build 997

  • 130 of 145 (89.66%) changed or added relevant lines in 7 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage increased (+0.2%) to 89.79%

Changes Missing Coverage Covered Lines Changed/Added Lines %
core-api/src/main/java/com/optimizely/ab/notification/NotificationCenter.java 42 57 73.68%
Totals Coverage Status
Change from base Build 987: 0.2%
Covered Lines: 2911
Relevant Lines: 3242

💛 - Coveralls

@mikecdavis
Copy link
Contributor Author

This is failing compatibility-suite changes since I removed the explicit DecisionNotificationListener class and methods for a more extensible pattern. This should be resolved with updates to the test-app.

Copy link
Contributor

@aliabbasrizvi aliabbasrizvi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good.

We will have to update documentation here: https://docs.developers.optimizely.com/full-stack/docs/register-notification-listeners

Work is tracked in OASIS-4583


public int addHandler(NotificationHandler<T> newHandler) {

// Prevent registering a duplication listener.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit. duplicate.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to flip the map to be Map<NotificationHandler, Int>?

}

/**
* Convenience method for adding TrackNotification Handlers
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

adding DecisionNotification handlers

import com.optimizely.ab.event.LogEvent;

import javax.annotation.Nonnull;
import java.rmi.activation.ActivateFailedException;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this needed? I am not seeing it used below

package com.optimizely.ab.notification;

/**
* Interface used to identify supported annotations
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Supported notifications?

Copy link
Contributor

@loganlinn loganlinn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are going to identify the type of notification by its Class object, we should could do a little more to restrict the type hierarchy. Can we make the Notification implementations declared here final? it wouldn't make sense AFAICT to want to/be able to extend those three classes.

Also, since there are a closed set of notification types that the system supports, the Notification is ideally not public.

I think the subtleties of restricted type hierarchy is more clearly conveyed if the type family is isolated to a single file or package.

Copy link
Contributor

@loganlinn loganlinn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should try to add comments everywhere there’s a @Deprecated annotation to describe the canonical/preferred alternative


public int addHandler(NotificationHandler<T> newHandler) {

// Prevent registering a duplication listener.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to flip the map to be Map<NotificationHandler, Int>?


private static final Logger logger = LoggerFactory.getLogger(NotificationManager.class);

private final Map<Integer, NotificationHandler<T>> handlers = new HashMap<>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use LinkedHashMap so there’s consistent order in handler invocation?

}

@Override
public final void notify(ActivateNotification message) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it doesn’t break public API compatibility, it would nice to not be overloading Object.notify anymore...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, good point I can change that. Suggestion? handle? since the new interface is NotificationHandler<T>

private final AtomicInteger counter;

public NotificationManager(AtomicInteger counter) {
this.counter = counter;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems odd for this constructor to be public; could use a null check if it remains public.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made package private

@SuppressWarnings("unchecked")
public <T> NotificationManager<T> getNotificationManager(Class<? extends Notification> clazz) {
NotificationManager<T> newManager = new NotificationManager<>(counter);
NotificationManager<T> manager = (NotificationManager<T>) notifierMap.putIfAbsent(clazz, newManager);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use computeIfAbsent() to avoid creating throw-away instance when manager exists? Alternatively, the set of notification types is (all likely will remain) trivially small, so could just pre-populate the map.

decisionListenerHolder.clear();
for (NotificationType type : NotificationType.values()) {
clearNotificationListeners(type);
for (NotificationManager<?> manager : notifierMap.values()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: just clearing the entire notifierMap would be more consistent with the current scheme of lazy population used with putIfAbsent

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update: We're now creating a unmodifiable map containing all possible NotificationManager types, so clearing the individual managers is required.

public void send(Notification notification) {
NotificationManager handler = notifierMap.get(notification.getClass());
if (handler == null) {
return;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems worth logging a warn or error

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replaced with an OptimizelyRuntimeException since "we" control the notifications and should be catching this in our CI

package com.optimizely.ab.notification;

public interface NotificationHandler<T> {
void notify(T message);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left a comment elsewhere but I’ll leave one here too: not a great name for any Java method as every Object has notify() for very different purpose.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still need to do this.... sticking with "handle" since no other option has been proposed :)

@mikeproeng37
Copy link
Contributor

@mikecdavis
Copy link
Contributor Author

@loganlinn as always, thanks for the feedback:)

I ended up defining an unmodifiable Map of allowable notification types to control access in lieu of the Notification interface which proved difficult to constrain. So instead of lazily instantiating the set of NotificationManagers we're defining those up front to gate registration and notification sending and to fail fast during development of new notifications.

@mikeproeng37
Copy link
Contributor

@mikeproeng37 mikeproeng37 merged commit 4e28052 into master May 2, 2019
@mikecdavis mikecdavis deleted the mikecdavis/refactor-notification-center branch May 8, 2019 19:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants