How to?Wzór.9 Mam więc listę napisów (Stefan,Jarek,Bajek) i chciałbyś je wyświetlić w specyficzny sposób. Możesz to zrobić po przecinku : (Stefan,Jarek,Bajek).

A może wyświetlić je po kropce : (Stefan.Jarek.Bajek)

A może wyświetlić je korzystają z elementu <ul><li> w HTML

  • Stefan
  • Jarek
  • Bajek

Każdy z tych formatów wymaga swojej logiki przetłumaczenia listy napisów na odpowiedni wynik.

Można by powiedzieć, że strategii.

Co, jeśli bym chciał wybierać tę strategię w trakcie działania programu? Zobaczmy jak wzorzec projektowy strategi działa.

Zmienna strategia działania

Naszym celem jest stworzenie listy napisów na różne sposoby. Najpierw określimy, w jakich formatach będzie łączyć napisy

Będziemy mieli przecinki,kropki, formatowanie pierwszego elementu, styl Markdown i listę HTML.

public enum OutputFormat
{
    Commas,
    Dots,
    FirstElement,
    Markdown,
    Html
}

Szkieletem naszej strategii będzie ten interfejs.

Będę miał możliwości obsłużenia listy przed i po jej utworzeniu poprzez metody Start i End.

Dla każdego elementu listy będę miał też informację czy jest to ostatni, czy pierwszy element listy.

public interface IListStrategy
{
    void Start(StringBuilder sb);
    void AddListItem(StringBuilder sb, string item,
        bool first, bool last);
    void End(StringBuilder sb);
}

Pora napisać klasę, która będzie przetwarzać elementy. Do metody AppendList dodamy listę słów, a później używając metody ToString(), wyrzucimy odpowiednio sformatowaną listę w zależności od podanej przez nas strategii.

Po zwrocie napisu trzeba wyczyścić buffor StringBuildera. Oznacza to, że każdym razem metoda ToString() może zwrócić inaczej sformatowaną listę.

Domyślnie strategia jest pusta zgodnie ze wzorcem NullObject. Czyli nie będzie robić nic.

public class TextProcessor
{
    private StringBuilder sb = new StringBuilder();
    private IListStrategy listStrategy = new NullListStrategy();
    private List<string> words = new List<string>();;

    public void AppendList(IList<string> items)
    {
        words.AddRange(items);
    }

    public override string ToString()
    {
        listStrategy.Start(sb);
    
        for (int i = 0; i < words.Count(); i++)
        {
            if (i == 0)
                listStrategy.AddListItem(sb, words[i], true, false);
            else if (i != words.Count() - 1)
                listStrategy.AddListItem(sb, words[i], false, false);
            else
                listStrategy.AddListItem(sb, words[i], false, true);
        }
    
        listStrategy.End(sb);
        var result = sb.ToString();
        sb.Clear();
        return result;
    }


    public void SetOutputFormat(OutputFormat format)
    {
        throw new NotImplementedException();
    }
}

Teraz spójrzmy na implementacje naszych strategii. Oto strategia pusta zgodna ze wzorcem Null Object.

public class NullListStrategy : IListStrategy
{
    public void Start(StringBuilder sb) => sb.AppendLine("");
    public void End(StringBuilder sb) => sb.AppendLine("");

    public void AddListItem(StringBuilder sb, string item,
         bool first, bool last)
    {

    }
}

Tutaj mamy strategię kropkową.  Tutaj muszę dopilnować, aby ostatni element nie miał dodanej kropki .

public class DotListStrategy : IListStrategy
{
    public void Start(StringBuilder sb) { }
    public void End(StringBuilder sb) { }

    public void AddListItem(StringBuilder sb, string item,
         bool first, bool last)
    {
        if (!last)
            sb.Append($"{item}.");
        else
            sb.Append($"{item}");
    }
}

Strategia przycinkowa działa w podobny sposób.

public class CommaListStrategy : IListStrategy
{
    public void Start(StringBuilder sb) { }
    public void End(StringBuilder sb) { }

    public void AddListItem(StringBuilder sb, string item,
         bool first, bool last)
    {
        if (!last)
            sb.Append($"{item},");
        else
            sb.Append($"{item}");
    }
}

Strategia pierwszego elementu dodaje trzy dwu kropki przy pierwszym elementu,a później robimy to samo co w innych strategiach.

public class FirstElementListStrategy : IListStrategy
{
    public void Start(StringBuilder sb) { }
    public void End(StringBuilder sb) { }

    public void AddListItem(StringBuilder sb, string item,
         bool first, bool last)
    {
        if (first)
            sb.Append($"{item}:::");
        else
        {
            if (!last)
                sb.Append($"{item},");
            else
                sb.Append($"{item}");
        }

    }
}

