» Wzorce projektowe
Wstęp
Wzorce projektowe to temat bardzo ciekawy, ale kiedy sam do niego usiadłem, to muszę przyznać, że wiele z tych pozycji było dla mnie nieoczywistych. Mogą bardzo pomóc w pisaniu aplikacji, a dokładniej mogą w nie tchnąć obiektowego ducha, przez co aplikacja staje się bardziej czytelna dla programisty i potem nie musi on dostawać szczękościsku na samą myśl o dodaniu nowej funkcjonalności (to ja!), czy zmianie już istniejących. Stety niestety na świecie zwykle bywa tak, że aby osiągnąć jedną rzecz, trzeba zapłacić czymś innym. Dzięki wzorcom projektowym aplikacja staje się bardziej przejrzysta… Tylko dla kogo? Dla tego, kto umie ją dobrze odczytać – dla programisty obiektowego, który musi umieć operować abstrakcją, w moim mniemaniu, na całkiem wysokim poziomie. Na starcie polecę książkę „Head First Design Patterns” wydawnictwa Helion, która bardzo nieszablonowo tłumaczy wzorce, a także na każdym kroku, że tak powiem, zachęca do myślenia obiektowego. Dodatkowo bardzo ciekawym rozwinięciem jest strona refactoring.guru, na której znalazłem ciekawe przykłady zastosowania wzorców, które pomogły mi (te przykłady) zrozumieć zależności i schematy postępowania podczas wdrażania wzorców projektowych we własnych aplikacjach. Ja chciałbym dorzucić do świata programistów swoje spojrzenie na kilka z nich. Może kiedyś komuś się to przyda :). Możliwe, że niektórych stwierdzeń nie jeden programista by nie wypowiedział, lecz proszę mi wybaczyć te momenty. Piszę to przepełniony chęcią pomocy (i na zaliczenie), ale niestety uważam, że poziom mojego doświadczenia jest jeszcze niski. Jeśli Wam to nie przeszkadza zapraszam do lektury!
Co omawiam?
Chciałbym wyjść od metody fabrykującej, która często jest „lekkim” początkiem dla niektórych z pozostałych wzorców. Dalej projekty nierzadko ewoluują w Fabrykę Abstrakcyjną, Budowniczego czy Prototyp.
Powyższe wzorce przedstawiam w Javie.
Problem
Wszystkie cztery wzorce chciałbym przedstawić na jednym problemie, żeby jak najlepiej je porównać i zobaczyć jak jeden program może wyglądać w zależności od użytego wzorca.
Wyobraźcie sobie, że tworzymy gierkę typu „point & click” jak np. Torin’s Passage. Użytkownik ma do dyspozycji dom i okolicę – miejsce akcji. Może on wchodzić w interakcje z poszczególnymi elementami domu. Ale do tego chcemy dodać możliwość tworzenia własnych plansz, historii, itp. Powiedzmy, że gra już jest, tylko teraz trzeba zaprogramować funkcjonalność tworzenia własnego miejsca akcji.
Przygotowanie materiałów
Skoro mówimy o domie i elementach go tworzących, co innego pozwoli lepiej zwizualizować sobie nasze obiekty jak nie właśnie obiekty (w ujęciu obiektowym – programowania obiektowego)? Stwórzmy kilka elementów naszego miejsca akcji… (a w zasadzie kilka formularzy tworzenia obiektów – czyli klasy)
Na początku interfejsy:
public interface MainBuilding { void openFrontDoor(); void closeFrontDoor(); void allLightsOn(); void allLightsOff(); } public interface Bed { void move(); void sleep(); void sitOn(); } public interface Chair { void move(); void sitOn(); } public interface Garden { void turnOnSprinkler(); void turnOffSprinkler(); void turnOnLight(); void turnOffLight(); }
Oczywiście to tylko część elementów. Na potrzeby przykładu nie chciałem rozpisywać się i tworzyć nie wiadomo ile klas, żeby wszystko było w miarę przejrzyste i czytelne.
To teraz implementujemy klasy rzeczywiste. Żeby użytkownik miał jakiś wybór stwórzmy po trzy rodzaje każdego z tych elementów. Na początek styl kamienny:
public class MainBuildingStoneStyle implements MainBuilding { private String mainMaterial = "Stone"; private String lamps = "Torches"; private int windowsNumber = 5; private int walls = 4; private int doors = 3; @Override public void openFrontDoor() { System.out.println("Opening STONE door..."); } @Override public void closeFrontDoor() { System.out.println("Closing STONE door..."); } @Override public void allLightsOn() { System.out.println("Igniting TORCHES..."); } @Override public void allLightsOff() { System.out.println("Extinguishing TORCHES..."); } } public class BedStoneStyle implements Bed { private String material = "Stone"; private int legs = 0; @Override public void move() { System.out.println("It's too heavy for moving anywhere..."); } @Override public void sitOn() { System.out.println("Sitting on uncomfortable stone bed. Stamina regenerated: 0 points."); } @Override public void sleep() { System.out.println("Sleeping in uncomfortable bed.\n" + "Stamina regenerated: 0 points.\n" + "Health regenerated: 0 points"); } } public class ChairStoneStyle implements Chair { private String material = "Stone"; private int legs = 0; @Override public void move() { System.out.println("It's too heavy for moving anywhere..."); } @Override public void sitOn() { System.out.println("Sitting on uncomfortable stone chair. Stamina regenerated: 0 points."); } } public class GardenStoneStyle implements Garden { private String lamps = "Torches"; private int lampsQuantity = 5; private String flowers = "Blue Orchids"; private int flowersQuantity = 100; private boolean sprinkler = false; @Override public void turnOnSprinkler() { System.out.println("There's no sprinkler..."); } @Override public void turnOffSprinkler() { System.out.println("There's no sprinkler..."); } @Override public void turnOnLight() { System.out.println("Igniting torches..."); } @Override public void turnOffLight() { System.out.println("Extinguishing torches..."); } }
Styl drewniany:
public class MainBuildingWoodStyle implements MainBuilding { private String mainMaterial = "Wood"; private String lamps = "Lamps"; private int windowsNumber = 4; private int walls = 5; private int doors = 2; @Override public void openFrontDoor() { System.<em>out</em>.println("Opening WOODEN door..."); } @Override public void closeFrontDoor() { System.<em>out</em>.println("Closing WOODEN door..."); } @Override public void allLightsOn() { System.<em>out</em>.println("Turning on LAMPS..."); } @Override public void allLightsOff() { System.<em>out</em>.println("Turning off LAMPS..."); } } public class BedWoodStyle implements Bed { private String material = "Wood"; private int legs = 4; @Override public void move() { System.out.println("Moving wooden bed..."); } @Override public void sitOn() { System.out.println("Sitting on wooden bed. Stamina regenerated: 10 points."); } @Override public void sleep() { System.out.println("Sleeping in wooden bed.\n" + "Stamina regenerated: 20 points.\n" + "Health regenerated: 15 points"); } } public class ChairWoodStyle implements Chair { private String material = "Wood"; private int legs = 4; @Override public void move() { System.out.println("Moving wooden chair..."); } @Override public void sitOn() { System.out.println("Sitting on wooden chair. Stamina regenerated: 10 points."); } } public class GardenWoodStyle implements Garden { private String lamps = "Lamps"; private int lampsQuantity = 3; private String flowers = "Roses"; private int flowersQuantity = 50; private boolean sprinkler = true; @Override public void turnOnSprinkler() { System.out.println("Turning on sprinkler..."); } @Override public void turnOffSprinkler() { System.out.println("Turning off sprinkler..."); } @Override public void turnOnLight() { System.out.println("Glowstone can't be turned on. It glows all the time."); } @Override public void turnOffLight() { System.out.println("Glowstone can't be turned off. It glows all the time."); } }
I styl…powiedzmy pałacowy:
public class MainBuildingPalaceStyle implements MainBuilding { private String mainMaterial = "Diamond"; private String lamps = "Glowstone"; private int windowsNumber = 15; private int walls = 10; private int doors = 5; @Override public void openFrontDoor() { System.out.println("Opening DIAMOND door..."); } @Override public void closeFrontDoor() { System.out.println("Closing DIAMOND door..."); } @Override public void allLightsOn() { System.out.println("GLOWSTONE lights all the time..."); } @Override public void allLightsOff() { System.out.println("GLOWSTONE lights all the time..."); } } public class BedPalaceStyle implements Bed { private String material = "Wool and diamond."; private int legs = 0; @Override public void move() { System.out.println("Moving ultra comfortable bed..."); } @Override public void sitOn() { System.out.println("Sitting on ultra comfortable bed. Stamina regenerated: 1000 points."); } @Override public void sleep() { System.out.println("Sleeping in ultra comfortable bed.\n" + "Stamina regenerated: 100% points.\n" + "Health regenerated: 100% points"); } } public class ChairPalaceStyle implements Chair { private String material = "Wool and diamond"; private int legs = 0; @Override public void move() { System.out.println("Moving wool diamond chair..."); } @Override public void sitOn() { System.out.println("Sitting on wool diamond chair. Stamina regenerated: 500 points."); } } public class GardenPalaceStyle implements Garden { private String lamps = "Glowstones"; private int lampsQuantity = 10; private String flowers = "Sunflowers"; private int flowersQuantity = 75; private boolean sprinkler = true; @Override public void turnOnSprinkler() { System.out.println("Turning on sprinkler..."); } @Override public void turnOffSprinkler() { System.out.println("Turning off sprinkler..."); } @Override public void turnOnLight() { System.out.println("Turning on lamp..."); } @Override public void turnOffLight() { System.out.println("Turning on lamp..."); } }
Użyte tutaj materiały niekoniecznie mają coś wspólnego z rzeczywistością. Diamentowe łóżko raczej wcale nie jest najwygodniejsze. I kto by posiadał w domu kamienne krzesło? Ale cóż… Przejdźmy dalej zrzucając na to zasłonę milczenia… Glowstone, którego użyłem jako oświetlenie jest wzorowany na Minecraftowym glowstownie. Tam jest to blok, który świeci nieustannie, dopóki się go nie rozbije. Zwróćcie uwagę, że każdy styl ma inną implementację poszczególnych metod. W jednym światłem jest pochodnia, gdzie indziej glowstone. W jednym ogródku występuje spryskiwacz, w drugim go nie ma, więc nie można go włączyć czy wyłączyć.
Metoda fabrykująca
Jak już napisałem, jest to w moim mniemaniu dosyć „lekki” wzorzec, który nie jest bardzo skomplikowany, a raczej traktuję go jako małe usprawnienie, które daje duże efekty. Najpierw polecam przejrzeć ten przykład, który bardzo ciekawie pokazuje zastosowanie metody fabrykującej. U nas dużo lepsze zastosowanie znajdzie ona po przebudowaniu do fabryki abstrakcyjnej.
Załóżmy, że na razie chcemy zająć się samym głównym budynkiem (klasa MainBuilding). Wszystkie poszczególne elementy jak MainBuilding czy Garden będziemy implementować w klasie odpowiadającej całemu środowisku, w którym będzie poruszała się postać.
public class Building { }
I tak w zasadzie, to można powiedzieć: „Co Ty chcesz tu jeszcze dodawać? Przecież mamy już klasy rzeczywiste… Po co nam te całe interfejsy?”. Fakt. Mamy klasy rzeczywiste. I okej, można je zastosować i w klasie Building aplikacji napisać:
public class Building { MainBuildingPalaceStyle palace = new MainBuildingPalaceStyle(); MainBuildingStoneStyle palace = new MainBuildingStoneStyle (); MainBuildingWoodStyle palace = new MainBuildingWoodStyle(); }
Tylko co, gdybyśmy chcieli teraz na przykład chociażby mieć kilka takich budynków w liście i iterować po nich? Poza tym uzależniamy dalszy kod od rzeczywistych klas i taka nawet drobna zmiana stylu budynku głównego z pałacowego na kamienny będzie wymagała zmian w kilku, kilkunastu, może nawet kilkuset linijkach kodu. To wszystko zależy od tego jak bardzo nasza gra się rozbuduje, ale już gołym okiem widać, że im dalej w las tym więcej krzaków, nie? Z każdą następną modyfikacją, która będzie miała zawierać implementację budynku głównego uzależniamy nasz kod od tych trzech klas rzeczywistych. To teraz moja propozycja. Co powiecie na to?
public abstract class Building { List<MainBuilding> mainBuildingList = new ArrayList<>(); public abstract void createMainBuilding(); public void openFrontDoor(int whichOne) { mainBuildingList.get(whichOne).openFrontDoor(); } public void closeFrontDoor(int whichOne) { mainBuildingList.get(whichOne).closeFrontDoor(); } public void allLightsOn(int whichOne) { mainBuildingList.get(whichOne).allLightsOn(); } public void allLightsOff(int whichOne) { mainBuildingList.get(whichOne).allLightsOff(); } } public class PalaceBuilding extends Building { @Override public void createMainBuilding() { this.mainBuildingList.add(new MainBuildingPalaceStyle()); } // Jakaś dalsza logika różniąca się od pozostałych styli. } public class WoodenBuilding extends Building { @Override public void createMainBuilding() { this.mainBuildingList.add(new MainBuildingWoodStyle()); } // Jakaś dalsza logika różniąca się od pozostałych styli. } public class StoneBuilding extends Building { @Override public void createMainBuilding() { this.mainBuildingList.add(new MainBuildingStoneStyle()); } // Jakaś dalsza logika różniąca się od pozostałych styli. }
Proszę Państwa, przedstawiam Wam Metodę Fabrykującą createMainBuilding()! Poznajcie się. 🙂 Udostępnia ona interfejs do tworzenia obiektów w ramach klasy bazowej, ale pozwala podklasom zmieniać typ tworzonych obiektów. (definicja refactoring.guru).
public class Client { public static void main(String[] args) { Building palaceStyleBuilding = new PalaceBuilding(); allLightsOn(palaceStyleBuilding); } private static void allLightsOn(Building building) { building.allLightsOn(); } }
W ten sposób kod klienta uzależniamy wyłącznie od abstrakcyjnej klasy Building. Cokolwiek będziemy robić z aplikacją, klient będzie świadomy jedynie tego, że ma do czynienia z klasą Building i od tego, jaka klasa rzeczywista jest tam ukryta, zależy implementacja naszej metody fabrykującej, która również będzie zwracała (do pola klasy czy w naszym przypadku listy) typ MainBuilding pod którym będzie ukryta klasa MainBuildingPalaceStyle, MainBuildingWoodStyle czy MainBuildingStoneStyle w zależności, którą implementację Buildingu wybierzemy w kliencie. Jeszcze raz napiszę, że naprawdę polecam przykład ze strony refactoring.guru, który tłumaczy to na przykładzie firmy logistycznej i różnego rodzaju transportów (lądowych i wodnych). Muszą one mieć jeszcze dodatkowo inną implementację dostarczania towaru, więc tam zastosowanie Metody Fabrykującej nabiera jeszcze dodatkowego sensu.
Fabryka Abstrakcyjna
Okej, to co dalej? Mamy jeszcze do dyspozycji trzy inne elementy naszego miejsca akcji. Dodajmy je!
W tej chwili przechodzimy do rozwiązania, które można nazwać rozwiązaniem kompletnym i zastosować je w naszym przykładzie. Dodajmy fabryki!
Najpierw abstrakcyjna:
public interface AbstractHouseFactory { Chair createChair(); Bed createBed(); Garden createGarden(); MainBuilding createMainBuilding(); }
Następnie rzeczywiste:
public class PalaceStyleHouseFactory implements AbstractHouseFactory { @Override public Chair createChair() { return new ChairPalaceStyle(); } @Override public Bed createBed() { return new BedPalaceStyle(); } @Override public Garden createGarden() { return new GardenPalaceStyle(); } @Override public MainBuilding createMainBuilding() { return new MainBuildingPalaceStyle(); } } public class StoneStyleHouseFactory implements AbstractHouseFactory { @Override public Chair createChair() { return new ChairStoneStyle(); } @Override public Bed createBed() { return new BedStoneStyle(); } @Override public Garden createGarden() { return new GardenStoneStyle(); } @Override public MainBuilding createMainBuilding() { return new MainBuildingStoneStyle(); } } public class WoodStyleHouseFactory implements AbstractHouseFactory { @Override public Chair createChair() { return new ChairWoodStyle(); } @Override public Bed createBed() { return new BedWoodStyle(); } @Override public Garden createGarden() { return new GardenWoodStyle(); } @Override public MainBuilding createMainBuilding() { return new MainBuildingWoodStyle(); } }
Wzorzec Fabryka Abstrakcyjna to jakby metoda fabrykująca razy ileś. Składa się z interfejsu, który narzuca metody tworzenia takich obiektów abstrakcyjnych jakie programista chce stworzyć oraz zestawu fabryk rzeczywistych, które implementują sposób i typ tworzonych abstrakcyjnych obiektów pozbawiając je tym samym swojej abstrakcyjności. A tak prościej, to interfejs mówi: „Stwórz krzesło.”. No ale krzeseł jest wiele na świecie i każdy, kiedy wyobrazi sobie krzesło może otrzymać inny obraz w głowie. Klasy rzeczywiste określają właśnie konkretny styl jaki ma być zaaplikowany do procesu tworzenia krzesła. I w naszym przypadku mamy 3 do wyboru: pałacowy, drewniany i kamienny.
Teraz nasza klasa Building będzie wyglądała jakoś tak:
public class Building { private MainBuilding mainBuilding; private Bed bed; private Garden garden; private Chair chair; public Building(AbstractHouseFactory abstractHouseFactory) { this.mainBuilding = abstractHouseFactory.createMainBuilding(); this.bed = abstractHouseFactory.createBed(); this.garden = abstractHouseFactory.createGarden(); this.chair = abstractHouseFactory.createChair(); } public void openFrontDoor() { mainBuilding.openFrontDoor(); } public void closeFrontDoor() { mainBuilding.closeFrontDoor(); } public void allMainBuildingLightsOn() { mainBuilding.allLightsOn(); } public void allMainBuildingLightsOff() { mainBuilding.allLightsOff(); } public void moveBed() { bed.move(); } public void sleepInBed() { bed.sleep(); } public void sitOnBed() { bed.sitOn(); } public void turnOnSprinkler() { garden.turnOnSprinkler(); } public void turnOffSprinkler() { garden.turnOffSprinkler(); } public void turnOnLight() { garden.turnOnLight(); } public void turnOffLight() { garden.turnOffLight(); } public void moveChair() { chair.move(); } public void sitOnChair() { chair.sitOn(); } }
Idąc od samej góry, dodaliśmy kilka pól, czyli naszych slotów, w które będziemy umieszczać poszczególne obiekty jednostkowe, które będą tworzyć całą mapę gry. W konstruktorze pobieramy konkretną fabrykę rzeczywistą, z której otrzymujemy już w gotowym stylu nasze konkretne obiekty (budynek główny, ogród). Na końcu mamy implementacje wszystkich metod, które możemy użyć na naszych obiektach jednostkowych, żeby nie musieć odwoływać się bezpośrednio do nich. Dzięki temu nasz klient może wyglądać tak:
public class Client { public static void main(String[] args) { Building stoneHouse = new Building(new StoneStyleHouseFactory()); Building woodHouse = new Building(new WoodStyleHouseFactory()); Building palaceHouse = new Building(new PalaceStyleHouseFactory()); System.out.println("\nStone house"); stoneHouse.openFrontDoor(); stoneHouse.allMainBuildingLightsOn(); stoneHouse.turnOnSprinkler(); stoneHouse.sitOnBed(); stoneHouse.allMainBuildingLightsOff(); stoneHouse.sleepInBed(); System.out.println("\nWooden house"); woodHouse.openFrontDoor(); woodHouse.allMainBuildingLightsOn(); woodHouse.turnOnSprinkler(); woodHouse.sitOnBed(); woodHouse.allMainBuildingLightsOff(); woodHouse.sleepInBed(); System.out.println("\nPalace house"); palaceHouse.openFrontDoor(); palaceHouse.allMainBuildingLightsOn(); palaceHouse.turnOnSprinkler(); palaceHouse.sitOnBed(); palaceHouse.allMainBuildingLightsOff(); palaceHouse.sleepInBed(); } }
Po uruchomieniu kompilatora otrzymujemy:
Stone house
Opening STONE door...
Igniting TORCHES...
There's no sprinkler...
Sitting on uncomfortable stone bed. Stamina regenerated: 0 points.
Extinguishing TORCHES...
Sleeping in uncomfortable bed.
Stamina regenerated: 0 points.
Health regenerated: 0 points
Wooden house
Opening WOODEN door...
Turning on LAMPS...
Turning on sprinkler...
Sitting on wooden bed. Stamina regenerated: 10 points.
Turning off LAMPS...
Sleeping in wooden bed.
Stamina regenerated: 20 points.
Health regenerated: 15 points
Palace house
Opening DIAMOND door...
GLOWSTONE lights all the time...
Turning on sprinkler...
Sitting on ultra comfortable bed. Stamina regenerated: 1000 points.
GLOWSTONE lights all the time...
Sleeping in ultra comfortable bed.
Stamina regenerated: 100% points.
Health regenerated: 100% points
I tak może wyglądać jedno z rozwiązań naszego problemu. Ale co, jeśli będziemy chcieli trochę pomieszać rodziny produktów i mieć możliwość dodania Pałacowego budynku głównego wraz z drewnianym ogrodem? Okej, można napisać nową fabrykę rzeczywistą. Dobra, a co jeśli nie będziemy chcieli dodawać wszystkich elementów i na przykład pominąć dodawanie krzesła? Mało tego, co jeśli my nie wiemy, co użytkownik będzie chciał pominąć? Nie będziemy mogli tego wszystkiego predefniować. Trzeba będzie dać trochę wolności naszemu graczowi. Zatem czas na wzorzec Budowniczy!
Budowniczy
Po pierwsze… Rozmawialiśmy (w sumie to jest monolog, ale mam nadzieję, że nie jestem sam i Wy też, kiedy analizujecie jakiś kod, to przytakujecie do monitora czy znacząco dajecie do zrozumienia w gestach nieukrywanej irytacji, że nie rozumiecie o czym to ktoś do Was rozmawia…) o pustych slotach. W takim razie czym je zapełnimy, kiedy będą puste? Jak ten brak elementu zaznaczymy? Null? Może być, ale wtedy trzeba sprawdzać za pomocą instrukcji warunkowych, czy dany obiekt jest „not null” przed każdym wywołaniem czegokolwiek na tym obiekcie, bo inaczej możemy otrzymywać NullPointerException. A co powiecie na to, żeby stworzyć puste klasy?
public class HelpingMethods { public static void printEmptyMessage(String whatIsMissing) { System.out.println("You have not added " + whatIsMissing +"."); } } public class MainBuildingEmpty implements MainBuilding { String name = "MAIN BUILDING"; @Override public void openFrontDoor() { HelpingMethods.printEmptyMessage(name); } @Override public void closeFrontDoor() { HelpingMethods.printEmptyMessage(name); } @Override public void allLightsOn() { HelpingMethods.printEmptyMessage(name); } @Override public void allLightsOff() { HelpingMethods.printEmptyMessage(name); } } public class BedEmpty implements Bed { String name = "BED"; @Override public void move() { HelpingMethods.printEmptyMessage(name); } @Override public void sleep() { HelpingMethods.printEmptyMessage(name); } @Override public void sitOn() { HelpingMethods.printEmptyMessage(name); } } public class ChairEmpty implements Chair { String name = "CHAIR"; @Override public void move() { HelpingMethods.printEmptyMessage(name); } @Override public void sitOn() { HelpingMethods.printEmptyMessage(name); } } public class GardenEmpty implements Garden { String name = "GARDEN"; @Override public void turnOnSprinkler() { HelpingMethods.printEmptyMessage(name); } @Override public void turnOffSprinkler() { HelpingMethods.printEmptyMessage(name); } @Override public void turnOnLight() { HelpingMethods.printEmptyMessage(name); } @Override public void turnOffLight() { HelpingMethods.printEmptyMessage(name); } }
Ja zdecydowałem się na to, żeby każda metoda pustej klasy pokazywała ten sam napis na ekranie (ze zmienioną nazwą klasy), ale można dać na przykład wyrzucenie błędu:
throw new UnsupportedOperationException(HelpingMethods.printEmptyMessage(name));
Dalej zmodyfikowałbym klasę Building, żeby domyślnie zawsze definiowała puste klasy w swoich slotach a konstruktor zostawiła pusty. Zamiast definiowania wszystkich elementów na raz (w konstruktorze) przerzuciłbym tę kwestię do poszczególnych setterów:
public class Building { private MainBuilding mainBuilding = new MainBuildingEmpty(); private Bed bed = new BedEmpty(); private Garden garden = new GardenEmpty(); private Chair chair = new ChairEmpty(); public Building() {} public void setMainBuilding(MainBuilding mainBuilding) { this.mainBuilding = mainBuilding; } public void setBed(Bed bed) { this.bed = bed; } public void setGarden(Garden garden) { this.garden = garden; } public void setChair(Chair chair) { this.chair = chair; } // Metody takie jak openFrontDoor(), moveBed(), itd. pozostawione bez żadnej zmiany. }
Zatem stwórzmy naszych pierwszych budowniczych! Najpierw oczywiście w stronę popularnego Obiektowa interfejs (nie byłem wybitny z geografii, więc może dlatego do czasu przeczytania książki „Head First Design Patterns”, nigdy nie słyszałem o takiej miejscowości…):
public interface Builder { void setMainBuilding(); void setBed(); void setGarden(); void setChair(); Building getBuilding(); }
Następnie budowniczy…budowniczowie…builderzy rzeczywiści:
public class BuilderPalaceStyle implements Builder { Building building; public BuilderPalaceStyle() { this.building = new Building(); } @Override public void setMainBuilding() { building.setMainBuilding(new MainBuildingPalaceStyle()); } @Override public void setBed() { building.setBed(new BedPalaceStyle()); } @Override public void setGarden() { building.setGarden(new GardenPalaceStyle()); } @Override public void setChair() { building.setChair(new ChairPalaceStyle()); } @Override public Building getBuilding() { return building; } } public class BuilderStoneStyle implements Builder { Building building; public BuilderStoneStyle() { this.building = new Building(); } @Override public void setMainBuilding() { building.setMainBuilding(new MainBuildingStoneStyle()); } @Override public void setBed() { building.setBed(new BedStoneStyle()); } @Override public void setGarden() { building.setGarden(new GardenStoneStyle()); } @Override public void setChair() { building.setChair(new ChairStoneStyle()); } @Override public Building getBuilding() { return building; } } public class BuilderWoodStyle implements Builder { Building building; public BuilderWoodStyle() { this.building = new Building(); } @Override public void setMainBuilding() { building.setMainBuilding(new MainBuildingWoodStyle()); } @Override public void setBed() { building.setBed(new BedWoodStyle()); } @Override public void setGarden() { building.setGarden(new GardenWoodStyle()); } @Override public void setChair() { building.setChair(new ChairWoodStyle()); } @Override public Building getBuilding() { return building; } }
Budowniczy pozwala na większą swobodę w Kliencie, lecz tym samym daje większy wgląd w logikę naszej aplikacji i wymaga, aby Klient „wiedział” troche więcej niż taki Klient wzorca Fabryka Abstrakcyjna. Każdy budowniczy zawiera w swoim jedynym polu całe miejsce akcji i może dodawać do niego budynki na polecenie Klienta. Z tym, że każdy budowniczy obraca się tylko i wyłącznie w swoim stylu (pałacowym, drewnianym czy kamiennym). Ale w tej chwili możemy spokojnie dodawać tylko i wyłącznie to, co chcemy. Nie ma potrzeby już tworzyć na raz wszystkich elementów naszej mapy.
public class Client { public static void main(String[] args) { // Palace building System.out.println("Palace building."); Builder palaceBuilder = new BuilderPalaceStyle(); palaceBuilder.setMainBuilding(); palaceBuilder.setBed(); palaceBuilder.setChair(); palaceBuilder.setGarden(); Building myPalaceHouse = palaceBuilder.getBuilding(); myPalaceHouse.allMainBuildingLightsOff(); myPalaceHouse.sleepInBed(); // Stone building without garden System.out.println("\nStone building without garden"); Builder stoneBuilder = new BuilderStoneStyle(); stoneBuilder.setMainBuilding(); stoneBuilder.setBed(); stoneBuilder.setChair(); Building myStoneHouse = stoneBuilder.getBuilding(); myStoneHouse.openFrontDoor(); // Z pustymi klasami, kiedy wywołasz metodę "niezaimplementowanego" obiektu, // nie dostaniesz wyjątku NullPointerException. // Aplikacja po prostu wypisze Ci informację o "nieistniejącym" obiekcie. myStoneHouse.turnOnLightInTheGarden(); } }
Wyjście:
Palace building.
GLOWSTONE lights all the time...
Sleeping in ultra comfortable bed.
Stamina regenerated: 100% points.
Health regenerated: 100% points
Stone building without garden
Opening STONE door...
You have not added GARDEN.
I teraz całkiem istotny szczegół. Aktualnie niestety z poziomu Klienta można zrobić coś takiego:
public class Client { public static void main(String[] args) { // Palace building System.out.println("Palace building."); Builder palaceBuilder = new BuilderPalaceStyle(); palaceBuilder.setMainBuilding(); palaceBuilder.setBed(); palaceBuilder.setChair(); palaceBuilder.setGarden(); Building myPalaceHouse = palaceBuilder.getBuilding(); myPalaceHouse.setBed(new BedWoodStyle()); // <<<================ OTOTO! } }
Możemy z poziomu Klienta zmienić rodzaj każdego elementu. Dobrze byłoby zabronić mu dostępu do takich metod. Dlatego tak jak wcześniej nic nie mówiłem o paczkach (packages), tak teraz przydałoby się stworzyć jedną i wrzucić tam wszystkich budowniczych (razem z interfejsem oczywiście) i klasę Building:
- Builders (paczka)
- Builder (interfejs)
- BuilderPalaceStyle (klasa rzeczywista)
- BuilderWoodStyle (klasa rzeczywista)
- BuilderStoneStyle (klasa rzeczywista)
- Building (klasa rzeczywista)
…i następnie w klasie Building zmienić modyfikatory dostępu setterów na…nic. Po prostu usuńmy je:
public class Building { // To co było przed setterami bez zmian. void setMainBuilding(MainBuilding mainBuilding) { this.mainBuilding = mainBuilding; } void setBed(Bed bed) { this.bed = bed; } void setGarden(Garden garden) { this.garden = garden; } void setChair(Chair chair) { this.chair = chair; } // To co było za setterami bez zmian. }
W tej chwili te metody są widoczne tylko i wyłącznie dla klas w tej samej paczce, a niedostępne dla pozostałych. Dlatego budowniczowie mogą spokojnie korzystać z nich do woli i ustawiać, co chcą, a przy tym Klient nie ma pojęcia, że takie metody w tych klasach w ogóle istnieją. Sprawdźcie u siebie!
Kierownik
Opcjonalnym jest dodanie tzw. Kierownika (Director), który może zarządzać danym budowniczym i nakazywać co ma dodawać, a czego nie. Przydatne, jeżeli takie konkretne ustawienia będziemy powielać kilka razy. Wtedy podsyłamy konkretnego budowniczego takiemu kierownikowi, wywołujemy na nim metodę, np. make() i pobieramy gotowy budynek (od budowniczego nie kierownika!).
public interface Director { void changeBuilder(Builder builder); void make(); } // Kierownik, który dodaje wszystko. public class AllThingsDirector implements Director { Builder builder; public AllThingsDirector(Builder builder) { this.builder = builder; } @Override public void changeBuilder(Builder builder) { this.builder = builder; } @Override public void make() { builder.setMainBuilding(); builder.setBed(); builder.setChair(); builder.setGarden(); } }
Klient:
public class Client { public static void main(String[] args) { // Director System.out.println("\nWooden building made with director:"); Builder woodStyleBuilder = new BuilderWoodStyle(); Director allThingsDirector = new AllThingsDirector(woodStyleBuilder); allThingsDirector.make(); // Budynek pobieramy od budowniczego! Building myHouseMadeWithDirector = woodStyleBuilder.getBuilding(); myHouseMadeWithDirector.openFrontDoor(); myHouseMadeWithDirector.allMainBuildingLightsOn(); } }
Wyjście:
Wooden building made with director:
Opening WOODEN door...
Turning on LAMPS...
Napisałem już dwa razy, że budynek pobieramy od budowniczego. Ogólnie, to można w tym przypadku pobrać budynek bezpośrednio od kierownika dodając uprzednio metodę getBuilding(). Ale! Jest to możliwe tylko dlatego, że w tym przypadku zwracamy wyłącznie obiekty typu Building. Gdybyśmy tworzyli jeszcze do tego, dajmy na to instrukcję obsługi, to nie było by już tak kolorowo. Wtedy kierownik nie miałby pojęcia który typ zwrócić w metodzie getBuilding() i zresztą sama nazwa metody byłaby nieadekwatna. Potrzebna byłaby nowa nazwa, a w konsekwencji najlepiej nowa metoda, jeżeli trzymamy się schematu jedna funkcja/metoda – jedna odpowiedzialność. A jak takich produktów różnych typów by przybywało, to szybko odechciałoby się nam zwracania ich poprzez kierownika. Dlatego, podsumowując, w tym konkretnym przypadku można zaimplementować zwracanie całego miejsca akcji w klasie Kierownik (i stamtąd je pobierać). Lecz kiedy zwracany produkt nie koniecznie musi być jednego typu, to lepiej zostawić to konkretnemu budowniczemu, który ten typ zna.
Prototyp
Ogólnie to te dwa poprzednie wzorce są wystarczającymi rozwiązaniami i prototypu nie używałbym równolegle w tym przypadku, ale żeby porównać go z omówionymi już wzorcami konstrukcyjnymi, przedstawię go tutaj. Jest to mówiąc prosto klonowanie obiektów. Na samym początku naprawdę polecam prześledzić omówienie tego wzorca na stronie refactoring.guru w raz z przykładem w Javie. Dopiero po tym zapraszam to analizy naszej aplikacji pod kątem wzorca Prototyp. W Javie interfejs Cloneable jest dostępny od razu i można z niego skorzystać, my jednak moglibyśmy napisać go sami, tylko że… Skoro zwracany obiekt jest tylko i wyłącznie typu Building (a nie Circle czy Rectangle jak w przytoczonym przykładzie), nie widzę sensu tworzenia interfejsu. Dorzuciłbym tylko trzy rzeczy do naszej klasy Building:
- dodatkowy konstruktor,
- metodę clone(),
- wiązkę getterów (bo settery już mamy).
Zmodyfikowana klasa Building wyglądałaby jakoś w ten sposób:
public class Building { private MainBuilding mainBuilding = new MainBuildingEmpty(); private Bed bed = new BedEmpty(); private Garden garden = new GardenEmpty(); private Chair chair = new ChairEmpty(); public Building() { } // Cloneable feature ------------------------------------- private Building(Building building) { this.mainBuilding = building.getMainBuilding(); this.bed = building.getBed(); this.garden = building.getGarden(); this.chair = building.getChair(); } public Building clone() { return new Building(this); } // ------------------------------------------------------ public void getObjects() { System.out.println(mainBuilding.toString()); System.out.println(bed.toString()); System.out.println(garden.toString()); System.out.println(chair.toString()); } // Setters ------------------------ void setMainBuilding(MainBuilding mainBuilding) { this.mainBuilding = mainBuilding; } void setBed(Bed bed) { this.bed = bed; } void setGarden(Garden garden) { this.garden = garden; } void setChair(Chair chair) { this.chair = chair; } // ------------------------------ // Getters ------------------------ public MainBuilding getMainBuilding() { return mainBuilding; } public Bed getBed() { return bed; } public Garden getGarden() { return garden; } public Chair getChair() { return chair; } // ------------------------------ // Metody interakcji z konkretnymi elementami miejsca akcji. } public class Client { public static void main(String[] args) { // Simple cloning Building myOwnHouse = new Building(); myOwnHouse.setMainBuilding(new MainBuildingPalaceStyle()); myOwnHouse.setBed(new BedWoodStyle()); myOwnHouse.setGarden(new GardenPalaceStyle()); Building myOwnHouseClone = myOwnHouse.clone(); System.out.println("Object: "); myOwnHouse.openFrontDoor(); System.out.println("\nObject's clone: "); myOwnHouseClone.openFrontDoor(); System.out.println("\nObject = Object's clone? "); System.out.println(myOwnHouse == myOwnHouseClone); } }
Wyjście:
Object:
Opening DIAMOND door...
Object's clone:
Opening DIAMOND door...
Object = Object's clone?
false
Jak widać, drzwi w obydwu przypadkach są diamentowe (jest to styl pałacowy), ale obiekty nie są te same.
Można jeszcze zrobić coś w rodzaju presetu (ustalonych wcześniej ustawień), żeby to wyglądało trochę jak w konkretnym budowniczym – chodzi o stworzenie tzw. cache’a i wrzucenie tam ustalonych wcześniej konfiguracji. Dzięki temu będzie można pobierać kopie gotowych konstrukcji od ręki.
public class BuildingCache { private Map<String, Building> cache = new HashMap<>(); public BuildingCache() { Building buildingStyle1 = new Building(); buildingStyle1.setMainBuilding(new MainBuildingPalaceStyle()); buildingStyle1.setBed(new BedWoodStyle()); Building buildingStyle2 = new Building(); buildingStyle2.setMainBuilding(new MainBuildingWoodStyle()); buildingStyle2.setGarden(new GardenPalaceStyle()); buildingStyle2.setChair(new ChairWoodStyle()); cache.put("Building - style 1", buildingStyle1); cache.put("Building - style 2", buildingStyle2); } public Building put(String key, Building building) { cache.put(key, building); return building; } public Building get(String key) { return cache.get(key).clone(); } }
Klient:
public class Client { public static void main(String[] args) { // Clone from cache BuildingCache buildingCache = new BuildingCache(); Building cachedHouse = buildingCache.get("Building - style 2"); System.out.println("\nObject's clone from cache: "); cachedHouse.openFrontDoor(); } }
Wyjście:
Object's clone from cache - style 2:
Opening WOODEN door...
Zakończenie
Jeśli dotarłeś aż tutaj, to bardzo serdecznie Ci dziękuje za poświęcony czas i mam nadzieję, że choć w małym stopniu udało mi się przybliżyć Ci te cztery wzorce i ich powiązania między sobą. Jeśli nie, to jeszcze raz polecam pozycje „Head First Design Patterns”, refactoring.guru, które mi w dużej mierze okazały się pomocne. Z książki Head First przerabiałem przykład za przykładem w swoim IDE i uważam, że to właśnie zwiększyło poziom mojego zrozumienia tematu dwukrotnie w porównaniu do tego, gdybym tylko przeczytał rozdziały niniejszej książki. Dobrego dnia i powodzenia na szlaku programistycznym!
And now here is my secret, a very simple secret: It is only with the heart that one can see rightly; what is essential is invisible to the eye.
-- Little Prince --