decorative yellow lines on background

Why this approach?

Experiments using Optimizely X’s Full Stack SDKs are typically implemented by using conditionals in your codebase to decide between feature variations.  As experiments progress and clear winners from experimentation emerge this conditional code can create technical debt, once competing variations are no longer needed.  

This method also creates dependencies between the variation conditions and experiment code since the if / else statements need to know the variation names in order to route the application to the appropriate code path.  

This can become a problem, and I’m often asked by Full Stack users, what the best practice is for developing experimentation code that doesn’t leave sections of irrelevant code inside business logic and allows for additional flexibility when introducing new feature variations.

To achieve those goals, there are a few options to consider when implementing an experiment:

Standard Implementation: In Business Logic

Implement the feature variation and variation selection code within the business logic.

  • Pros
    • Easy to implement in the short term.
  • Cons
    • Creates technical debt within business logic.
    • Creates a coupling between if / else conditions and experiment configurations.

Alternative 1: Using the Factory Pattern

Abstract the feature variation selection code into a Factory class using the Factory Pattern.

  • Pros
    • Experimentation logic is kept separate from business logic, minimizing technical debt.
  • Cons
    • Still coupled with experimentation configuration.

Alternative 2: Factory Pattern Using Reflection

Further abstract the variation code by using Reflection in the Factory class.

  • Pros
    • Experimentation logic is kept separate from business logic, minimizing technical debt.
    • Generic enough to be reused across experiments, there is no direct coupling to variation names.
    • Increases flexibility for adding or removing feature variations.
  • Cons
    • Reflection adds overhead, reducing performance.
    • Not a feature available in all languages.

Experiment Setup

Let’s dig into each of these to see how each would look using a real world example.  For instance let’s say we wanted to experiment with how we presented products to users with different sorting algorithms.

Using this example we’d setup an experiment in Optimizely Full Stack that would look something like:

Fig 1.

background pattern

graphical user interface, application

Here we’re experimenting with three different sorting algorithms that prioritize products based on price, the category they’re in, and their alphabetical name.  We’ll apply our three different implementation options to this sorting experiment.

Standard Implementation: In Business Logic

The sample code below is the Java example code presented if you start a new Java Full Stack experiment like the one shown above in Fig 1.

//Business Logic
//
Variation variation = this.optimizelyClient.activate(experimentName, userId);
if(variation.is("CategoryProductSort")){
CategoryComparator comparator = new CategoryComparator();
Collections.sort(unSortedList, comparator);
} else if(variation.is("NameProductSort")){
NameComparator comparator = new NameComparator();
Collections.sort(unSortedList, comparator);
} else if(variation.is("PriceProductSort")){
PriceComparator comparator = new PriceComparator();
Collections.sort(unSortedList, comparator);
}
sort.sort(sampleProductList());
//Continue Business Logic
//

Using this method, these conditionals are likely embedded directly into the business logic of the application.  In addition, the if / else conditions are tied directly to the variation names we’ve established in our Full Stack experiment configuration.  As the experiment progresses, one of the variations on how to sort will likely emerge as a clear winner leaving the other variations obsolete.  

Having to clean up the obsolete variations results in cruft code embedded directly in business logic, creating hard-to-remove technical debt.  Over time this technical debt can build up contributing to a messy codebase.

Feature Variables

Before we talk about the alternatives, I’d like to take a moment to introduce Feature Management. In Optimizely Full Stack you now have the ability to create Feature Flags which represent new or experimental functionality in your code.  Included with Feature Flags are Feature Variables.  With Variables, you can instrument Features with configurable components or parameters.  

We will be using these variables for the Factory Pattern implementation examples, since they are a cleaner fit than using variation names and provide more flexibility for specifying the name of the sorting algorithm in a Factory Pattern implementation..

Using Feature Variables

In the Factory class examples you will notice the line:

String className = this.optimizelyClient.getFeatureVariableString(featureFlag, featureVariable, userId);

That was used in the Standard Implementation example.

Instead of the line:

Variation variation = this.optimizelyClient.activate(experimentName, userId);

The use of the getFeatureVariableString(), which is based on Feature Variables, instead of activate(), which is based on the variation name, will give us more flexibility in how we can specify which algorithm we use.  This will come in handy as we get into our Factory class implementations.

Feature Flag Setup

Let’s look at how we’ve setup our Feature Flag for our sorting algorithm experiment:

Fig 2.

graphical user interface, text, application, email

Feature Flag in an Experiment

Used in an experiment the Feature Flag would then look like:

Fig 3.

graphical user interface, application

We will be using the class_name variable to specify the sorting implementation to be used.  You can see that the string data type used for the class_name variable allows us to use fully qualified class names which will be useful in our later examples.

Alternative 1: Using the Factory Pattern

As previously mentioned, an alternative to using the Standard Implementation would be to use the Factory Pattern.  The factory pattern is one of the “Gang of Four” design patterns which is used to solve the problem of creating objects without having to specify the exact class of the object being created.  This solution comes in handy when we’re trying to abstract conditional logic from business logic.

For this section, we’re going to use Java to demonstrate how to use the Factory Pattern with experimentation.  Most languages have their own particular manner of implementing the Factory Pattern which can then be used to apply a similar approach.

