Mateusz Mazurek – programista z pasją

Python, architektura, ciekawostki ze świata IT

Algorytmika Inżynieria oprogramowania Programowanie Programowanie webowe

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ę:
drzewo
Ale przed przystąpieniem do implementacji tego, chciałbym pokazać Wam czym jest tytułowy wzorzec Kompozyt. Popatrzmy na diagram UML:
Diagram UML Kompozyt

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:
tree2

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:
Untitled Diagram

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

A może wolisz nowości na mail?

Subskrybuj
Powiadom o
guest

Witryna wykorzystuje Akismet, aby ograniczyć spam. Dowiedz się więcej jak przetwarzane są dane komentarzy.

2 komentarzy
Inline Feedbacks
View all comments
Włodi

Takiego dobrego wytłumaczenia to jeszcze nie widziałem, pozdro !

Mateusz M.

Dzięki!