Kihagyás

Composite tervezési minta

A tervezési minták alapvető célja, hogy a sokaknál előforduló problémákra adjon egy olyan általános megoldásvázat, amelyet aztán a konkrét problémára ültetve, arra specializálva egy jó minőségű megoldást kapunk.

Az előadáson is ismertetett egyik tervezési minta a Composite (magyarul talán összetételnek mondhatjuk).

Célja

Rész-egész szerkezetek leképezése objektum-hierarchiára (fa-szerkezetre), miközben a kliens a rész és egész egységeket azonosan kezeli. Ez annyit jelent, hogy egy olyan fa-struktúrát hozunk létre, amelyben a kliens az összetett egységet és a rész-egységet nem tudja megkülönböztetni, ugyanúgy tudja kezelni.

Ezt úgy érjük el, hogy egy fa-szerkezetet csinálunk, amelyet használ a kliens (lásd a Szerkezete részt). A fa attól lesz fa, hogy komponensekből áll, amiknek konkrét megvalósításai lehetnek: a levél, vagy pedig köztes "csomópont", ami szintén komponensekből áll. És ezeket a kliens csak komponensként látja, nem tudja konkrétan, hogy levél, vagy köztes csomópont.

Először is érdemes utánanézni, hogy mi az a fa-szerkezet.

Szerkezete

Composite tervezési minta

Az ábrán a kliens nincs rajta, az előadásban viszont úgy szerepel. Mindkettő elfogadható, az a lényeg, hogy a kliens, aki használja ezt a fa-szerkezetet, csak annyit tud az egyes elemekről, hogy komponensek, más egyebet nem.

A Componentnek megfelelő komponens általában interfész, de lehet osztály is, az UML-diagramon jelenleg osztályként van reprezentálva.

A Leaf az egy "végállomás", ez mindig egy konkrét valami, ami már "elemi" részét fogja képezni a szerkezetnek.

A Composite pedig egy összetett objektum, amely tartalmaz Component típusú objektumokat (általában listában vagy tömbben). Ez egy olyan objektum, ami az őt tartalmazó komponensek segítségével épül fel (ezt el lehet úgy képzelni, mint egy legó várat, ami legó kockákból épül fel. A kockák már "elemiek", ők csak vannak, és nem épülnek fel semmiből. Ezzel szemben a várak ezekből az "elemi" kockákból épülnek fel).

De tekintsünk egy példát, remélhetőleg érthető lesz.

Példa

A Composite tervezési minta sokszor használt, például felhasználó felületeknél.

A jelen példákban talán a legközismertebb fa-szerkezetet fogjuk megvalósítani, mégpedig a könyvtár-hierarchiát. Egy könyvtáron belül lehet egy másik könyvtár, vagy lehetnek benne fájlok, sőt, akár vegyíthetjük is őket.

A fő feladat, amit szeretnénk megvalósítani az az legyen, hogy bármilyen komponensnek tudjuk lekérni a méretét, akár fájlnak, akár könyvtárnak (és természetesen egy könyvtár mérete a benne lévő fájlok méretének összege).

A mintát ismerve alakítsuk ki tehát a szükséges osztályokat:

  • Client: egy kliens, ami használni fogja a fa-szerkezetet, jelen esetben legyen egy sima egyszerű osztály, amelynek main függvényében létrehozunk pár könyvtárat, pár fájlt, és ezt a mappaszerkezetet kiíratjuk egy általunk definiált függvénnyel: CommanderClient.java
  • Component: Egy olyan interfész, amelyben a szükséges műveleteket definiáljuk. Jelen példában ez a Resource.java lesz.
  • Leaf: Levél osztály(ok adott esetben), amely(ek)re elemiként gondolunk, amelyeket nem építenek fel más elemek. A példában egy ilyen levél lesz, az pedig a File osztály. File.java
  • Composite: Ez a Folder.java fájl lesz, a könyvtárakat reprezentáló osztály, amelyekben komponensek lesznek (azaz Resource-ok).

Kezdjük is a Resource-szal, ami jelenleg egy interfész lesz, de lehetne akár egy osztály, vagy absztrakt osztály is. Bármely Resourcenak lehessen lekérni a nevét és a méretét (ez lenne a lényeg nekünk!).