Strategia HTML przed tworzeniem listy dodaje tag HTML <UL>, po stworzeniu listy dodajemy jej zamknięcie </UL>

Każde słowo jest także udekorowane przez tag <LI>. Tym razem też korzystam z metody AppendLine, co oznacza, że każdy elementy zacznie się od nowej linij.

public class HtmlListStrategy : IListStrategy
{
    public void Start(StringBuilder sb) => sb.AppendLine("<ul>");
    public void End(StringBuilder sb) => sb.AppendLine("</ul>");

    public void AddListItem(StringBuilder sb, string item,
         bool first, bool last)
    {
        sb.AppendLine($" <li>{item}</li>");
    }
}

Strategia Markdown nie potrzebuje tagów,a więc tylko do poszczególnych słów dodajemy gwiazdki.

public class MarkdownListStrategy : IListStrategy
{
    public void Start(StringBuilder sb) { }
    public void End(StringBuilder sb) { }
    public void AddListItem(StringBuilder sb, string item,
         bool first, bool last)
    {
        sb.AppendLine($" * {item}");
    }
}

Pozostało nam już tylko obsłużyć metodę "SetOutputFormat" w klasie TextProcessor.  Mamy w końcu dynamicznie zmieniać strategie działania naszej klasy. 

public void SetOutputFormat(OutputFormat format)
{
    switch (format)
    {
        case OutputFormat.Commas:
            listStrategy = new CommaListStrategy();
            break;
        case OutputFormat.Dots:
            listStrategy = new DotListStrategy();
            break;
        case OutputFormat.FirstElement:
            listStrategy = new FirstElementListStrategy();
            break;
        case OutputFormat.Html:
            listStrategy = new HtmlListStrategy();
            break;
        case OutputFormat.Markdown:
            listStrategy = new MarkdownListStrategy();
            break;
        default:
            throw new ArgumentOutOfRangeException(nameof(format), format, null);
    }
}

Sprawdźmy, czy kod działa prawidłowo i zgodnie z zamysłem.

TextProcessor textProcessor = new TextProcessor();

textProcessor.SetOutputFormat(OutputFormat.Commas);
textProcessor.AppendList(new[]
{ "Przemawianie","Programowanie","Granie" });

Console.WriteLine(textProcessor);
Console.WriteLine("");
textProcessor.SetOutputFormat(OutputFormat.Dots);
Console.WriteLine(textProcessor);
Console.WriteLine("");
textProcessor.SetOutputFormat(OutputFormat.FirstElement);
Console.WriteLine(textProcessor);
Console.WriteLine("");
textProcessor.SetOutputFormat(OutputFormat.Html);
Console.WriteLine(textProcessor);
Console.WriteLine("");
textProcessor.SetOutputFormat(OutputFormat.Markdown);
Console.WriteLine(textProcessor);
Console.ReadLine();

Oto wyniki:

strategy.PNG

Teraz spójrzmy czy może lepiej rozwiązać podobny problem przy pomocy klas generycznych. 

Statyczna strategia

Dzięki magi klas generycznych możemy wyrzucić głupie typy wyliczeniowe i skrócić bardzo kod.

Gdy stworzymy już tę klasę, to nie będziemy już zmieniać jej strategii. Dlatego większość kodu z metody ToString() przeszło do metody AppendList();

public class TextProcessor<LS> where LS : IListStrategy, new()
{
    private StringBuilder sb = new StringBuilder();
    private IListStrategy listStrategy = new LS();

    public void AppendList(IList<string> items)
    {
        listStrategy.Start(sb);

        for (int i = 0; i < items.Count(); i++)
        {
            if (i == 0)
                listStrategy.AddListItem(sb, items[i], true, false);
            else if (i != items.Count() - 1)
                listStrategy.AddListItem(sb, items[i], false, false);
            else
                listStrategy.AddListItem(sb, items[i], false, true);
        }

        listStrategy.End(sb);
    }

    public override string ToString() => return sb.ToString();

}

Kod działa w praktyce tak samo. Musimy tylko dla każdej strategi utworzyć odpowiednią klasę.

var tp = new TextProcessor<CommaListStrategy>();
tp.AppendList(new[] { "Moto", "Myszy", "Z", "Marsa" });
Console.WriteLine(tp);
var tp2 = new TextProcessor<HtmlListStrategy>();
tp2.AppendList(new[] { "jQuery", "JavaScript", "C#" });
Console.WriteLine(tp2);

Podsumowanie

Wzorzec projektowy Strategia pozwala ci zdefiniować szkielet algorytmu i podać mu brakującą implementację  związaną z danym zadaniem

Do dyspozycji masz rozwiązania dynamiczne i statyczne. W prawdziwym kodzie strategia też byłaby wstrzykiwana w kontenerze wstrzykiwania zależności.