Wzorce projektowe #1 – Strategia

chess
W pierwszym wpisie na moim blogu chcę rozpocząć krótką serię dotyczącą wybranych najważniejszych wzorców projektowych. Jako pierwszy na warsztat został wzięty wzorzec Strategia. Jest on niezbyt skomplikowany, a jego zastosowanie może znacznie poprawić czytelność pisanego kodu, co postaram się w niniejszym wpisie udowodnić.

Trochę teorii

Strategia to wzorzec definiujący rodzinę algorytmów i hermetyzujący je w postaci osobnych klas. Dzięki temu zmiany w implementacji poszczególnych algorytmów wykonywane są niezależnie od kodu klienta, który je wykorzystuje.
Wzorzec Strategia należy do grupy wzorców operacyjnych(czynnościowych), które to odpowiadają za interakcję współpracujących ze sobą obiektów.
Wzorzec strategia sprawdza się w tych częściach kodu, gdzie pojawia się nam duża ilość instrukcji warunkowych.

Problem projektowy – fabryka maszyn

Wyobraźmy sobie, że pracujemy w fabryce maszyn budowlanych w dziale testów. Nasz zespół jest odpowiedzialny za testowanie poprawności działania podwozia(chassis) oraz wyposażenia(equipment) poszczególnych maszyn.
Fabryka produkuje koparki i spycharki. Koparka jest wyposażona w podwozie kołowe oraz narzędzie robocze w postaci łyżki. Spychacz natomiast posiada podwozie gąsienicowe oraz pług jako narzędzie robocze.
Najprostszy kod umożliwiający testowanie maszyn mógłby wyglądać tak:
if (chassis == "wheeled chassis")
    System.out.println("Uruchamian mechanizm podwozia kołowego");
else if (chassis == "continuous track")
    System.out.println("Uruchamian mechanizm podwozia gąsienicowego");

if (equipment == "scoop")
    System.out.println("Uruchamiam mechanizm ruchu łyżki");
else if (equipment == "plow")
    System.out.println("Uruchamiam mechanizm ruchu pługa"); 
W powyższym przykładzie pierwsza instrukcja warunkowa umożliwia przetestowanie działania podwozia, natomiast druga jest odpowiedzialna za test narzędzia roboczego. Póki co, takie rozwiązanie nie wydaje się złe. Wyobraźmy sobie jednak, że nasza fabryka wprowadziła do produkcji nowe rodzaje maszyn:
  • wywrotka wyposażona w podwozie kołowe i narzędzie w postaci skrzyni ładunkowej
  • dwa nowe rodzaje koparki wyposażone w podwozie gąsienicowe oraz podwozie szynowe, do prac kolejowych
  • dźwig stacjonarny, który nie ma możliwości poruszania się(dla uproszczenia wyposażymy go w „podwozie stacjonarne”), posiadający hak jako narzędzie robocze.
Produkcja nowych maszyn spowodowała konieczność utworzenia dwóch nowych rodzajów podwozia: szynowego i stacjonarnego oraz dwóch nowych rodzajów narzędzi roboczych: skrzynia ładunkowa i hak.
Kod testujący maszyny po wprowadzeniu zmian będzie teraz wyglądać tak:
if (chassis == "wheeled chassis")
    System.out.println("Uruchamian mechanizm podwozia kołowego");
else if (chassis == "continuous track")
    System.out.println("Uruchamian mechanizm podwozia gąsienicowego");
else if (chassis == "rail chassis")
    System.out.println("Uruchamian mechanizm podwozia szynowego");
else if (chassis == "stationary chassis")
    System.out.println("Uruchamian mechanizm obrotu wokół własnej osi");

if (equipment == "scoop")
    System.out.println("Uruchamiam mechanizm ruchu łyżki");
else if (equipment == "plow")
    System.out.println("Uruchamiam mechanizm ruchu pługa");
else if (equipment == "dump")
    System.out.println("Uruchamiam mechanizm wychyłu wywrotki");
else if (equipment == "hook")
    System.out.println("Uruchamiam mechanizm podnoszenia haka"); 
