© Dmitri Nesteruk 2020
D. NesterukDesign Patterns in .NET Core 3https://doi.org/10.1007/978-1-4842-6180-4_23

23. Strategy

Dmitri Nesteruk1  
(1)
St. Petersburg, c.St-Petersburg, Russia
 
Suppose you decide to take an array or vector of several strings and output them as a list
  • Just

  • Like

  • This

If you think about the different output formats, you probably know that you need to take each element and output it with some additional markup. But in the case of languages such as HTML or LaTeX, the list will also need the start and end tags or markers.

We can formulate a strategy for rendering a list:
  • Render the opening tag/element.

  • For each of the list items, render that item.

  • Render the closing tag/element.

Different strategies can be formulated for different output formats, and these strategies can be then fed into a general, nonchanging algorithm to generate the text.

This is yet another pattern that exists in dynamic (runtime-replaceable) and static (generics-based, fixed) incarnations. Let’s take a look at both of them.

Dynamic Strategy

So our goal is to print a simple list of text items in the following formats :
public enum Output Format
{
  Markdown,
  Html
}
The skeleton of our strategy will be defined in the following base class:
public interface IListStrategy
{
  void Start(StringBuilder sb);
  void AddListItem(StringBuilder sb, string item);
  void End(StringBuilder sb);
}
Now, let us jump to our text processing component. This component would have a list-specific method called, say, AppendList() .
public class TextProcessor
{
  private StringBuilder sb = new StringBuilder();
  private IListStrategy listStrategy;
  public void AppendList(IEnumerable<string> items)
  {
    listStrategy.Start(sb);
    foreach (var item in items)
      listStrategy.AddListItem(sb, item);
    listStrategy.End(sb);
  }
  public override string ToString() => sb.ToString();
}

So we’ve got a buffer called sb where all the output goes, the listStrategy that we’re using for rendering lists, and of course AppendList() which specifies the set of steps that need to be taken to actually render a list with a given strategy.

Now, pay attention here. Composition, as used earlier, is one of two possible options that can be taken to allow concrete implementations of a skeleton algorithm. Instead, we could add functions such as AddListItem() as abstract or virtual members to be overridden by derived classes: that’s what the Template Method pattern does.

Anyways , back to our discussion, we can now go ahead and implement different strategies for lists, such as a HtmlListStrategy:
Public class HtmlListStrategy : ListStrategy
{
  public void Start(StringBuilder sb) => sb.AppendLine("<ul>");
  public void End(StringBuilder sb) => sb.AppendLine("</ul>");
  public void AddListItem(StringBuilder sb, string item)
  {
    sb.AppendLine($" <li>{item}</li>");
  }
}
By implementing the overrides, we fill in the gaps that specify how to process lists. We implement a MarkdownListStrategy in a similar fashion, but because Markdown does not need opening/closing tags, we only do work in the AddListItem() method :
public class MarkdownListStrategy : IListStrategy
{
  // markdown doesn't require list start/end tags
  public void Start(StringBuilder sb) {}
  public void End(StringBuilder sb) {}
  public void AddListItem(StringBuilder sb, string item)
  {
    sb.AppendLine($" * {item}");
  }
}
We can now start using the TextProcessor , feeding it different strategies and getting different results. For example:
var tp = new TextProcessor(); tp.SetOutputFormat(OutputFormat.Markdown);
tp.AppendList(new []{"foo", "bar", "baz"});
WriteLine(tp);
// Output:
// * foo
// * bar
// * baz
We can make provisions for strategies to be switchable at runtime – this is precisely why we call this implementation a dynamic strategy. This is done in the SetOutputFormat() method , whose implementation is trivial:
public void SetOutputFormat(OutputFormat format)
{
  switch (format) {
    case OutputFormat.Markdown:
      listStrategy = new MarkdownListStrategy();
      break;
    case OutputFormat.Html:
      listStrategy = new HtmlListStrategy();
      break;
    default:
      throw new ArgumentOutOfRangeException(nameof(format), format, null);
  }
}
Now, switching from one strategy to another is trivial, and you get to see the results straight away :
tp.Clear(); // erases underlying buffer
tp.SetOutputFormat(OutputFormat.Html);
tp.AppendList(new[] { "foo", "bar", "baz" });
WriteLine(tp);
// Output:
// <ul>
//   <li>foo</li>
//   <li>bar</li>
//   <li>baz</li>
// </ul>

Static Strategy

Thanks to the magic of generics, you can bake any strategy right into the type . Only minimal changes are necessary to the TextStrategy class:
public class TextProcessor<LS>
  where LS : IListStrategy, new()
{
  private StringBuilder sb = new StringBuilder();
  private IListStrategy listStrategy = new LS();
  public void AppendList(IEnumerable<string> items)
  {
    listStrategy.Start(sb);
    foreach (var item in items)
      listStrategy.AddListItem(sb, item);
    listStrategy.End(sb);
  }
  public override string ToString() => return sb.ToString();
}
What changed in the dynamic implementation is as follows: we added the LS generic argument, made a listStrategy member with this type, and started using it instead of the reference we had previously. The results of calling the adjusted AppendList() are identical to what we had before.
var tp = new TextProcessor<MarkdownListStrategy>();
tp.AppendList(new []{"foo", "bar", "baz"});
WriteLine(tp);
var tp2 = new TextProcessor<HtmlListStrategy>();
tp2.AppendList(new[] { "foo", "bar", "baz" });
WriteLine(tp2);

