Strategy

It has been said that there is more than one way to skin a cat. I have, wisely, never looked into how many ways there are. The same is frequently true for algorithms in computer programming. Frequently there are numerous versions of an algorithm that trades off memory usage for CPU usage. Sometimes there are different approaches that provide different levels of fidelity. For example, performing a geo-location on a smart phone typically uses one of three different sources of data:

  • GPS chip
  • Cell phone triangulation
  • Nearby WiFi points

Using the GPS chip provides the highest level of fidelity however it is also the slowest and requires the most battery. Looking at the nearby WiFi points requires very little energy and is very quick, however it provides poor fidelity.

The strategy pattern provides a method of swapping these strategies out in a transparent fashion. In a traditional inheritance model each strategy would implement the same interface which would allow for any of the strategies to be swapped in. The following diagram shows multiple strategies that could be swapped in:

Strategy

Selecting the correct strategy to use can be done in a number of different ways. The simplest method is to select the strategy statically. This can be done through a configuration variable or even hard coded. This approach is best for times when the strategy changes infrequently or is specific to a single customer or user.

Alternately an analysis can be run on the dataset on which the strategy is to be run and then a proper strategy selected. If it is known that strategy A works better than strategy B when the data passed in is clustered around a mean, then a fast algorithm for analyzing spread could be run first and then the appropriate strategy selected.

If a particular algorithm fails on data of a certain type, this too can be taken into consideration when choosing a strategy. In a web application this can be used to call a different API depending on the shape of data. It can also be used to provide a fallback mechanism should one of the API endpoints be down.

Another interesting approach is to use progressive enhancement. The fastest and least accurate algorithm is run first to provide rapid user feedback. At the same time a slower algorithm is also run and, when it is finished, the superior results are used to replace the existing results. This approach is frequently used in the GPS situation outlined above. You may notice when using a map on a mobile device your location is updated a moment after the map loads; this is an example of progressive enhancement.

Finally, the strategy can be chosen completely at random. It sounds like a strange approach but can be useful when comparing the performance of two different strategies. In this case, statistics would be gathered about how well each approach works and an analysis run to select the best strategy. The strategy pattern can be the foundation for A/B testing.

Selecting which strategy to use can be an excellent place to apply the factory pattern.

Implementation

In the land of Westeros there are no planes, trains, or automobiles but there is still a wide variety of different ways to travel. One can walk, ride a horse, sail on a seagoing vessel, or even take a boat down the river. Each one has different advantages and drawbacks but in the end they still take a person from point A to point B. The interface might look something like the following:

export interface ITravelMethod{
  Travel(source: string, destination: string) : TravelResult;
}

The travel result communicates back to the caller some information about the method of travel. In our case we track how long the trip will take, what the risks are, and how much it will cost:

class TravelResult {
  constructor(durationInDays, probabilityOfDeath, cost) {
    this.durationInDays = durationInDays;
    this.probabilityOfDeath = probabilityOfDeath;
    this.cost = cost;
  }
}

In this scenario we might like to have an additional method which predicts some of the risks to allow for automating selection of a strategy.

Implementing the strategies is as simple as the following:

class SeaGoingVessel {
  Travel(source, destination) {
    return new TravelResult(15, .25, 500);
  }
}

class Horse {
  Travel(source, destination) {
    return new TravelResult(30, .25, 50);
  }
}

class Walk {
  Travel(source, destination) {
    return new TravelResult(150, .55, 0);
  }
}

In a traditional implementation of the strategy pattern the method signature for each strategy should be the same. In JavaScript there is a bit more flexibility as excess parameters to a function are ignored and missing parameters can be given default values.

Obviously, the actual calculations around risk, cost, and duration would not be hard coded in an actual implementation. To make use of these one needs only to do the following:

var currentMoney = getCurrentMoney();
var strat;
if (currentMoney> 500)
  strat = new SeaGoingVessel();
else if (currentMoney> 50)
  strat = new Horse();
else
  strat = new Walk();
var travelResult = strat.Travel();

To improve the level of abstraction for this strategy we might replace the specific strategies with more generally named ones that describe what it is we're optimizing for:

var currentMoney = getCurrentMoney();
var strat;
if (currentMoney> 500)
  strat = new FavorFastestAndSafestStrategy();
else
  strat = new FavorCheapest();
var travelResult = strat.Travel();

Strategy is a very useful pattern in JavaScript. We're also able to make the approach much simpler than in a language which doesn't use prototype inheritance: there is no need for an interface. We don't need to return the same shaped object from each of the different strategies. So long as the caller is somewhat aware that the returned object may have additional fields, this is a perfectly reasonable, if difficult to maintain, approach.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.188.91.44