Wzorce Projektowe: Strategia (Strategy)

Definicja

Strategia to behawioralny (czynnościowy) wzorzec projektowy, przekształcający zestaw algorytmów definiujących konkretne zachowania w obiekty, które można następnie zamiennie stosować w pierwotnym obiekcie – pozwala na pakowanie całych rodzin algorytmów w indywidualne zestawy oraz dynamiczną zmianę zachowań obiektów w trakcie działania programu.

Przykład zastosowania wzorca Strategia

Zagłębmy się w kod, aby zrozumieć co tak naprawdę znaczy powyższa definicja. W przykładzie utworzymy symulator zachowań kotów – stworzymy klasę nadrzędną Cat z typowymi zachowaniami, po której dziedziczyć będą konkretne typy kotów.

public abstract class Cat {
    public abstract void display(); // każdy kot ma inny wygląd
    public void eat() {}
    public void meow() {}
    // inne metody
} 

Metoda display() jest abstrakcyjna, ponieważ każdy z kotów różni się wyglądem. Wszystkie klasy rozszerzające klasę Cat, odziedziczą jej zachowania lub zastąpią je własnymi implementacjami.

Póki co wszystko wydaje się być w porządku – problem pojawi się w przypadku gdy będziemy potrzebować zdefiniowania np. kota pluszowego, który nie wykonuje wszystkich czynności zdefiniowanych w super klasie Cat. 
public class PlushCat extends Cat {
    @override
    public void eat() {} // Nie rób nic
} 

Możemy nadpisywać poszczególne metody tak, aby nie robiły nic, jednak nie wygląda to czysto i nie jest pożądane.

Czy interfejs może rozwiązać problem?
A co z interfejsami? Zobaczmy, czy mogą rozwiązać nasz problem – tworzymy więc CanEat oraz CanMeow interfejs:

interface CanEat {
    public void eat();
}

interface CanMeow {
    public void meow();
} 

Usunęliśmy teraz metody meow() oraz eat() z klasy nadrzędnej Cat i zdefiniowaliśmy je w odpowiednich interfejsach. Koty będą mogły miauczeć i jeść tylko wtedy gdy zaimplementują odpowiedni interfejs.
Problem wydaje się być rozwiązany, jednak co w przypadku gdy każda odmiana kota będzie potrzebować innego rodzaju karmy? Konieczna będzie modyfikacja metody eat() we wszystkich podklasach klasy Cat!

Widzimy więc, że interfejsy tylko częściowo rozwiązują problem – nie mają jednak możliwości ponownego wykorzystania kodu. Z pomocą przychodzi tutaj wzorzec Strategia.

Wzorzec Strategia

Przedstawię teraz jedną z reguł projektowania:

Zidentyfikuj fragmenty aplikacji, które się zmieniają i oddziel je od tych, które pozostają stałe.

Powyższa reguła mówi, aby dokonać hermetyzacji zmieniających się fragmentów kodu, tak aby ich przyszła modyfikacja i aktualizacja nie pociągała za sobą konieczności wprowadzania zmian w elementach niezmiennych aplikacji.

W naszym przypadku zachowania eat() i meow() różnią się dla poszczególnych podtypów kota, dlatego możemy je wyodrębnić do osobnych klas (interfejsów).

interface EatBehavior {
    public void eat();
}

interface MeowBehavior {
    public void meow();
} 

Od teraz wszystkie klasy, które reprezentują te zachowania będą implementować odpowiedni interfejs.

public class Meow implements MeowBehavior {
    @override
    public void meow() {
        System.out.println("Meow! Meow!");
    }
}

public class MuteMeow implements MeowBehavior {
    @override
    public void meow() {
        System.out.println("Do not meow");
    }
}

public class AdultDiet implements EatBehavior {
    @override
    public void eat() {
        System.out.println("This is a diet for adult cats");
    }
}

public class IndoorDiet implements EatBehavior {
    @override
    public void eat() {
        System.out.println("This is a diet for indoor cats");
    }
} 

Kolejnym krokiem będzie zastosowanie się do następnej reguły projektowania:

Skoncentruj się na tworzeniu interfejsów, a nie implementacji.

Zamiast używać konkretnych klas używamy zmiennych, które są klasami nadrzędnymi. Dzięki takiemu rozwiązaniu różne typy obiektów mogą wykorzystywać odpowiednie zachowania (zmienne EatBehavior i MeowBehavior), a klasa Cat nie musi mieć żadnych informacji na temat rzeczywistych typów obiektów tych zmiennych.

public abstract class Cat {
    private EatBehavior eatBehavior;
    private MeowBehaviour meowBehavior;

    public Cat() {}

    public void doMeow() {
        meowBehavior.meow();
    }
 
    public void doEat() {
        eatBehavior.eat();
    }
} 

Zwróć uwagę na metody tej klasy – klasa zamiast realizować poszczególne zachowania samodzielnie lub dziedziczyć, deleguje je do odpowiednich podtypów klasy (nie obchodzi nas rzeczywisty typ obiektu, chcemy tylko aby odpowiednie zachowanie zostało wykonane).

Stwórzmy teraz konkretnego kota.

public class Bengal extends Cat {

    public Bengal() {
        meowBehavior = new Meow();
        eatBehavior = new AdultDiet();
    }
 
    public void display() {
        System.out.println("I'm an adult Bengal cat");
    } 
    ...
} 

Wzorzec Strategia w akcji

Zobaczmy symulator kotów w akcji:

public class CatSimulator {
    public static void main(String[] args) {
        Cat cat = new Bengal();
  
        cat.doEat();
        cat.doMeow();
    }
}

Wynik:
"This is a diet for adult cats"
"Meow! Meow!" 

Program możemy ulepszyć dając obiektom możliwość zmiany zachowań w trakcie działania programu – dodajmy setery do klasy Cat.

public void setEatBehavior(EatBehavior eatBehavior) {
    eatBehavior = eatBehavior;
}

public void setMeowBehavior(MeowBehavior meowBehavior) {
    meowBehavior = meowBehavior;
} 

Teraz w trakcie wykonywania programu, możemy dowolnie zmieniać zachowania kotów:

public class CatSimulator {
    public static void main(String[] args) {
        Cat cat = new Bengal();
  
        cat.doEat();
        cat.setEatBehavior(new IndoorDiet());
        cat.doEat();
        cat.doMeow();
    }
}
Wynik:
"This is a diet for adult cats"
"This is a diet for indoor cats"
"Meow! Meow!" 

Diagram klas UML wzorca Strategia

Podsumowanie

Wzorzec projektowy Strategia jest łatwy w implementacji i prosty w swoich założeniach, a jednocześnie daje bardzo duże korzyści. Praktycznie w każdej aplikacji znajdziemy fragmenty, gdzie wzorzec Strategia sprawdzi się idealnie.

Leave a Comment

Your email address will not be published. Required fields are marked *