CodeGym /Blog Java /Random-PL /Wzorzec projektowy strategii
Autor
John Selawsky
Senior Java Developer and Tutor at LearningTree

Wzorzec projektowy strategii

Opublikowano w grupie Random-PL
Cześć! W dzisiejszej lekcji porozmawiamy o wzorcu strategii. Na poprzednich lekcjach zapoznaliśmy się już pokrótce z koncepcją dziedziczenia. Jeśli zapomniałeś, przypomnę, że termin ten odnosi się do standardowego rozwiązania typowego zadania programistycznego. W CodeGym często mówimy, że możesz znaleźć w Google odpowiedź na prawie każde pytanie. Dzieje się tak dlatego, że twoje zadanie, jakiekolwiek by ono nie było, prawdopodobnie zostało już pomyślnie rozwiązane przez kogoś innego. Wzorce to sprawdzone rozwiązania najczęstszych zadań lub metody rozwiązywania problematycznych sytuacji. To są jak "koła", których nie trzeba wymyślać samemu, ale trzeba wiedzieć jak i kiedy ich używać :) Innym celem wzorców jest promowanie jednolitej architektury. Czytanie cudzego kodu nie jest łatwym zadaniem! Każdy pisze inny kod, ponieważ to samo zadanie można rozwiązać na wiele sposobów. Ale użycie wzorców pomaga różnym programistom zrozumieć logikę programowania bez zagłębiania się w każdą linię kodu (nawet jeśli widzi się to po raz pierwszy!). Dzisiaj przyjrzymy się jednemu z najczęstszych wzorców projektowych o nazwie „Strategia”. Wzorzec projektowy: Strategia - 2Wyobraź sobie, że piszemy program, który będzie aktywnie działał z obiektami Conveyance. Nie ma znaczenia, co dokładnie robi nasz program. Stworzyliśmy hierarchię klas z jedną klasą nadrzędną Conveyance i trzema klasami podrzędnymi: Sedan , Truck i F1Car .

public class Conveyance {

   public void go() {
       System.out.println("Moving forward");
   }

   public void stop() {

       System.out.println("Braking!");
   }
}

public class Sedan extends Conveyance {
}

public class Truck extends Conveyance {
}

public class F1Car extends Conveyance {
}
Wszystkie trzy klasy potomne dziedziczą po rodzicu dwie standardowe metody: go() i stop() . Nasz program jest bardzo prosty: nasze samochody mogą poruszać się tylko do przodu i hamować. Kontynuując naszą pracę, postanowiliśmy nadać samochodom nową metodę: fill() (co oznacza „napełnij zbiornik paliwa”). Dodaliśmy go do klasy nadrzędnej Conveyance :

public class Conveyance {

   public void go() {
       System.out.println("Moving forward");
   }

   public void stop() {

       System.out.println("Braking!");
   }
  
   public void fill() {
       System.out.println("Refueling!");
   }
}
Czy naprawdę mogą pojawić się problemy w tak prostej sytuacji? Właściwie już mają... Wzorzec projektowy: Strategia - 3

public class Stroller extends Conveyance {

   public void fill() {
      
       // Hmm... This is a stroller for children. It doesn't need to be refueled :/
   }
}
Nasz program ma teraz środek transportu (wózek dziecięcy), który nie pasuje dobrze do ogólnej koncepcji. Może mieć pedały lub być sterowany radiowo, ale jedno jest pewne – nie będzie miał gdzie wlać gazu. Nasza hierarchia klas spowodowała, że ​​wspólne metody są dziedziczone przez klasy, które ich nie potrzebują. Co powinniśmy zrobić w tej sytuacji? Cóż, moglibyśmy zastąpić metodę fill() w klasie Stroller , aby nic się nie działo podczas próby tankowania wózka:

public class Stroller extends Conveyance {

