Strategia

Wzorce projektowe

  |   8 min czytania

Zastosowanie

Strategia (ang. Strategy) (wzorzec behawioralny) definiuje rodzinę wymiennych algorytmów (w postaci klas), które służą do rozwiązywania tego samego problemu na kilka różnych sposobów. Klient może dynamicznie wybrać strategie dla obiektu i dowolnym momencie zmienić ją na inną. Szczegóły implementacji konkretnych strategii są ukryte dla klienta, a ewentualna modyfikacja wiążę się zmianą kodu w klasie konkretnej strategii. Ponadto zwiększa możliwość niezależnego testowania strategii i klienta. W celu usprawnienia procesu wyboru strategii wzorzec ten może być łączony ze wzorcem Metoda wytwórcza.

Ograniczenia

Ze względu na podobieństwa w strukturze, wzorzec ten może być mylony ze wzorcami Adapter i Most, których zastosowanie wynika z realizacji innych celów. Należy zatem się upewnić, że wybór wzorca Strategia jest zasadny. W sytuacji, gdy obiekt kontekstu nie wymaga przekazania strategii w konstruktorze, należy zadbać o jej domyślną implementacje. Dodatkowo wzrasta koszt komunikacji między obiektami, a w przypadku wielu implementacji algorytmu powstaje wiele klas. Klient powinien znać różnicę między strategiami w celu wyboru właściwej.

Użycie

Wzorzec ten podobnie jak Adapter wykorzystuje mechanizm delegowania operacji do innego obiektu. Ponadto używa podobnej struktury jak Most, jednakże celem wzorca Most jest zaprojektowanie odpowiedniej struktury klas projektu, natomiast Strategia skupia się zmienności zachowania obiektu. Strategia ma zastosowanie w sytuacjach, gdzie dane zadanie może zostać zrealizowane na wiele różnych sposobów, a decyzja o wyborze implementacji zapada dynamicznie.

Implementacja

Klasa kontekstu Context zawiera referencję do obiektu klasy strategii AbstractStrategy. Obiekt strategii może zostać wstrzyknięty przez konstruktor bądź metodę dostępową. Metoda klasy kontekstu wykorzystuje obiekt strategii w celu finalizacji operacji. Klasy strategii ConcreteStrategy1, ConcreteStrategy2 itd. implementują metody interfejsu AbstractStrategy.

Strategia diagram

Poniższy listing przedstawia implementację wzorca Strategia wykorzystywaną w obiektach klasy Context w oparciu o dwa warianty ConcreteStrategy1 oraz ConcreteStrategy2.

public class Context {

    private Strategy strategy;

    public Context(Strategy strategy) {
        this.strategy = strategy;
    }

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

    public void run(Object args) {
        strategy.action(args);
    }
}

public class Strategy1 implements Strategy {

    @Override
    public void action(Object args) {
        //do action specific for Strategy1
    }
}

public class Strategy2 implements Strategy {

    @Override
    public void action(Object args) {
        //do action specific for Strategy2
    }
}

interface Strategy {

    void action(Object args);
}

Klient przed wywołaniem metody docelowej dokonuje wyboru wstrzykniętej strategii w zależności od stanu i spełnionych warunków.

Context context = new Context(new Strategy1());
String arg = "args passed to strategy";
context.run(arg);

//conditions have changed
context.setStrategy(new Strategy2());
arg = "new args for strategy";
context.run(arg);

Przykład

Aplikacja sklepu internetowego znajdującego się na terenie Europy obsługuje dostawę swoich produktów do różnych rejonów świata. W zależności od rejonu lub kraju wysyłki naliczane jest odpowiednia opłata celna oraz wzrasta koszt wysyłki EuropeShopping, AmericaShopping. Niektóre produkty ze względu na gabaryt mogą nie być dostępne do wysyłki w dane miejsce. Aplikacja automatycznie wykrywa kraj klienta i dobiera odpowiednią strategie (Shopping) naliczania ceny całkowitej w walucie klienta (podstawową walutą jest euro). Poniższy listing prezentuje sposób realizacji doboru strategii naliczania ceny końcowej.

