Wzór strategii - Strategy pattern

W programowaniu komputerowym The pattern strategia (znany również jako wzór polityki ) jest behawioralny wzorzec projektowy , który umożliwia wybranie algorytmu przy starcie. Zamiast bezpośrednio implementować pojedynczy algorytm, kod otrzymuje instrukcje w czasie wykonywania, które w rodzinie algorytmów mają być użyte.

Strategia pozwala algorytmowi różnić się niezależnie od klientów, którzy go używają. Strategia jest jednym z wzorców zawartych we wpływowej książce Design Patterns autorstwa Gamma et al. który spopularyzował koncepcję wykorzystania wzorców projektowych do opisania sposobu projektowania elastycznego i wielokrotnego użytku oprogramowania obiektowego. Odroczenie decyzji o tym, którego algorytmu użyć do czasu uruchomienia, pozwala na większą elastyczność i ponowne użycie kodu wywołującego.

Na przykład klasa, która przeprowadza walidację danych przychodzących, może użyć wzorca strategii do wyboru algorytmu walidacji w zależności od typu danych, źródła danych, wyboru użytkownika lub innych czynników dyskryminujących. Czynniki te nie są znane do czasu uruchomienia i mogą wymagać przeprowadzenia radykalnie innej walidacji. Algorytmy (strategie) walidacji, hermetyzowane oddzielnie od obiektu walidującego, mogą być wykorzystywane przez inne obiekty walidujące w różnych obszarach systemu (lub nawet w różnych systemach) bez powielania kodu .

Zazwyczaj wzorzec strategii przechowuje odwołanie do kodu w strukturze danych i pobiera go. Można to osiągnąć poprzez takie mechanizmy, jak natywnego wskaźnika funkcji , z funkcją pierwszej klasy , klas lub instancji klasy w programowaniu obiektowym językach lub dostępu do pamięci wewnętrznej wdrożenia językowej kodu poprzez odbicie .

Struktura

Diagram klas i sekwencji UML

Przykładowy diagram klas i sekwencji UML dla wzorca projektowego Strategia.

W powyższym UML schemacie klasy The Contextklasa nie implementuje algorytm bezpośrednio. Zamiast tego Contextodnosi się do Strategyinterfejsu do wykonywania algorytmu ( strategy.algorithm()), który Contextuniezależnia sposób implementacji algorytmu. Strategy1I Strategy2klasy wdrożenia Strategyinterfejsu, czyli przyrząd (kapsułkowania) algorytm. UML schemat sekwencji przedstawia interakcje wykonywania czasu: obiektów delegatów algorytm do różnych obiektów. Po pierwsze, rozmowy na obiekcie, który wykonuje algorytm i zwraca wynik do . Następnie zmienia swoją strategię i połączeń na obiekcie, który wykonuje algorytm i zwraca wynik .
ContextStrategyContextalgorithm()Strategy1ContextContextalgorithm()Strategy2Context

Diagram klas

Wzorzec strategii w UML

Wzorzec strategii w LePUS3 ( legenda )

Przykład

C#

Poniższy przykład jest w C# .

public class StrategyPatternWiki
{
    public static void Main(String[] args)
    {
        // Prepare strategies
        var normalStrategy    = new NormalStrategy();
        var happyHourStrategy = new HappyHourStrategy();

        var firstCustomer = new CustomerBill(normalStrategy);

        // Normal billing
        firstCustomer.Add(1.0, 1);

        // Start Happy Hour
        firstCustomer.Strategy = happyHourStrategy;
        firstCustomer.Add(1.0, 2);

        // New Customer
        var secondCustomer = new CustomerBill(happyHourStrategy);
        secondCustomer.Add(0.8, 1);
        // The Customer pays
        firstCustomer.Print();

        // End Happy Hour
        secondCustomer.Strategy = normalStrategy;
        secondCustomer.Add(1.3, 2);
        secondCustomer.Add(2.5, 1);
        secondCustomer.Print();
    }
}

// CustomerBill as class name since it narrowly pertains to a customer's bill
class CustomerBill
{
    private IList<double> drinks;

    // Get/Set Strategy
    public IBillingStrategy Strategy { get; set; }

    public CustomerBill(IBillingStrategy strategy)
    {
        this.drinks = new List<double>();
        this.Strategy = strategy;
    }

    public void Add(double price, int quantity)
    {
        this.drinks.Add(this.Strategy.GetActPrice(price * quantity));
    }

    // Payment of bill
    public void Print()
    {
        double sum = 0;
        foreach (var drinkCost in this.drinks)
        {
            sum += drinkCost;
        }
        Console.WriteLine($"Total due: {sum}.");
        this.drinks.Clear();
    }
}

interface IBillingStrategy
{
    double GetActPrice(double rawPrice);
}

// Normal billing strategy (unchanged price)
class NormalStrategy : IBillingStrategy
{
    public double GetActPrice(double rawPrice) => rawPrice;
}

// Strategy for Happy hour (50% discount)
class HappyHourStrategy : IBillingStrategy
{
    public double GetActPrice(double rawPrice) => rawPrice * 0.5;
}

Jawa

Poniższy przykład jest w Javie .

import java.util.ArrayList;
import java.util.List;

interface BillingStrategy {
    // Use a price in cents to avoid floating point round-off error
    int getActPrice(int rawPrice);
  
    // Normal billing strategy (unchanged price)
    static BillingStrategy normalStrategy() {
        return rawPrice -> rawPrice;
    }
  