The output from the preceding example is the same as for the dynamic strategy. Note that we’ve had to make two instances of TextProcessor, each with a distinct list-handling strategy, since it is impossible to switch a type’s strategy midstream: it is baked right into the type.

Equality and Comparison Strategies

The most well-known use of the Strategy pattern inside .NET is, of course, the use of equality and comparison strategies.

Consider a simple class such as the following:
class Person
{
  public int Id;
  public string Name;
  public int Age;
}
As it stands, you can put several Person instances inside a List, but calling Sort() on such a list would be meaningless.
var people = new List<Person>();
people.Sort(); // does not do what you want

The same goes for comparisons using the == and != operators: at the moment, all those comparisons would do is a reference-based comparison.

We need to clearly distinguish two types of operations:
  • Equality checks whether or not two instances of an object are equal, according to the rules you define. This is covered by the IEquatable<T> interface (the Equals() method) as well as operators == and != which typically use the Equals() method internally.

  • Comparison allows you to compare two objects and find which one is less, equal, or greater than another. This is covered by the IComparable<T> interface and is required for things like sorting.

By implementing IEquatable<T> and IComparable<T>, every object can expose its own comparison and equality strategies. For example, if we assume that people have unique Ids, we can use that ID value for comparison:
public int CompareTo(Person other)
{
  if (ReferenceEquals(this, other)) return 0;
  if (ReferenceEquals(null, other)) return 1;
  return Id.CompareTo(other.Id);
}

So now, calling people.Sort() makes a kind of sense – it will use the built-in CompareTo() method that we’ve written. But there is a problem: a typical class can only have one default CompareTo() implementation for comparing the class with itself. The same goes for equality. So what if your comparison strategy changes at runtime?

Luckily, BCL designers have thought of that, too. We can specify the comparison strategy right at the call site, simply by passing in a lambda:
people.Sort((x, y) => x.Name.CompareTo(y.Name));

This way, even though Person’s default comparison behavior is to compare by ID , we can compare by name if we need to.

But that’s not all! There is a third way in which a comparison strategy can be defined. This way is useful if some strategies are common and you want to preserve them inside the class itself.

The idea is this: you define a nested class that implements the IComparer<T> interface . You then expose this class as a static variable:
public class Person
{
  // ... other members here
  private sealed class NameRelationalComparer :
 IComparer<Person>
  {
    public int Compare(Person x, Person y)
    {
      if (ReferenceEquals(x, y)) return 0;
      if (ReferenceEquals(null, y)) return 1;
      if (ReferenceEquals(null, x)) return -1;
      return string.Compare(x.Name, y.Name,
        StringComparison.Ordinal);
    }
  }
  public static IComparer<Person> NameComparer { get; }
    = new NameRelationalComparer();
}
As you can see, the preceding class defines a stand-alone strategy for comparing two Person instances using their names. We can now simply take a static instance of this class and feed it into the Sort() method:
people.Sort(Person.NameComparer);

As you may have guessed , the situation with equality comparison is fairly similar: you can use an IEquatable<T>, pass in a lambda, or generate a class that implements an IEqualityComparer<T>. Your choice!

Functional Strategy

The functional variation of the Strategy pattern is simple: all OOP constructs are simply replaced by functions. First of all, TextProcessor devolves from being a class to being a function. This is actually idiomatic (i.e., the right thing to do) because TextProcessor has a single operation.
let processList items startToken itemAction endToken =
  let mid = items |> (Seq.map itemAction) |> (String.concat " ")
  [startToken; mid; endToken] |> String.concat " "

The preceding function takes four arguments: a sequence of items, the starting token (note: it’s a token, not a function), a function for processing each element, and the ending token. Since this is a function, this approach assumes that processList is stateless, that is, it does not keep any state internally.

As you can see from earlier text, our strategy is not just a single, neatly self-contained element, but rather a combination of three different items: the start and end tokens as well as a function that operates upon each of the elements in the sequence. We can now specialize processList in order to implement HTML and Markdown processing as before:
let processListHtml items =
  processList items "<ul>" (fun i -> " <li>" + i + "</li>") "</ul>"
let processListMarkdown items =
  processList items "" (fun i -> " * " + i) ""
This is how you would use these specializations, with predictable results:
let items = ["hello"; "world"]
printfn "%s" (processListHtml items)
printfn "%s" (processListMarkdown items)

The interesting thing to note about this example is that the interface of processList gives absolutely no hints whatsoever as to what the client is supposed to provide as the itemAction. All they know it’s an 'a -> string, so we rely on them to guess correctly what it’s actually for.

Summary

The Strategy design pattern allows you to define a skeleton of an algorithm and then use composition to supply the missing implementation details related to a particular strategy. This approach exists in two incarnations:
  • Dynamic strategy simply keeps a reference to the strategy currently being used. Want to change to a different strategy? Just change the reference. Easy!

  • Static strategy requires that you choose the strategy at compile time and stick with it – there is no scope for changing your mind later on.

Should one use dynamic or static strategies? Well, dynamic ones allow you reconfiguration of the objects after they have been constructed. Imagine a UI setting which controls the form of the textual output: what would you rather have, a switchable TextProcessor or two variables of type TextProcessor<MarkdownStrategy> and TextProcessor<HtmlStrategy>? It’s really up to you.

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

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