-
Notifications
You must be signed in to change notification settings - Fork 31
Refact: Added builder pattern in decision listener #275
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Refact: Added builder pattern in decision listener #275
Conversation
…de constants insead of enum
…tification from notification center
| * @return true if removed otherwise false (if the notification is already registered, it returns false). | ||
| */ | ||
| public boolean removeNotificationListener(int notificationID) { | ||
| for (NotificationHolder holder : decisionListenerHolder) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mikecdavis What do you suggest here? There is separate logic to remove for DecisionListener object.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/**
* Remove the notification listener based on the notificationId passed back from addNotificationListener.
*
* @param notificationID the id passed back from add notification.
* @return true if removed otherwise false (if the notification is already registered, it returns false).
*/
public boolean removeNotificationListener(int notificationID) {
for (ArrayList<NotificationHolder> notificationHolders: notificationsListeners.values()) {
if (removeNotificationListener(notificationHolders)) {
return true;
}
}
return removeNotificationListener(decisionListenerHolder);
}
/**
* Helper method to iterate find NotificationHolder in an List identified by by the notificationId
*/
private static final public boolean removeNotificationListener(int notificationID, List<NotificationHolder>) {
for (NotificationHolder holder : decisionListenerHolder) {
if (holder.notificationId != notificationID) {
continue;
}
notificationsListeners.remove(holder);
logger.info("Notification listener removed {}", notificationID);
return true;
}
}
mikecdavis
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Much better thank you! There are a few more tweaks I'd like to see, and then one open question regarding a new normal for notification going forward.
| public void onDecision(@Nonnull String type, @Nonnull String userId, @Nonnull Map<String, ?> attributes, @Nonnull Map<String, ?> decisionInfo) { | ||
| decisionNotificationListenerInterface.onDecision(type, userId, attributes, decisionInfo); | ||
| public int addDecisionNotificationListener(DecisionNotificationListener decisionNotificationListener) { | ||
| notificationListenerID++; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should replace int notificationListenerID with an AtomicInteger and use the incrementAndGet() method.
https://stackoverflow.com/questions/25168062/why-is-i-not-atomic
| * @return true if removed otherwise false (if the notification is already registered, it returns false). | ||
| */ | ||
| public boolean removeNotificationListener(int notificationID) { | ||
| for (NotificationHolder holder : decisionListenerHolder) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/**
* Remove the notification listener based on the notificationId passed back from addNotificationListener.
*
* @param notificationID the id passed back from add notification.
* @return true if removed otherwise false (if the notification is already registered, it returns false).
*/
public boolean removeNotificationListener(int notificationID) {
for (ArrayList<NotificationHolder> notificationHolders: notificationsListeners.values()) {
if (removeNotificationListener(notificationHolders)) {
return true;
}
}
return removeNotificationListener(decisionListenerHolder);
}
/**
* Helper method to iterate find NotificationHolder in an List identified by by the notificationId
*/
private static final public boolean removeNotificationListener(int notificationID, List<NotificationHolder>) {
for (NotificationHolder holder : decisionListenerHolder) {
if (holder.notificationId != notificationID) {
continue;
}
notificationsListeners.remove(holder);
logger.info("Notification listener removed {}", notificationID);
return true;
}
}
| public void sendNotifications(DecisionNotification decision) { | ||
| for (NotificationHolder holder : decisionListenerHolder) { | ||
| try { | ||
| (holder.decisionNotificationListener).onDecision(decision); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove the parenthesis holder.decisionNotificationListener.onDecision(decision);
| import java.util.HashMap; | ||
| import java.util.Map; | ||
|
|
||
| public class FeatureVariableNotification extends DecisionNotification { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a pure builder. This does not need to extend DecisionNotification nor do we need a Builder subclass. Refactor as FeatureVariableDecisionNotificationBuilder a bit wordy, I know, but accurate.
Actually let's subclass under DecisionNotification so the interface will be:
DecisionNotification decisionNotification = DecisionNotification.newFeatureVariableBuilder()
...
.build();
Other "types" can be additional sub-classes.
| ***************************************************************************/ | ||
|
|
||
| package com.optimizely.ab.notification; | ||
| package com.optimizely.ab.notification.decisionInfo; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Relocate this back under com.optimizely.ab.notification nesting packages is often less ideal.
| @Nonnull String userId, | ||
| @Nonnull Map<String, ?> attributes, | ||
| @Nonnull Map<String, ?> decisionInfo); | ||
| void onDecision(@Nonnull DecisionNotification decisionNotification); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've been trying to see if we can use generics to maintain some higher order generality. Ideally NotificationLister originally would have been defined like:
public interface NotificationListener<T> {
void notify(T notification);
}
but since it wasn't trying to think the best way forward.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, you are right we can use generics but I think than we have to refactor Track and Activate listeners as well. If I am understanding this correctly?
| decisionInfo.put(VARIABLE_KEY, variableKey); | ||
| decisionInfo.put(VARIABLE_TYPE, variableType); | ||
| decisionInfo.put(VARIABLE_VALUE, variableValue); | ||
| if (featureDecision.decisionSource != null && featureDecision.decisionSource.equals(FeatureDecision.DecisionSource.EXPERIMENT)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| if (featureDecision.decisionSource != null && featureDecision.decisionSource.equals(FeatureDecision.DecisionSource.EXPERIMENT)) { | |
| if (featureDecision != null && FeatureDecision.DecisionSource.EXPERIMENT.equals(featureDecision.decisionSource)) { |
| return this; | ||
| } | ||
|
|
||
| public FeatureVariableDecisionNotificationBuilder withFeatureEnabled(Boolean featureEnabled) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| public FeatureVariableDecisionNotificationBuilder withFeatureEnabled(Boolean featureEnabled) { | |
| public FeatureVariableDecisionNotificationBuilder withFeatureEnabled(boolean featureEnabled) { |
| decisionInfo.put(SOURCE, FeatureDecision.DecisionSource.ROLLOUT); | ||
| } | ||
|
|
||
| this.type = NotificationCenter.DecisionNotificationType.FEATURE_VARIABLE.toString(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If type is not something that's settable, it's more clear if it's set in the constructor. Maybe it should even be parameter of BaseDecisionNotificationBuilder constructor.
|
|
||
| this.type = NotificationCenter.DecisionNotificationType.FEATURE_VARIABLE.toString(); | ||
|
|
||
| return super.build(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It doesn't seem like the builder class is buying us much and it makes this code harder to follow (stateful logic & having to jump between two classes). Would be more straightforward to just instantiate here
return new DecisionNotification(
type,
userId,
attributes,
decisionInfo);Regarding the defaulting of attributes = new HashMap<>(), that can be done at field declaration on this class or in the DecisionNotification constructor.
| this.type = type; | ||
| this.userId = userId; | ||
| this.attributes = attributes; | ||
| this.decisionInfo = decisionInfo; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are all of these fields allowed to be null? I see in BaseDecisionNotificationBuilder, that we default attributes to empty map -- that should be moved here (otherwise assert non-null in constructor) to make it consistent, otherwise nulls can slip in.
| public void onDecision(@Nonnull String type, @Nonnull String userId, @Nonnull Map<String, ?> attributes, @Nonnull Map<String, ?> decisionInfo) { | ||
| decisionNotificationListenerInterface.onDecision(type, userId, attributes, decisionInfo); | ||
| public int addDecisionNotificationListener(DecisionNotificationListener decisionNotificationListener) { | ||
| int id = this.notificationListenerID.incrementAndGet(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider moving this call to where id is actually needed. Since there are early-returns in this method, we could bump the id sequence unnecessarily.
| if (holder.notificationListener == notificationListener ) { | ||
| if (holder.notificationListener == notificationListener) { | ||
| logger.warn("Notification listener was already added"); | ||
| return -1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see that returning -1 is existing behavior. It's misleading and worth changing IMO. Maybe a later PR, but maybe add a TODO
| } | ||
|
|
||
| for (ArrayList<NotificationHolder> notificationHolders : notificationsListeners.values()) { | ||
| if(removeNotificationListener(notificationID, notificationHolders)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| if(removeNotificationListener(notificationID, notificationHolders)) { | |
| if (removeNotificationListener(notificationID, notificationHolders)) { |
| return true; | ||
| } | ||
|
|
||
| for (ArrayList<NotificationHolder> notificationHolders : notificationsListeners.values()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| for (ArrayList<NotificationHolder> notificationHolders : notificationsListeners.values()) { | |
| for (List<NotificationHolder> notificationHolders : notificationsListeners.values()) { |
| * @param notificationHolderList list from which to remove notification listener. | ||
| * @return true if removed otherwise false | ||
| */ | ||
| private boolean removeNotificationListener(int notificationID, ArrayList<NotificationHolder> notificationHolderList) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| private boolean removeNotificationListener(int notificationID, ArrayList<NotificationHolder> notificationHolderList) { | |
| private boolean removeNotificationListener(int notificationID, List<NotificationHolder> notificationHolderList) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
side-note: a Map, like LinkedHashMap, keyed by id would make things easier but also retain ordering...
| decisionInfo.put(VARIABLE_TYPE, variableType); | ||
| decisionInfo.put(VARIABLE_VALUE, variableValue); | ||
| if (featureDecision.decisionSource != null && featureDecision.decisionSource.equals(FeatureDecision.DecisionSource.EXPERIMENT)) { | ||
| if (featureDecision.decisionSource != null && FeatureDecision.DecisionSource.EXPERIMENT.equals(featureDecision.decisionSource)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential NullPointerException if featureDecision is null
mikecdavis
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor remaining comment, and I think you should consider @loganlinn comments as well.
| return new FeatureVariableDecisionNotificationBuilder(); | ||
| } | ||
|
|
||
| private static class BaseDecisionNotificationBuilder { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This base class is not needed.
|
@loganlinn can you take another look? |
aliabbasrizvi
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
Summary
Once these changes will get approved than we can move forward and we can apply these changes in all decision listener PRs