Strategia

Załóżmy, że w zależności od warunków, aplikacja musi wykonać inne działania. Można wydzielić taką grupę wymiennych działań (algorytmów) do osobnych klas. Te działania to strategie. Wzorzec projektowy Strategia pozwala dynamicznie (w czasie działania aplikacji) zmieniać stosowane strategie.

Dzięki Strategii można oddzielić konkretne działania od klas. Kod staje się bardziej czytelny i łatwiejszy w rozbudowie. Jednocześnie dzięki temu użytkownik nie musi znać wszystkich interfejsów używanych klas – musi tylko ustawić strategię i ją wywołać.

Rozważmy aplikację, która łączy się z kilkoma różnymi API. Aplikacja musi się uwierzytelnić w API ale każde z nich wymaga innego sposobu uwierzytelnienia.

Typ wyliczeniowy AuthType przechowuje przewidziane w aplikacji sposoby uwierzytenienia:

public enum AuthType {
    BASIC,
    OAUTH,
    JWT;
}

Klasa APIConnector odpowiada za uwierzytelnienie:

public class APIConnector {
    
    public void authBasic (AuthType authType) {
        if (authType.equals(AuthType.BASIC)) {
            System.out.println("Uwierzytelnienie przy użyciu Basic Auth");
        } else {
            System.err.println("Błędny typ uwierzytelnienia");
        }
    }

    public void authOauth (AuthType authType) {
        if (authType.equals(AuthType.OAUTH)) {
            System.out.println("Uwierzytelnienie przy użyciu OAuth2");
        } else {
            System.err.println("Błędny typ uwierzytelnienia");
        }
    }

    public void authJwt (AuthType authType) {
        if (authType.equals(AuthType.JWT)) {
            System.out.println("Uwierzytelnienie przy użyciu JWT");
        } else {
            System.err.println("Błędny typ uwierzytelnienia");
        }
    }
}

Uwierzytelnienie wymaga utworzenia obiektu klasy APIConnector i wywołania na nim odpowiedniej metody:

public class Main {
    public static void main (String[] args) {

        APIConnector connector = new APIConnector();
        connector.authBasic(AuthType.BASIC);
    }
}

Powyższe rozwiązanie ma 2 wady:

  • użytkownik (klasa Main) musi znać interfejsy klasy APIConnector żeby jej używać,
  • każda nowa metoda uwierzytelnienia wymaga modyfikacji klasy APIConnector.

Można zmodyfikować klasę APIConnector tak, żeby miała tylko 1 interfejs:

public class APIConnector {

    public void authenticate (AuthType authType) {
        switch (authType) {
            case BASIC:
                System.out.println("Uwierzytelnienie przy użyciu Basic Auth");
                break;
            case OAUTH:
                System.out.println("Uwierzytelnienie przy użyciu OAuth2");
                break;
            case JWT:
                System.out.println("Uwierzytelnienie przy użyciu JWT");
                break;
            default:
                System.err.println("Nieznany typ uwierzytelnienia");
        }
    }
} 

W ten sposób użycie klasy APIConnector jest łatwiejsze (Main musi znać 1 interfejs, authenticate()), ale dalej każda nowa metoda uwierzytelnienia wymaga modyfikacji w APIConnector. Strategia pozwala rozwiązać ten problem.

Implementacja wzorca Strategia

Interfejs AuthStrategy deklaruje 1 metodę, authenticate():

public interface AuthStrategy {
    void authenticate();
}

Klasy BasicAuthStrategy, OAuthStrategy i JwtAuthStrategy implementują interfejs. To są konkretne strategie. Każda z nich implementuje metodę authenticate() inaczej odpowiadając za inny sposób uwierzytelnienia:

public class BasicAuthStrategy implements AuthStrategy{
    @Override
    public void authenticate() {
        System.out.println("Uwierzytelnienie przy użyciu Basic Auth");
    }
}
public class OAuthStrategy implements AuthStrategy{
    @Override
    public void authenticate() {
        System.out.println("Uwierzytelnienie przy użyciu OAuth2");
    }
}
public class JwtAuthStrategy implements AuthStrategy{
    @Override
    public void authenticate() {
        System.out.println("Uwierzytelnienie przy użyciu JWT");
    }
}

Klasa ApiConnector ma prywatne pole przechowujące obiekt implementujący interfejs AuthStrategy i odpowiedni setter. W metodzie authenticate() zamiast całej logiki jest tylko wywołanie metody authenticate() na tym obiekcie:

public class APIConnector {

    private AuthStrategy authStrategy;

    public void authenticate () {
        this.authStrategy.authenticate();
    }

    public void setAuthStrategy(AuthStrategy authStrategy) {
        this.authStrategy = authStrategy;
    }
}

W klasie Main trzeba utworzyć obiekt klasy APIConnector (bez zmian), a następnie ustawić odpowiednią strategię i wywołać metodę authenticate():

public class Main {
    public static void main (String[] args) {

        APIConnector connector = new APIConnector();

        connector.setAuthStrategy(new BasicAuthStrategy());
        connector.authenticate();
    }
}

Dzięki temu, że pole authStrategy jest typu AuthStrategy można do niego zapisać obiekt każdej z klas implementujących ten interfejs: BasicAuthStrategy, OAuthStrategy i JwtAuthStrategy. AuthStrategy służy jako dodatkowa warstwa abstrakcji pośrednicząca między klasą APIConnector a konkretnymi strategiami. W momencie wywołania metody authenticate() aplikacja automatycznie wywoła jej właściwą implementację.

Enum AuthType nie jest już potrzebny.

Dodanie nowej metody uwierzytelniania nie wymaga już modyfikacji klasy APIConnector. Wystarczy dodać nową strategię.

Podsumowanie

Wzorzec Strategia to praktyczna implementacja zasady otwarte-zamknięte oraz zasady odwrócenia zależności.

Strategia to popularny i prosty wzorzec. Jest stosowana tam, gdzie potrzebna jest możliwość elastycznej zmiany zachowania aplikacji. Wzorzec ułatwia testowanie, rozszerzanie i utrzymanie kodu.


Wzorzec Strategia i wzorzec Stan umożliwiają dynamiczną modyfikację działania aplikacji, ale mają inny cel:

  • Strategia służy enkapsulacji poszczególnych algorytmów które są potem wstrzykiwane z zewnątrz, a klient wybiera którego algorytmu użyć,
  • Stan z góry implementuje jakie zachowanie ma wystąpić przy jakim stanie obiektu, a zmiana zachowania następuje w sposób automatyczny.