   @Override
   public void fill() {
       System.out.println("A stroller cannot be refueled!");
   }
}
Ale trudno to nazwać udanym rozwiązaniem, jeśli nie z innego powodu niż zduplikowany kod. Na przykład większość klas będzie używać metody klasy nadrzędnej, ale reszta będzie zmuszona ją zastąpić. Jeśli mamy 15 klas i musimy nadpisać zachowanie w 5-6 z nich, duplikacja kodu stanie się dość rozległa. Może interfejsy mogą nam pomóc? Na przykład tak:

public interface Fillable {
  
   public void fill();
}
Stworzymy interfejs Fillable z jedną metodą fill() . Wtedy te środki transportu, które trzeba zatankować, zaimplementują ten interfejs, podczas gdy inne środki transportu (na przykład nasz wózek dziecięcy) nie. Ale ta opcja nam nie odpowiada. W przyszłości nasza hierarchia klasowa może urosnąć do bardzo dużych rozmiarów (wyobraźcie sobie, ile różnych typów środków transportu jest na świecie). Zrezygnowaliśmy z poprzedniej wersji obejmującej dziedziczenie, ponieważ nie chcemy zastępować funkcji fill()metoda wiele, wiele razy. Teraz musimy to wdrożyć na każdych zajęciach! A co jeśli mamy 50? A jeśli w naszym programie będą wprowadzane częste zmiany (a jest to prawie zawsze prawdziwe w przypadku prawdziwych programów!), musielibyśmy spieszyć się przez wszystkie 50 klas i ręcznie zmieniać zachowanie każdej z nich. Co więc w końcu powinniśmy zrobić w tej sytuacji? Aby rozwiązać nasz problem, wybierzemy inną drogę. Mianowicie oddzielimy zachowanie naszej klasy od samej klasy. Co to znaczy? Jak wiesz, każdy obiekt ma stan (zbiór danych) i zachowanie (zbiór metod). Zachowanie naszej klasy przenoszenia składa się z trzech metod: go() , stop() i fill() . Pierwsze dwie metody są w porządku tak jak są. Ale przeniesiemy trzecią metodę zKlasa transportu . To oddzieli zachowanie od klasy (dokładniej oddzieli tylko część zachowania, ponieważ pierwsze dwie metody pozostaną tam, gdzie są). Więc gdzie powinniśmy umieścić naszą metodę fill() ? Nic nie przychodzi mi do głowy :/ Wygląda na to, że jest dokładnie tam, gdzie powinno być. Przeniesiemy go do osobnego interfejsu: FillStrategy !

public interface FillStrategy {

   public void fill();
}
Dlaczego potrzebujemy takiego interfejsu? To wszystko jest proste. Teraz możemy stworzyć kilka klas, które implementują ten interfejs:

public class HybridFillStrategy implements FillStrategy {
  
   @Override
   public void fill() {
       System.out.println("Refuel with gas or electricity — your choice!");
   }
}

public class F1PitstopStrategy implements FillStrategy {
  
   @Override
   public void fill() {
       System.out.println("Refuel with gas only after all other pit stop procedures are complete!");
   }
}

public class StandardFillStrategy implements FillStrategy {
   @Override
   public void fill() {
       System.out.println("Just refuel with gas!");
   }
}
Stworzyliśmy trzy strategie behawioralne: jedną dla zwykłych samochodów, jedną dla hybryd i jedną dla samochodów wyścigowych Formuły 1. Każda strategia realizuje inny algorytm tankowania. W naszym przypadku po prostu wyświetlamy napis na konsoli, ale każda metoda może zawierać jakąś złożoną logikę. Co robimy potem?

public class Conveyance {

   FillStrategy fillStrategy;

   public void fill() {
       fillStrategy.fill();
   }

   public void go() {
       System.out.println("Moving forward");
   }

   public void stop() {
       System.out.println("Braking!");
   }
  
}
Używamy naszego interfejsu FillStrategy jako pola w klasie nadrzędnej Conveyance . Zauważ, że nie wskazujemy konkretnej implementacji — używamy interfejsu. Klasy samochodów będą wymagały określonych implementacji interfejsu FillStrategy :