Tak rozbudowane instrukcje warunkowe stają się coraz mniej czytelne. Ich złożoność powoduje, że są podatne na powstanie błędów w wyniku ich zmian. Kod klienta nie jest odseparowany od kodu naszej aplikacji. Należy mieć na uwadze także fakt, iż, jeżeli kod testujący maszyny występuje w kodzie aplikacji w wielu różnych miejscach, wszelkie zmiany należy wprowadzić w każdym z tych miejsc.
Zastosowanie w tym zadaniu wzorca Strategia sprawi, że kod stanie się bardziej czytelny, odseparowany od kodu klienta, oraz łatwiejszy w utrzymaniu.

Zastosowanie wzorca Strategia w praktyce

Strukturę wzorca Strategia najprościej będzie wyjaśnić na przykładzie diagramu UML:
UML
Interfejs Strategy stanowi jeden wspólny algorytm zachowania. Poszczególne implementacje tego zachowania zawarte są w klasach ConcreteStrategy.
Klasa Context zawiera referencję do obiektu ConcreteStrategy poprzez interfejs Strategy. Z klasy Context dziedziczą klasy ConcreteContext, które są “powiązane” z poszczególnymi strategiami.

Działanie wzorca Strategii można opisać w następujący sposób: poszczególne obiekty klasy ConcreteContext zawierają referencje do różnych obiektów klasy Concrete Strategy. Każdy z obiektów klasy ConcreteStrategy odpowiada za inną implementację danego zachowania. Tak więc w zależności od tego z którą konkretną strategią “komunikuje się” konkretny kontekst, takie ten obiekt będzie miał zachowanie.

Diagram UML fabryki maszyn

Diagram klas naszej fabryki maszyn będzie wyglądał następująco:
UML
Fabryka maszyn ma za zadanie testować działanie podwozia oraz działanie narzędzia roboczego. Mamy tutaj dwa algorytmy zachowań: ruch podwozia i działanie narzędzia roboczego, które są reprezentowane przez Interfejsy Chassis (podwozie) i Equipment (narzędzie robocze). Konkretne implementacje tych zachowań są zawarte w klasach implementujących te interfejsy (np. WheeledChassis – podwozie kołowe, ContinousTrak – podwozie gąsienicowe, Scoop – łyżka koparki, Plow – pług buldożera itd).
Maszyny do przetestowania to obiekty poszczególnych klas dziedziczących po klasie Machine (np. WheelExcavator – koparka kołowa, DumpTrack – wywrotka itd).
Przyjrzyjmy się teraz bliżej wszystkim klasom i interfejsom programu.

Implementacja wzorca Strategia

public interface Chassis {
    public void move();
}

public interface Equipment {
    public void start();
} 
Interfejsy Chassis i Equipment zawierają po jednej metodzie:
  • move – umożliwia ruch podwozia
  • start – umożliwia uruchomienie narzędzia roboczego.
public class WheeledChassis implements Chassis {
    @Override
    public void move() {
        System.out.println("Uruchamian mechanizm podwozia kołowego");
    }
}

public class ContinousTrack implements Chassis {
    @Override
    public void move() {
        System.out.println("Uruchamian mechanizm podwozia gąsienicowego");
    }
}

public class RailChassis implements Chassis {
    @Override
    public void move() {
        System.out.println("Uruchamian mechanizm podwozia szynowego");
    }
}

public class StationaryChassis implements Chassis {
    @Override
    public void move() {
        System.out.println("Uruchamian mechanizm obrotu wokół własnej osi");
    }
} 
Klasy implementujące interfejs Chassis realizują ruch konkretnego rodzaju podwozia.
public class Scoop implements Equipment {
    @Override
    public void start() {
        System.out.println("Uruchamiam mechanizm ruchu łyżki");
    }
}

public class Plow implements Equipment {
    @Override
    public void start() {
        System.out.println("Uruchamiam mechanizm ruchu pługa");
    }
}

public class Dump implements Equipment {
    @Override
    public void start() {
        System.out.println("Uruchamiam mechanizm wychyłu wywrotki");
    }
}

public class Hook implements Equipment {
    @Override
    public void start() {
        System.out.println("Uruchamiam mechanizm podnoszenia haka");
    }
} 
Analogicznie, klasy implementujące interfejs Equipment realizują ruch konkretnego rodzaju narzędzia.
public abstract class Machine {
    protected Chassis chassis;
    protected Equipment equipment;