Java Inheritance Example

We first need to set the stage a bit for this section.  Let’s say we have a simple set of objects that follow a basic object oriented inheritance scenario:

diagram

Shape being the parent interface or abstract class that sets the requirement of any child object having to know how to print itself.  In an experimentation context the printSelf function would be the feature that is subject to experimentation or a controlled rollout.

This kind of inheritance structure would be akin to a more real world example.  For instance let’s revisit our product sorting example from before.  Instead of implementing the various sorting algorithms all within the experiment conditionals, we could encapsulate each implementation in its own class which we could call when a user is placed in a variation associated to that algorithm.

diagram

In the diagram above we see different sorting algorithm implementations encapsulated in child classes of sort.

Using the sorting example, let’s see how an application can take advantage of this encapsulation and inheritance structure:

The following code excerpt is using an OptimizelyFactory class to get only the version of Sort that’s relevant to the variation of the experiment.

OptimizelyFactory optimizely = new OptimizelyFactory(dataFile);
//
//
Sort sortingAlgorithm = optimizelyFactory.getExperimentImpl(“ProductSort”, userId);
sort.sort(sampleProductList());

Using the Standard Implementation, at this point you would receive a variation object from the activate method, which you would then need to use conditionals to settle on a variation code path.  

However, with the Factory Pattern, all you need to do is rely on the Factory class to deliver the appropriate implementation to you.  Moving the burden of the variation conditionals to the Factory class, and away from the business logic.  Improving the overall cleanliness and maintainability of your code.

Implementing the Factory Pattern

Let’s take a look at the getExperimentImpl method in OptimizelyFactory and see how this is done:

public ProductSort getExperimentImpl(String experimentName, String userId){
Variation variation = this.optimizelyClient.activate(experimentName, userId);
ProductSort retobj = null;
if(variation.getKey().equals("PriceProductSort")){
retobj = new CategoryProductSort();
} else if(variation.getKey().equals("NameProductSort")){
retobj = new NameProductSort();
} else if(variation.getKey().equals("CategoryProductSort")){
retobj = new PriceProductSort();
} else{
//Defaulting to price sorting
retobj = new PriceProductSort();
}
return retobj;
}

The getExperimentImpl method uses the previously mentioned getFeatureVariableString() method to get the name of the sorting algorithm to be used (in this example the fully qualified class name did not need to be used, we just used it for consistency with the other examples).   You can see that it takes the burden of creating the implementation object away from the main application code.  So the logic of creating separate code paths based on the variation a user is in, is abstracted away in the factory classes and the sorting child classes.

Looking back to our experiment setup in Fig 1.  You’ll see the Factory class has corresponding values for the experiment name and the class names.  This still creates the coupling between the if / else conditions and the experiment configuration but the conditions have now been removed from the business logic making the conditionals easier to remove once the experiment is over.

Alternative 2: Factory Pattern Using Reflection

Reflection is a feature of some object oriented programming languages that allows for inspection and instantiation of objects at runtime without having to know about their classes at compile time. This can be very useful when developing experimentation code.

Since the names of classes that a factory creates don’t have to be known at compile time, the experimentation Factory class can use the values of Optimizely Feature Variables to identify and instantiate objects for its clients.  

This decouples the factory class code from the experiment configuration, and allows the Factory class to remain generic enough to the point where a Factory class needs no knowledge of the experiment implementations at all.  Allowing us to use the same Factory class for any inheritance based experiment.

Reflection Resources:

Reflection Wikipedia

Java Reflection Tutorial

public T getExperimentImpl(String experimentName, String userId){
Variation variation = this.optimizelyClient.activate(experimentName, userId);
T retobj = null;
try {
String className = this.packageName + "." + variation.getKey();
Class cls = Class.forName(className);
Class partypes[] = new Class[0];
Constructor ct = cls.getConstructor(partypes);
retobj = (T)ct.newInstance();
}
catch (Throwable e) {
System.err.println(e);
e.printStackTrace();
return null;
}
return retobj;
}

The version of getExperimentImpl above uses the variables specified in Fig 3 and Java reflection to create the implementation objects.  The value from getFeatureVariableString() is used to determine which child class of Sort needs to be instantiated.  

It should be noted that generics are used here so any parent class can be used.  For the case of Sort we initialize the generic type when we create an instance of the OptimizelyReflectionFactory class.

OptimizelyReflectionFactory<ProductSort> optimizely = new OptimizelyReflectionFactory<ProductSort>();
//
//
ProductSort sort = optimizely.getExperimentImpl(“ProductSort”, input);
sort.sort(sampleProductList());

Reflection Performance

While the performance of Java reflection has improved over the years, it is important to note that reflection still does come at a performance cost in exchange for the abstraction convenience.  If performance considerations are paramount above convenience in your experimentation use cases, you should likely go with the conditional based Factory class presented earlier.

Other Languages

Reflection isn’t common across all languages, so you may not be able to automatically tie variation names/variables to class instantiations.  However, the factory pattern can be implemented in any language.  This allows us to take use the same approach anywhere the factory pattern can be used.

For factory pattern examples in other languages take a look at:

Node Factory Pattern

Python Factory Pattern

Github

For the full source code of both factory implementations, check it out on Github!