Implementacja wzorca Kompozyt na przykładzie hierarchii produktów
Cześć,
ostatnio wpadłem, całkiem przypadkiem na ciekawy wzorzec projektowy o nazwie Composite – czyli Kompozyt. Zakłada on prostą i pomysłową implementację struktur drzewiastych.
Problem który rozwiążemy to fakt że większość produktów to produkty złożone – składające się z innych produktów.
Rozłożymy na części (niezbyt szczegółowo) komputer. Zaimplementujemy taką strukturę:
Ale przed przystąpieniem do implementacji tego, chciałbym pokazać Wam czym jest tytułowy wzorzec Kompozyt. Popatrzmy na diagram UML:
A więc mamy klasę bazową (może być ona interfejsem jeśli zachodzi taka potrzeba implementacyjna), która ma podstawowe działania potrzebne do operacji na drzewie:
– Add
– Remove
– GetChild
które odpowiednio dodają dziecko, usuwają dziecko i pobierają dziecko o odpowiednim indeksie. Dodatkowo jest operacja, która będzie wykonywana dla każdego dziecka w drzewie, poczynając od wybranego przez nas node’a. A wracając do diagramu, to z klasy bazowej dziedziczą (lub odpowiednio w przypadku interfejsu – implementują) dwie klasy:
– Leaf
– Composite
Leaf jest pojedynczym liściem bez dzieci, natomiast Composite jest węzłem (liściem z dziećmi).
Dobra, przygotujmy klasę bazową, u mnie będzie ona abstrakcyjna (nie ma potrzeby utworzenia jej instancji, ale jest potrzeba do otrzymania jednolitego API):
1 2 3 4 5 6 7 8 9 10 11 | public abstract class ProductAbstract { protected abstract String getProductName(); public void showMe(){ System.out.println("Produkt "+this.getProductName()); } public abstract void addChild(ProductAbstract p); public abstract void removeChild(ProductAbstract p); } |
Jak widać nasza klasa to ProductAbstract. Klasy pochodne:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | public class Product extends ProductAbstract { String name; public Product(String name) { this.name=name; } @Override protected String getProductName() { return name; } @Override public void addChild(ProductAbstract p) { System.out.println("Nie można dodać."); } @Override public void removeChild(ProductAbstract p) { System.out.println("Brak dzieci."); } } |
no i Composite (owy Kompozyt):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | public class Composite extends ProductAbstract { private ArrayList<ProductAbstract> _children = new ArrayList<ProductAbstract>(); String name; public Composite(String name) { this.name=name; } @Override protected String getProductName() { return name; } @Override public void showMe() { super.showMe(); for(ProductAbstract pa : _children) { pa.showMe(); } } @Override public void addChild(ProductAbstract p) { _children.add(p); } @Override public void removeChild(ProductAbstract p) { _children.remove(p); } } |
Zobacz w jaki sposób to zrobiłem. Dwie klasy typu ProductAbstract, obie maja to samo API (otrzymujemy jednolity sposób zarządzania obiektem, nie ważne czy jest prostym czy złożonym liściem), ale jedna ma listę swoich dzieci. Zwróć uwagę na operację showMe() – w przypadku liścia to wyświetlenie tekstu, w przypadku wywołanie funkcji z klasy wyżej oraz wywołanie jej dla wszystkich dzieci. Czy już widzisz konsekwencje tego?
Dobra, a co do problemu produktów, to możemy zrobić to tak:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | public class Main { public static void main(String[] args) { Composite main = new Composite("Komputer"); //root Composite keyboard = new Composite("Klawiatura"); Composite alfaKeyboard = new Composite("Klawiatura alfanumeryczna");/ alfaKeyboard.addChild(new Product("Klawisze funkcyjne F1-F12")); keyboard.addChild(alfaKeyboard); keyboard.addChild(new Product("Klawiatura numeryczna")); Composite system_unit = new Composite("Jednostka Centralna"); system_unit.addChild(new Product("Napęd DVD")); system_unit.addChild(new Product("Diody sygnalizacyjne")); Composite processor = new Composite("Procesor"); Composite registers = new Composite("Rejestry"); registers.addChild(new Product("Przerzutnik")); processor.addChild(registers); processor.addChild(new Product("ALU")); system_unit.addChild(new Product("Dysk twardy")); system_unit.addChild(new Product("Karta Graficzna")); system_unit.addChild(new Product("Pamięć")); system_unit.addChild(processor); main.addChild(keyboard); main.addChild(new Product("Monitor")); main.addChild(new Product("Mysz")); main.addChild(system_unit); main.showMe(); } } |
Zobacz architekturę tego założenia. Gdy potrzebujesz mieć węzeł tworzysz obiekt Composite i dodajesz np do innego Composite. Dzięki zastosowaniu klasy bazowej, możemy dodawać do węzłów inne węzły jak i liście.
W wyniku uruchomienia programu otrzymamy coś takiego:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | Produkt Komputer Produkt Klawiatura Produkt Klawiatura alfanumeryczna Produkt Klawisze funkcyjne F1-F12 Produkt Klawiatura numeryczna Produkt Monitor Produkt Mysz Produkt Jednostka Centralna Produkt Napęd DVD Produkt Diody sygnalizacyjne Produkt Dysk twardy Produkt Karta Graficzna Produkt Pamięć Produkt Procesor Produkt Rejestry Produkt Przerzutnik Produkt ALU |
Jak przeanalizujesz kod, to zobaczysz że to co dostaliśmy to rekurencyjne przeszukanie naszego drzewa, metodą Przeszukiwania w głąb.
Takie przeszukiwanie da nam pewność odwiedzenia każdego elementu drzewa.
Gdy popatrzymy na wynik działania programu to mało intuicyjnie widać co jest złożone z czego, prawda? Spróbujmy zrobić to ładniej.
Wzorce projektowe nie rozwiązują problemów dosłownie, a dają nam pomysł, sprawdzoną drogę, którą można pójść w celu rozwiązania owego problemu, więc nie bójmy się modyfikować wzorców – bierzmy z nich pomysł, ale udoskonalajmy wg potrzeb. Nie ma sensu, podobnie jak z podejściami do zwinnego wytwarzania oprogramowania, trzymać się sztywno reguł. Warto eksperymentować, zbierać feedback i wybierać najlepsze rozwiązania dla naszego kontekstu.
A więc chcemy jakoś zasygnalizować to, na jakiej „głębokości” dany produkt jest. Możemy to obliczać na wiele różnych sposobów, ja spróbuję dedukować już na poziomie łączenia węzłów, na jakim poziomie są i w razie potrzeby, wprowadzać korektę.
Będziemy potrzebować kilku zmian już w klasie bazowej:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | public abstract class ProductAbstract { public abstract String getProductName(); protected int depth=0; protected String name; public void showMe(){ System.out.println(this.generateLvl(depth)+"> Produkt "+this.getProductName()); } private String generateLvl(int n){ StringBuilder sb = new StringBuilder(); while(n>0) { sb.append("-"); n--; } return sb.toString(); } public abstract void addChild(ProductAbstract p); protected abstract void repair(); public abstract void removeChild(ProductAbstract p); } |
Jak widać dodaliśmy zmienną wskazująca na poziom głębokości, a przy okazji poprawiłem fakt, że zmienna przechowująca nazwę powinna być zadeklarowana już na tym poziomie ;)
Metoda showMe() tworzy już nie tylko nazwę produktu a prosty wskaźnik informujące na którym poziomie jesteśmy w formie:
> dla poziomu 0
-> dla poziomu 1
–> dla poziomu 2
i tak dalej.
Metoda generateLvl() – tworzy właśnie odpowiednią ilość znaku „-” :)
Doszła deklaracja metody repair() – będzie ona służyła do ewentualnej korekty poziomu.
Klasa Product w sumie się nie zmieniła, metoda repair() będzie pusta.
Composite ma zaimplementowane repair()..
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | public class Composite extends ProductAbstract { private ArrayList<ProductAbstract> _children = new ArrayList<ProductAbstract>(); public Composite(String name) { this.name=name; } @Override public String getProductName() { return name; } @Override public void showMe() { super.showMe(); for(ProductAbstract pa : _children) { pa.showMe(); } } @Override public void addChild(ProductAbstract p) { _children.add(p); p.depth=this.depth+1; p.repair(); } @Override public void removeChild(ProductAbstract p) { _children.remove(p); } @Override protected void repair() { for(ProductAbstract pa : _children) { pa.depth=this.depth+1; pa.repair(); } } } |
Co się dzieje tutaj teraz. Popatrzmy na nieco zmieniony schemat drzewa:
Zauważ proszę, że zaznaczone poddrzewa również są drzewami, więc każdy z nich ma swoją własną wysokość i swoje własne poziomy i dopiero po podczepieniu do innego drzewa, te poziomy muszą być zaktualizowane. Więc gdy dodajemy coś do node’a to temu czemuś ustawiamy poziom o jeden większy od rodzica. Ma to sens, nie? Ale jednocześnie wywołujemy dla jego dzieci metodę która przebudowuje poziomy danego poddrzewa.
Zobacz ten proces na innym, prostszym drzewie:
Popatrz że jak usuwasz jakiś poziom nie musisz nic przebudowywać, gdyż usunięcie nie wpływa na poziomy wyżej.
Takie rozwiązanie ma swoje plusy i minusy.
Minusem jest przede wszystkim to że node’y są od siebie zależne, tzn jeśli wywołamy showMe() na innym węźle to dostaniemy poziomy odpowiednie dla całego drzewa (nie od zera).
Możemy to poprawić na wiele sposób, ja zaprezentuje użycie interfejsu – niech AbstractProduct implementuje interfejs:
1 2 3 4 5 | public interface CanBeIndependent { public void makeIndependent(); } |
w Product makeIndependent() będzie zerowało depth a w Composite zerowało i wywoływało repair().
W konsekwencji wywołania kodu:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | public class Main { public static void main(String[] args) { Composite main = new Composite("Komputer"); //root Composite keyboard = new Composite("Klawiatura"); //node Composite alfaKeyboard = new Composite("Klawiatura alfanumeryczna");//node alfaKeyboard.addChild(new Product("Klawisze funkcyjne F1-F12")); //leaf keyboard.addChild(alfaKeyboard); keyboard.addChild(new Product("Klawiatura numeryczna"));//leaf Composite system_unit = new Composite("Jednostka Centralna"); system_unit.addChild(new Product("Napęd DVD")); system_unit.addChild(new Product("Diody sygnalizacyjne")); Composite processor = new Composite("Procesor"); Composite registers = new Composite("Rejestry"); registers.addChild(new Product("Przerzutnik")); processor.addChild(registers); processor.addChild(new Product("ALU")); system_unit.addChild(new Product("Dysk twardy")); system_unit.addChild(new Product("Karta Graficzna")); system_unit.addChild(new Product("Pamięć")); system_unit.addChild(processor); main.addChild(keyboard); main.addChild(new Product("Monitor")); main.addChild(new Product("Mysz")); main.addChild(system_unit); processor.makeIndependent(); processor.showMe(); } } |
Dostaniemy przedstawienie jedynie procesora, zaczynając od poziomu zero. Gdy zostawimy makeIndependent() na processor a wywoałny showMe() dla main to nie dostaniemy poprawnego wyniku. Żeby całkowicie odseparować Processor od drzewa, trzeba pierw go usunąć z system_unit a potem makeIndependent() wywołać na obiekcie processor, co da nam dwa całkowicie osobne drzewa ;)
Pozdrawiam! ;)
PS. Dodam jeszcze efekt końcowy skryptu bez odłączania drzew:
> Produkt Komputer
-> Produkt Klawiatura
–> Produkt Klawiatura alfanumeryczna
—> Produkt Klawisze funkcyjne F1-F12
–> Produkt Klawiatura numeryczna
-> Produkt Monitor
-> Produkt Mysz
-> Produkt Jednostka Centralna
–> Produkt Napęd DVD
–> Produkt Diody sygnalizacyjne
–> Produkt Dysk twardy
–> Produkt Karta Graficzna
–> Produkt Pamięć
–> Produkt Procesor
—> Produkt Rejestry
—-> Produkt Przerzutnik
—> Produkt ALU
Mateusz Mazurek
Takiego dobrego wytłumaczenia to jeszcze nie widziałem, pozdro !
Dzięki!