public class Cart {

    private Shopping shopping;
    private List<Product> products;

    public Context(Shopping shopping) {
        this.shopping = shopping;
        products = new ArrayList();
    }

    public void setShoppingRegion(Shopping shopping) {
        this.shopping = shopping;
    }

    public double getTotalPrice() {
        return shopping.calculatePrice(products);
    }

    public boolean addProduct(Product product) {
        if(shopping.checkAvailability(product)) {
          products.add(product);
          return true;
        }
        else return false;
    }

    public boolean removeProduct(Product product) {
        return products.remove(product);
    }
}

public class EuropeShopping implements Shopping {

    @Override
    public double calculatePrice(List<Product> products) {
        double totalPrice = 0;
        double deliveryCost = 5;
        for(Product product : products) {
            totalPrice += product.getPrice();
            if(product.getSize() == Size.BIG)
                deliveryCost += 2;
        }
        return totalPrice;
    }

    @Override
    public boolean checkAvailability(Product product) {
        return true;
    }

    @Override
    public Currency getCurrency {
        return Currency.EUR;
    }
}

public class AmericaShopping implements Shopping {

    private final double USD_RATE = 0.75;
    private final double CUSTOMS_DUTY = 1.1;
    private final double CUSTOMS_DUTY_NORMAL = 1.5;

    @Override
    public double calculatePrice(List<Product> products) {
        double totalPrice = 0;
        double deliveryCost = 10;
        for(Product product : products) {
            if(product.getSize == Size.NORMAL) {
                totalPrice += product.getPrice() * CUSTOMS_DUTY_NORMAL;
                deliveryCost += 5;
            }
            else {
                totalPrice += product.getPrice() * CUSTOMS_DUTY;
                deliveryCost += 2;
            }
        }
        return (totalPrice + deliveryCost) * USD_RATE;
    }

    @Override
    public boolean checkAvailability(Product product) {
        if(product.getSize == Size.BIG)
            return false;
        else
            return true;
    }

    @Override
    public Currency getCurrency {
        return Currency.USD;
    }
}

interface Shopping {

    double calculatePrice(List<Product> products);
    boolean checkAvailability(Product product);
    Currency getCurrency();
}

Użytkownik dodaje produkty do swojego koszyka, a w przypadku braku możliwości wysłania produktu do danego regionu otrzymuje stosowany komunikat. Przed przystąpieniem do potwierdzenia zamówienia następuje proces wyliczania całkowitej ceny realizacji zamówienia.

//check world region
Shopping shopping;
if(Region.EUROPE)
    shopping = new EuropeShopping();
else if(Region.AMERICA)
    shopping = new AmericaShopping();
else {
    //stop shopping or choose another strategy if is possible
}

//if delivery not available for products show message  
Cart cart = new Cart(shopping);
cart.addProduct(new Product(1, "Ball", 5, Size.NORMAL));  
cart.addProduct(new Product(2, "Notebook", 100, Size.NORMAL));
cart.addProduct(new Product(3, "Smartphone", 80, Size.SMALL));
cart.addProduct(new Product(4, "Desk", 50, Size.LARGE)); //in America this product is not available to deliver

//check total price at the end
double totalPrice = cart.getTotalPrice();
Currency currency = cart.getCurrency();
//Europe: 192EUR, America: 193,125USD (without Desk)

Biblioteki

Przykładem realizującym implementacje wzorca Strategia może być metoda sort klasy Collections (należącej do standardowego pakietu Java) do której wstrzykiwany jest Comparator oraz doFilter klasy Filter. Wybór odpowiednich zasobów aplikacji (język, widoki, wielkości itp) w Androidzie w swojej idei przypomina wzorzec Strategia.