TemplateWzór.10 Wzorzec projektowy Strategy i Template Method są podobne do siebie.  Jakie jednak są różnice . Wzorzec projektowy Strategy używa interfejsów i kompozycji do określenia zachowania algorytmu. 

Natomiast Template Method używa dziedziczenia . Klasa ustala szkielet algorytmu i jego kolejności natomiast implementacja poszczególnych kroków jest w zupełnie innym miejscu. Tym zajmą się klasy, które będą dziedziczyć po tej klasie szablonu

Istnieje mnóstwo przykładów tego wzorca. Często jest on reprezentowany przez artykuły jedzenia. Przykładowo masz tutaj szablon robienia naleśnika.

abstract class Pancake
{
    public abstract void MixIngredients();

    public abstract void OnFryingPan();

    public virtual void Cut()
    {
        Console.WriteLine("Cut the " + GetType().Name + " pancake!");
    }

    public void Make()
    {
        MixIngredients();
        OnFryingPan();
        Cut();
    }
}

Schemat jest jednak ten sam. Klasa szablonu ma kroki algorytmu w formie metod abstrakcyjnych . Istnieje tylko jedna normalna metoda, która definiuje kolejność działania tego algorytmu.

Jednak by się nie koncentrować na jedzeniu. Ja pokaże Ci lepszy przykład 

Szablon gry

Większość gier przebiega w podobny sposób :

  • Deklarujemy liczbę graczy
  • Gra się rozpoczyna,
  • Gra przebiega w swoich turach, aż odnajdziemy zwycięzcę

Oto szablon takiego algorytmu

public abstract class TemplateGame
{
    public TemplateGame(int numberOfPlayers)
    {
        this.numberOfPlayers = numberOfPlayers;
    }
    protected int currentPlayer;
    protected readonly int numberOfPlayers;

    protected abstract void Start();
    protected abstract bool HaveWinner { get; }
    protected abstract void TakeTurn();
    protected abstract int WinningPlayer { get; }

    public void Run()
    {
        Start();
        while (!HaveWinner)
            TakeTurn();
        Console.WriteLine
            ($"Player {WinningPlayer} wins.");
    }
}

Jak widzisz metoda Run, która uruchomi daną grę, używa zbioru właściwości i metod. Te metody i właściwości są natomiast abstrakcyjne, jak i mają dostępność protected, czyli nie mogą być wywołane na zewnątrz

protected abstract void Start();
protected abstract bool HaveWinner { get; }
protected abstract void TakeTurn();
protected abstract int WinningPlayer { get; }

Oczywiście warto sobie zadać pytanie, czy każda gra np. będzie potrzebowała metody start.Bo o ile interfejs zawsze można rozbić na mniejsze, to klasa abstrakcyjna nie daje nam już takiej możliwości. Oznacza to, że z tym wzorcem łatwo jest złamać zasadę ("Segregacji Interfejsów")

Pokaże Ci to na przykładach.

Pora na pierwszą implementację tej gry. 

public class RandomLotterGame : TemplateGame
{
    private int _numberOfPlayers;

    public RandomLotterGame(int numberOfPlayers)
        : base(numberOfPlayers)
    {
        _numberOfPlayers = numberOfPlayers;
    }

    private bool winner;
    protected override bool HaveWinner
    { get { return winner; } }

    private int winningPlayer;
    protected override int WinningPlayer
    { get { return winningPlayer; } }

    protected override void Start()
    {
        Console.WriteLine
            ($"Game starts");
    }

    protected override void TakeTurn()
    {
        Random r = new Random();
        winningPlayer = r.Next(1, _numberOfPlayers);
        winner = true;
    }
}

Działanie tej gry jest proste . Losuje liczbę od 1 do liczby graczy. Liczba wylosowana typuje zwycięzcę

W tym przypadku zwycięzca jest natychmiastowy, więc po co są w ogóle tury?

Może konstruktor tego szablonu tego nie przewidział.

Stwórzmy więc inną grę.

public class DiceWithAReaperGame : TemplateGame
{
    private int _numberOfPlayers;
    
    public DiceWithAReaperGame(int numberOfPlayers)
        : base(numberOfPlayers)
    {
        _numberOfPlayers = numberOfPlayers;
    
        for (int i = 1; i < numberOfPlayers + 1; i++)
        {
            players.Add(i, true);
        }
    }
    
    private bool winner;
    protected override bool HaveWinner
    { get { return winner; } }
    
    private int winningPlayer;
    protected override int WinningPlayer
    { get { return winningPlayer; } }
    
    protected override void Start()
    {
        Console.WriteLine
            ($"Game starts");
    }

W grze śmiercionośne kości każdy gracz będzie rzucał kostką sześcienna i jeśli liczba oczek równa się 2 lub mniej to on odpada z gry. Gra się kończy, aż w któreś turze zostanie tylko jeden gracz. 

W grze będzie potrzebny mi słownik graczy, który będzie określał, który z graczy już przegrał.

    Dictionary<int, bool> players = new Dictionary<int, bool>();
    private int turnCounter = 1;
    
    protected override void TakeTurn()
    {
        Random r = new Random();
        Console.ForegroundColor = ConsoleColor.Green;
        Console.WriteLine($"Turn starts {turnCounter} :");
        Console.ResetColor();
    
        for (int i = 0; i < _numberOfPlayers; i++)
        {
            if (players[i + 1] == false)
                continue;
    
            Console.WriteLine
                ($"Player starts {i + 1} :");
            Console.WriteLine
                ($"Player : {i + 1} throw a dice");
    
            var number = r.Next(1, 6);
    
            Console.WriteLine
                ($"The dice is : {number} ");
    
            if (number <= 2)
            {
                players.Remove(i + 1);
                Console.ForegroundColor = ConsoleColor.Red;
                Console.WriteLine
                ($"Player : {i + 1} is removed from the game");
                Console.ResetColor();
                players[i + 1] = false;
            }
    
            if (players.Where(k => k.Value == true)
                .Count() == 1)
            {
                Console.ForegroundColor = ConsoleColor.Cyan;
                winningPlayer = players.First(k => k.Value == true).Key;
                Console.WriteLine
                    ($"Player : {winningPlayer} is the only one");
                Console.ResetColor();
                winner = true;
                break;
            }
        }
    
        _numberOfPlayers = players.Count();
        turnCounter++;
    }

Zobaczmy jak działanie, taki gry wygląda w praktyce.

DiceWithAReaperGame d = new DiceWithAReaperGame(4);
d.Run();

Gra może przebiegać różnie, gdyż ma ona elementy losowe. Jak widzimy, w pierwszej turze odpadł, tylko 4 gracz. A w drugiej turze 3 graczy nie zdążył wykonać swojej tury i już wygrał.

dice game.png

Teraz, gdybyś chciał napisać inny algorytm eliminujący gracz w danej turze, nie ma problemu, tworzysz kolejną grę na podstawie tego szablonu.

Podsumowanie:

Template Method jest rozwiązaniem tylko statycznym przeciwieństwem do Strategy . Dziedziczenie jest w końcu statyczne i nie ma możliwość zmiany tej charakterystyki po utworzeniu obiektu

Wzorzec jest prosty. Tworzysz metody abstrakcyjne i jedną metodę, które je wykona w odpowiedniej kolejności. Musisz tylko uważać i zadać sobie pytanie, czy rzeczywiście dla każdej klas pochodnej będzie potrzebna dana metoda. Bo jeśli nie to łamiesz zasadę ISP.