-
Notifications
You must be signed in to change notification settings - Fork 32
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
@@ -194,6 +204,13 @@ public int addNotificationListener(NotificationType notificationType, Notificati | |||
* @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;
}
}
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
@@ -194,6 +204,13 @@ public int addNotificationListener(NotificationType notificationType, Notificati | |||
* @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.
@@ -14,23 +14,43 @@ | |||
* limitations under the License. * | |||
***************************************************************************/ | |||
|
|||
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.
@@ -176,12 +186,12 @@ public int addNotificationListener(NotificationType notificationType, Notificati | |||
} | |||
|
|||
for (NotificationHolder holder : notificationsListeners.get(notificationType)) { | |||
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...
@@ -149,7 +141,7 @@ public DecisionNotification build() { | |||
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)) { | |||
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
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? |
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