    // Strategy for Happy hour (50% discount)
    static BillingStrategy happyHourStrategy() {
        return rawPrice -> rawPrice / 2;
    }
}

class CustomerBill {
    private final List<Integer> drinks = new ArrayList<>();
    private BillingStrategy strategy;

    public CustomerBill(BillingStrategy strategy) {
        this.strategy = strategy;
    }

    public void add(int price, int quantity) {
        this.drinks.add(this.strategy.getActPrice(price*quantity));
    }

    // Payment of bill
    public void print() {
        int sum = this.drinks.stream().mapToInt(v -> v).sum();
        System.out.println("Total due: " + sum);
        this.drinks.clear();
    }

    // Set Strategy
    public void setStrategy(BillingStrategy strategy) {
        this.strategy = strategy;
    }
}

public class StrategyPattern {
    public static void main(String[] arguments) {
        // Prepare strategies
        BillingStrategy normalStrategy    = BillingStrategy.normalStrategy();
        BillingStrategy happyHourStrategy = BillingStrategy.happyHourStrategy();

        CustomerBill firstCustomer = new CustomerBill(normalStrategy);

        // Normal billing
        firstCustomer.add(100, 1);

        // Start Happy Hour
        firstCustomer.setStrategy(happyHourStrategy);
        firstCustomer.add(100, 2);

        // New Customer
        CustomerBill secondCustomer = new CustomerBill(happyHourStrategy);
        secondCustomer.add(80, 1);
        // The Customer pays
        firstCustomer.print();

        // End Happy Hour
        secondCustomer.setStrategy(normalStrategy);
        secondCustomer.add(130, 2);
        secondCustomer.add(250, 1);
        secondCustomer.print();
    }
}

Strategia i zasada otwarte/zamknięte

Przyspieszenie i hamowanie muszą być zadeklarowane w każdym nowym modelu samochodu .

Zgodnie ze wzorcem strategii zachowania klasy nie powinny być dziedziczone. Zamiast tego powinny być hermetyzowane za pomocą interfejsów. Jest to zgodne z zasadą open/closed (OCP), która proponuje, aby klasy były otwarte na rozszerzenie, ale zamknięte na modyfikację.

Jako przykład rozważ klasę samochodu. Dwie możliwe funkcje samochodu to hamowanie i przyspieszanie . Ponieważ zachowania przyspieszania i hamowania często zmieniają się między modelami, powszechnym podejściem jest implementacja tych zachowań w podklasach. Takie podejście ma istotne wady: w każdym nowym modelu samochodu należy zadeklarować zachowanie przyspieszenia i hamowania. Praca związana z zarządzaniem tymi zachowaniami znacznie wzrasta wraz ze wzrostem liczby modeli i wymaga duplikowania kodu w różnych modelach. Ponadto nie jest łatwo określić dokładną naturę zachowania dla każdego modelu bez zbadania kodu w każdym z nich.

Wzorzec strategii wykorzystuje kompozycję zamiast dziedziczenia . We wzorcu strategii zachowania są zdefiniowane jako oddzielne interfejsy i określone klasy, które implementują te interfejsy. Pozwala to na lepsze rozdzielenie między zachowaniem a klasą, która używa tego zachowania. Zachowanie można zmienić bez przerywania klas, które go używają, a klasy mogą przełączać się między zachowaniami, zmieniając konkretną używaną implementację bez konieczności wprowadzania znaczących zmian w kodzie. Zachowania można również zmieniać w czasie wykonywania, a także w czasie projektowania. Na przykład zachowanie hamulca obiektu samochodu można zmienić z BrakeWithABS()na Brake()przez zmianę brakeBehaviorelementu na:

brakeBehavior = new Brake();
/* Encapsulated family of Algorithms
 * Interface and its implementations
 */
public interface IBrakeBehavior {
    public void brake();
}

public class BrakeWithABS implements IBrakeBehavior {
    public void brake() {
        System.out.println("Brake with ABS applied");
    }
}

public class Brake implements IBrakeBehavior {
    public void brake() {
        System.out.println("Simple Brake applied");
    }
}

/* Client that can use the algorithms above interchangeably */
public abstract class Car {
    private IBrakeBehavior brakeBehavior;

    public Car(IBrakeBehavior brakeBehavior) {
      this.brakeBehavior = brakeBehavior;
    }

    public void applyBrake() {
        brakeBehavior.brake();
    }

    public void setBrakeBehavior(IBrakeBehavior brakeType) {
        this.brakeBehavior = brakeType;
    }
}

/* Client 1 uses one algorithm (Brake) in the constructor */
public class Sedan extends Car {
    public Sedan() {
        super(new Brake());
    }
}

/* Client 2 uses another algorithm (BrakeWithABS) in the constructor */
public class SUV extends Car {
    public SUV() {
        super(new BrakeWithABS());
    }
}

/* Using the Car example */
public class CarExample {
    public static void main(final String[] arguments) {
        Car sedanCar = new Sedan();
        sedanCar.applyBrake();  // This will invoke class "Brake"

        Car suvCar = new SUV();
        suvCar.applyBrake();    // This will invoke class "BrakeWithABS"

        // set brake behavior dynamically
        suvCar.setBrakeBehavior( new Brake() );
        suvCar.applyBrake();    // This will invoke class "Brake"
    }
}

Zobacz też

Bibliografia

Linki zewnętrzne