    public void testMove() {
        chassis.move();
    }

    public void testStart() {
        equipment.start();
    };

    public abstract void getType();
}
 
Klasa Machine zawiera referencje do interfejsów poszczególnych algorytmów. Jest to klasa abstrakcyjna, gdyż chcemy jedną z metod definiować w poszczególnych klasach dziedziczących po Machine.
Metody testMove i testStart powodują uruchomienie metod move i start poszczególnych interfejsów.
Abstrakcyjna metoda getType będzie definiowana w klasach podrzędnych. Metoda ta wypisuje typ maszyny.
public class WheeledExcavator extends Machine {
    public WheeledExcavator() {
        chassis = new WheeledChassis();
        equipment = new Scoop();
    }

    @Override
    public void getType() {
        System.out.println("Koparka kołowa");
    }
}

public class ContinousTrackExcavator extends Machine {
    public ContinousTrackExcavator() {
        chassis = new ContinousTrack();
        equipment = new Scoop();
    }

    @Override
    public void getType() {
        System.out.println("Koparka gąsienicowa");
    }
}

public class RailExcavator extends Machine {
    public RailExcavator() {
        chassis = new RailChassis();
        equipment = new Scoop();
    }

    @Override
    public void getType() {
        System.out.println("Koparka szynowa");
    }
}

public class Bulldozer extends Machine {
    public Bulldozer() {
        chassis = new ContinousTrack();
        equipment = new Plow();
    }

    @Override
    public void getType() {
        System.out.println("Spychacz");
    }
}

public class DumpTrack extends Machine {
    public DumpTrack() {
        chassis = new WheeledChassis();
        equipment = new Dump();
    }

    @Override
    public void getType() {
        System.out.println("Wywrotka");
    }
}

public class Crane extends Machine {
    public Crane() {
        chassis = new StationaryChassis();
        equipment = new Hook();
    }

    @Override
    public void getType() {
        System.out.println("Dźwig");
    }
} 
Podczas tworzenia obiektów, konstruktor tworzy referencję do konkretnej implementacji każdego z algorytmów. Mamy tu też definicję metody getType zadeklarowanej w klasie nadrzędnej.
public class MachineTester {
    public static void main(String[] args) {
        Machine wheeledExcavator = new WheeledExcavator();
        Machine continuousTrackExcavator = new ContinousTrackExcavator();
        Machine railExcavator = new RailExcavator();
        Machine bulldozer = new Bulldozer();
        Machine dumpTrack = new DumpTrack();
        Machine crane = new Crane();


        wheeledExcavator.getType();
        wheeledExcavator.testMove();
        wheeledExcavator.testStart();

        continuousTrackExcavator.getType();
        continuousTrackExcavator.testMove();
        continuousTrackExcavator.testStart();

        railExcavator.getType();
        railExcavator.testMove();
        railExcavator.testStart();

        bulldozer.getType();
        bulldozer.testMove();
        bulldozer.testStart();

        dumpTrack.getType();
        dumpTrack.testMove();
        dumpTrack.testStart();

        crane.getType();
        crane.testMove();
        crane.testStart();
    }
}
 
Pora na sprawdzenie działania naszego testera maszyn w akcji. Najpierw tworzymy obiekty wszystkich maszyn, a następnie na każdej z nich wywołujemy metodę „zwracającą” typ maszyny, wykonującą test podwozia oraz test narzędzia roboczego.
W tej klasie wyraźnie widać odseparowanie kodu klienta od kodu całej logiki działania wzorca. Klient nie wie w jaki sposób implementowane jest wywołanie konkretnej strategii. Wystarczy, że klient utworzy obiekt i wywoła na nim odpowiednią metodę.
Oto wynik działania programu:
result
Jak widać, program działa bez zarzutu.

Podsumowanie

Wzorzec Strategia jest mało skomplikowanym wzorcem, aczkolwiek usprawniającym pracę z algorytmami zachowań. Kod napisany z wykorzystaniem Strategii jest prosty, czytelny, łatwy w rozbudowie i odseparowany od kodu klienta.

Znajdź mnie również na:

arrow