public interface Resource {
    String getName();
    int getSize();
}

Ezt az interfészt két osztály is meg fogja valósítani, a levél, ami a File lesz, illetve a köztes kompozit egység, a könyvtár. Kezdjük a File megvalósításával, aminek lesz neve, illetve mérete.

public class File implements Resource {
    private String name;
    private int size;

    public File(String name, int size) {
        super();
        this.name = name;
        this.size = size;
    }

    @Override
    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setSize(int size) {
        this.size = size;
    }

    @Override
    public int getSize() {
        return this.size;
    }

    @Override
    public String toString() {
        return this.name;
    }
}

A másik pedig maga a kompozit rész, a könyvtár lesz. Ez már kicsit komplikáltabb, ugyanis a kompozit tárolni fog további komponenseket (ami vagy levél (Fájl), vagy kompozit (Mappa) lesz). Jelenleg legyen a megvalósítás ArrayList, de ez persze nem szükségszerű, bárhogy lehetne, az egyszerűség kedvéért választottuk ezt. Szóval a mappa (kompozit) tárolni fog mappákat vagy fájlokat. Ezért az ArrayList az Resource-okat fog tárolni (amik lehetnek File objektumok, vagy Folder objektumok).

import java.util.ArrayList;
import java.util.List;

public class Folder implements Resource {

    private String name;
    private List<Resource> resources = new ArrayList<>();

    public Folder(String name) {
        super();
        this.name = name;
    }

    public void addResource(Resource resource) {
        this.resources.add(resource);
    }

    public List<Resource> getResources() {
        return this.resources;
    }

    @Override
    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public int getSize() {
        int size = 0;
        for (Resource resource: this.resources) {
            size += resource.getSize();
        }
        return size;
    }

    @Override
    public String toString() {
        return this.name + "/"; // Mappak vegere tegyunk '/' jelet
    }

}

És kész is vagyunk! Elkészült a fa-szerkezetünk, megvan a komponens (Resource), leaf (levél, File osztály), és composite (kompozit, Folder, ami komponenseket tárol magában).

Most már lehet egy mappánk, amiben vannak mappák, amiben vannak fájlok, mappák vegyesen. A mappákban lehetnek mappák, fájlok, és így tovább.

Egy egyszerű main függvényt is készítettünk hozzá, illetve egy printTreeStructure nevű metódust, aminek feladata, hogy bejárja a fa-szerkezetet, és a benne lévő fájlokat, mappákat rekurzívan kiírassa.

public class CommanderClient {

    public static void main(String[] args) {
        Folder root = new Folder("root");
        Folder dev = new Folder("dev");
        root.addResource(dev);
        Folder nul = new Folder("null");
        dev.addResource(nul);
        Folder home = new Folder("home");
        root.addResource(home);
        Folder user = new Folder("gazficko");
        home.addResource(user);
        File readme = new File("readme.txt", 11);
        File linux = new File("debian.iso", 2048);
        user.addResource(readme);
        user.addResource(linux);
        File welcome = new File("motd", 200);
        root.addResource(welcome);

        System.out.println("total size: " + root.getSize());

        printTreeStructure(root, "/", "");
    }

    public static void printTreeStructure(Resource resource, String directoryPath, String indent) {
        if (resource instanceof Folder) { // ha mappa, akkor jarjuk be azt is, es a kiiratashoz az utvonalat frissitsuk be
            Folder r = (Folder) resource;
            System.out.println(indent + r);
            for (Resource rs: r.getResources()) {
                printTreeStructure(rs, directoryPath + r, "    " + indent);
            }
        } else {
            // ha fajl, akkor irassuk ki az indentalast, majd a fajlt es a teljes utvonalat.
            System.out.println(indent + resource + "\t(full path: " + directoryPath + resource + ")");
        }
    }
}

Ennek kimenete:

total size: 2259
root/
    dev/
        null/
    home/
        gazficko/
            readme.txt  (full path: /root/home/gazficko/readme.txt)
            debian.iso  (full path: /root/home/gazficko/debian.iso)
    motd    (full path: /root/motd)

Utolsó frissítés: 2024-04-11 07:54:27