public class F1Car extends Conveyance {

   public F1Car() {
       this.fillStrategy = new F1PitstopStrategy();
   }
}

public class HybridCar extends Conveyance {

   public HybridCar() {
       this.fillStrategy = new HybridFillStrategy();
   }
}

public class Sedan extends Conveyance {

   public Sedan() {
       this.fillStrategy = new StandardFillStrategy();
   }
}

Zobaczmy, co mamy!

public class Main {

   public static void main(String[] args) {

       Conveyance sedan = new Sedan();
       Conveyance hybrid = new HybridCar();
       Conveyance f1car = new F1Car();

       sedan.fill();
       hybrid.fill();
       f1car.fill();
   }
}
Wyjście konsoli:

Just refuel with gas! 
Refuel with gas or electricity — your choice! 
Refuel with gas only after all other pit stop procedures are complete!
Świetnie! Proces tankowania przebiega tak, jak powinien! Swoją drogą nic nie stoi na przeszkodzie, aby użyć strategii jako parametru w konstruktorze! Na przykład tak:

public class Conveyance {

   private FillStrategy fillStrategy;

   public Conveyance(FillStrategy fillStrategy) {
       this.fillStrategy = fillStrategy;
   }

   public void fill() {
       this.fillStrategy.fill();
   }

   public void go() {
       System.out.println("Moving forward");
   }

   public void stop() {
       System.out.println("Braking!");
   }
}

public class Sedan extends Conveyance {

   public Sedan() {
       super(new StandardFillStrategy());
   }
}



public class HybridCar extends Conveyance {

   public HybridCar() {
       super(new HybridFillStrategy());
   }
}

public class F1Car extends Conveyance {

   public F1Car() {
       super(new F1PitstopStrategy());
   }
}
Uruchommy naszą metodę main() (która pozostaje niezmieniona). Otrzymujemy ten sam wynik! Wyjście konsoli:

Just refuel with gas! 
Refuel with gas or electricity — your choice! 
Refuel with gas only after all other pit stop procedures are complete!
Wzorzec projektowania strategii definiuje rodzinę algorytmów, hermetyzuje każdy z nich i zapewnia ich wymienność. Pozwala modyfikować algorytmy niezależnie od tego, jak są one używane przez klienta (ta definicja, zaczerpnięta z książki „Head First Design Patterns”, wydaje mi się znakomita). Wzorzec projektowy: Strategia - 4Interesującą nas rodzinę algorytmów (sposoby tankowania samochodów) określiliśmy już w osobnych interfejsach z różnymi implementacjami. Oddzieliliśmy je od samego samochodu. Teraz, jeśli będziemy musieli wprowadzić jakiekolwiek zmiany w konkretnym algorytmie tankowania, nie wpłynie to w żaden sposób na nasze klasy samochodów. Aby osiągnąć wymienność, wystarczy dodać jedną metodę ustawiającą do naszej klasy Conveyance :

public class Conveyance {

   FillStrategy fillStrategy;

   public void fill() {
       fillStrategy.fill();
   }

   public void go() {
       System.out.println("Moving forward");
   }

   public void stop() {
       System.out.println("Braking!");
   }

   public void setFillStrategy(FillStrategy fillStrategy) {
       this.fillStrategy = fillStrategy;
   }
}
Teraz możemy zmieniać strategie w locie:

public class Main {

   public static void main(String[] args) {

       Stroller stroller= new Stroller();
       stroller.setFillStrategy(new StandardFillStrategy());

       stroller.fill();
   }
}
Jeśli wózki dziecięce nagle zaczną jeździć na benzynie, nasz program będzie gotowy na ten scenariusz :) I to wszystko! Nauczyłeś się jeszcze jednego wzorca projektowego, który z pewnością będzie niezbędny i pomocny podczas pracy nad prawdziwymi projektami :) Do następnego razu!
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION