Skip to content

Instantly share code, notes, and snippets.

@andyg0808

andyg0808/strategy.md

Last active Mar 5, 2020
Embed
What would you like to do?
Strategy Pattern

Note: I'm not 100% certain of my descriptions vs. how others might define things; hopefully this is enough to get an intuative idea of what's going on.

The Strategy pattern lets you change how a function or method does its job. It might be helpful to think of it in terms of refactoring shapes. When you have two functions with similar parts at the top, you break that top part out into a new function. When they have similar parts at the bottom, you break that out into a new function. In both of those cases, you can replace the original function call with a call to the new "top half" function followed by a call to the new "bottom half" function. But if the similar parts are in the middle of the function, you can break them out into a strategy. This lets you replace a call to each of the specialized methods with a call to the new combined function, passing the appropriate strategy to handle the bit in the middle.

Consider the following two functions:

/**
 * @param $shipments Shipment[] List of Shipments to find dates for
 * @return Date[] The dates of the shipments, sorted in ascending order
 */
function getAscendingDates($shipments) {
  $dates = [];
  foreach ($shipments as $shipment) {
    $dates[] = $shipment->date;
  }
  sort($dates);
  return $dates;
}

/**
 * @param $shipments Shipment[] List of Shipments to find dates for
 * @return Date[] The dates of the shipments, sorted in descending order
 */
function getDescendingDates($shipments) {
  $dates = [];
  foreach ($shipments as $shipment) {
    $dates[] = $shipment->date;
  }
  rsort($dates);
  return $dates;
}

You might think, "There's way too much duplication between those. Let's pull them together:

/**
 * @param $shipments Shipment[] List of Shipments to find dates for
 * @param $ascending bool Sort in ascending order if `true`, descending if false.
 * @return Date[] The dates of the shipments, sorted according to `$ascending`
 */
function getAscendingDates($shipments, $ascending=true) {
  $dates = [];
  foreach ($shipments as $shipment) {
    $dates[] = $shipment->date;
  }
  if ($ascending) {
    sort($dates);
  } else {
    rsort($dates);
  }
  return $dates;
}

But then, when you want to add another option, the code gets more and more complicated:

/**
 * @param $shipments Shipment[] List of Shipments to find dates for
 * @param $ascending bool|"day" Sort in ascending order if `true`, descending if false, by day if "day".
 * @return Date[] The dates of the shipments, sorted according to `$ascending`
 */
function getAscendingDates($shipments, $ascending=true) {
  $dates = [];
  foreach ($shipments as $shipment) {
    $dates[] = $shipment->date;
  }
  if ($ascending === "day") {
    usort($dates, function($a, $b) {
      return $a->day <=> $b->day;
    });
  } elseif ($ascending) {
    sort($dates);
  } else {
    rsort($dates);
  }
  return $dates;
}

Now it's got a bunch of semi-related cases being handled. We can instead pull out a strategy object:

interface SortStrategy {
  /** Returns a sorted version of its argument */
  public function sort(array $list): array;
}

class AscendingSort implements SortStrategy {
  public function sort(array $list) {
    sort($list);
    return $list;
  }
}

…

class DaySort implements SortStrategy {
  public function sort(array $dates) {
    usort($dates, function($a, $b) {
      return $a->day <=> $b->day;
    });
    return $dates;
  }
}

/**
 * @param $shipments Shipment[] List of Shipments to find dates for
 * @param $strategy SortStrategy A SortStrategy instance to sort with.
 * @return Date[] The dates of the shipments, sorted according to `$strategy`
 */
function getAscendingDates($shipments, SortStrategy $strategy) {
  $dates = [];
  foreach ($shipments as $shipment) {
    $dates[] = $shipment->date;
  }
  return $strategy->sort($dates);
}

Now we can pass any strategy we want, instead of being bound by the original implementation.

This is very similar to just passing a closure; it gets more interesting if there's more than one method on the strategy interface, so you can have them change together.

In the case where you're working with methods on a class, you can avoid the need to add the new strategy as an argument to the function. Instead, you store the strategy in an instance variable on the class. Then all the class methods can call it, and depending on what strategy you put in that variable, exactly what happens will vary. Notice that this means the class methods don't need to care about what it's doing at all; they can just call it and assume that it'll do the right thing. Further, you can now add a very large number of strategies without needing to change the consumer class, because it doesn't need to know anything other than the interface of the strategy.

The refactoring to untangle a conditional using Strategy looks like Replace Conditional with Polymorphism. When you use a strategy in place of switching on a state variable, you have State Pattern.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment