Implementacja wzorca Kompozyt na przykładzie hierarchii produktów

Cześć! Cieszę się, że mnie odwiedziłeś/aś. Zanim przejdziesz do artykułu chciałbym zwrocić Ci uwagę na to, że ten artykuł był pisany kilka lat temu (2014-02-19) miej więc proszę na uwadzę że rozwiązania i przemyślenia które tu znajdziesz nie muszą być aktualne. Niemniej jednak zachęcam do przeczytania.

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

Dzięki za wizytę,
Mateusz Mazurek
Mateusz M.

Pokaż komentarze

Ostatnie wpisy

Podsumowanie: maj, czerwiec, lipiec i sierpień 2024

Oj daaawnoo mnie tu nie było. Ale wakacje to był czas dużej liczby intensywnych wyjazdów i tak naprawdę, dopiero jakoś… Read More

2 miesiące ago

Podsumowanie: kwiecień 2024

Cześć! Zapraszam na krótkie podsumowanie kwietnia. Wyjazd do Niemiec A dokładniej pod granicę z Francją. Chrześnica miała pierwszą komunię. Po… Read More

6 miesięcy ago

Podsumowanie: luty i marzec 2024

Ostatnio tygodnie były tak bardzo wypełnione, że nie udało mi się napisać nawet krótkiego podsumowanie. Więc dziś zbiorczo podsumuję luty… Read More

7 miesięcy ago

Podsumowanie: styczeń 2024

Zapraszam na krótkie podsumowanie miesiąca. Książki W styczniu przeczytałem "Homo Deus: Historia jutra". Książka łudząco podoba do wcześniejszej książki tego… Read More

9 miesięcy ago

Podsumowanie roku 2023

Cześć! Zapraszam na podsumowanie roku 2023. Książki Zacznijmy od książek. W tym roku cel 35 książek nie został osiągnięty. Niemniej… Read More

10 miesięcy ago

Podsumowanie: grudzień 2023

Zapraszam na krótkie podsumowanie miesiąca. Książki W grudniu skończyłem czytać Mein Kampf. Nudna książka. Ciekawsze fragmenty można by było streścić… Read More

11 miesięcy ago