Skip to content

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

Merged

Conversation

mnoman09
Copy link
Contributor

@mnoman09 mnoman09 commented Mar 22, 2019

Summary

  • Refactored FeatureVariable Decision listener PR and applied builder pattern
  • Updated notification listener to directly send notification from it and added its method

Once these changes will get approved than we can move forward and we can apply these changes in all decision listener PRs

@@ -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) {
Copy link
Contributor

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.

Copy link
Contributor

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;
    }
}

@msohailhussain msohailhussain requested a review from a team March 22, 2019 18:11
Copy link
Contributor

@mikecdavis mikecdavis left a 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++;
Copy link
Contributor

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) {
Copy link
Contributor

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);
Copy link
Contributor

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 {
Copy link
Contributor

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;
Copy link
Contributor

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);
Copy link
Contributor

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.

Copy link
Contributor Author

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?

@mnoman09 mnoman09 requested review from mikecdavis and msohailhussain and removed request for msohailhussain March 25, 2019 14:37
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)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
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) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
public FeatureVariableDecisionNotificationBuilder withFeatureEnabled(Boolean featureEnabled) {
public FeatureVariableDecisionNotificationBuilder withFeatureEnabled(boolean featureEnabled) {

decisionInfo.put(SOURCE, FeatureDecision.DecisionSource.ROLLOUT);
}

this.type = NotificationCenter.DecisionNotificationType.FEATURE_VARIABLE.toString();
Copy link
Contributor

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();
Copy link
Contributor

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;
Copy link
Contributor

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();
Copy link
Contributor

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;
Copy link
Contributor

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)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
if(removeNotificationListener(notificationID, notificationHolders)) {
if (removeNotificationListener(notificationID, notificationHolders)) {

return true;
}

for (ArrayList<NotificationHolder> notificationHolders : notificationsListeners.values()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
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) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
private boolean removeNotificationListener(int notificationID, ArrayList<NotificationHolder> notificationHolderList) {
private boolean removeNotificationListener(int notificationID, List<NotificationHolder> notificationHolderList) {

Copy link
Contributor

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...

@mnoman09 mnoman09 requested a review from loganlinn March 27, 2019 12:41
@@ -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)) {
Copy link
Contributor

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

@mnoman09 mnoman09 requested a review from loganlinn March 28, 2019 13:53
Copy link
Contributor

@mikecdavis mikecdavis left a 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 {
Copy link
Contributor

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.

@aliabbasrizvi
Copy link
Contributor

@loganlinn can you take another look?

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.

LGTM

@aliabbasrizvi aliabbasrizvi merged commit 158bee4 into mnoman/getFeatVarListener Apr 1, 2019
@aliabbasrizvi aliabbasrizvi deleted the mnoman/BuildPatrnOnDecision branch April 1, 2019 18:35
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.

5 participants