Composite
Kurz gesagt: Composite gibt einem einzelnen Teil und einer Gruppe dieselbe Form, sodass ein Baum aus Produkten und Boxen mit einem Aufruf behandelt wird, egal wie tief er ist.
Problem
Eine Box in einer Box in einer Box
Der Shop verkauft Geschenkboxen. Die Weihnachtsbox enthält Tee, Honig und eine kleinere Box mit Bonbons und Schokolade. Jede Box hat außerdem einen Preis für ihre Verpackung. Die Bestellung enthält eine Kerze und die Weihnachtsbox. Die Daten sehen so aus:
class Product { constructor(name, price) { this.name = name; this.price = price; }}
class Box { constructor(name, packaging, items) { this.name = name; this.packaging = packaging; this.items = items; }}Die Summe wird berechnet, indem man prüft, was jedes Teil ist: ein Produkt oder eine Box. Für eine Box gibt es eine zweite Schleife:
function total(order) { let sum = 0; for (const item of order) { if (item instanceof Product) { sum += item.price; } else if (item instanceof Box) { sum += item.packaging; for (const inner of item.items) { if (inner instanceof Product) { sum += inner.price; } } } } return sum;}const order = [ new Product("Kerze", 5), new Box("Weihnachtsbox", 2, [ new Product("Tee", 4.5), new Product("Honig", 6), new Box("Kleine Box", 1, [ new Product("Bonbons", 3.5), new Product("Schokolade", 2.5)])])];console.log(`Gesamt: ${total(order).toFixed(2)} €`);Gesamt: 17.50 €Die Bestellung kostet 24,50 €, und das Programm sagt 17,50 €. Die kleine Box liegt auf der dritten Ebene. Die zweite Schleife sieht sie, aber sie ist kein Produkt und verschwindet zusammen mit den Bonbons und der Schokolade aus der Summe. Es gibt keinen Fehler.
Eine dritte verschachtelte Schleife repariert diese Bestellung, aber nicht eine Box auf der vierten Ebene. Und dieselbe Kette von Prüfungen steckt auch im Zählen der Produkte und im Gewicht für den Kurier.
Die Idee
Composite: Teil und Ganzes mit einer Form
Das Produkt und die Box werden dasselbe gefragt: Wie viel kostest du? Das Produkt antwortet mit seinem Preis. Die Box antwortet mit ihrer Verpackung plus den Antworten von allem, was in ihr liegt.
Also haben beide dieselbe Methodetotal(). Die Box hält eine Liste von Teilen und fragt nicht, was sie sind: Sie ruft bei jedem total() auf. Ein Interface ist nicht nötig, solange jedes Teil diese Methode hat.Das ist das Muster Composite. Es hat drei Rollen:
- die gemeinsame Form: die Methode
total(); - das Blatt: das Produkt, das selbst rechnet;
- der Container: die Box, die Kinder mit derselben Form hält und jedes fragt.
Eine Box in der Box ist einfach ein Kind, das auch seine Kinder fragt. So gehen die Aufrufe bis zu den Produkten hinunter, egal wie viele Ebenen es gibt: Das ist Rekursion durch Objekte. Der Code, der nach der Summe fragt, weiß nicht, ob er ein Produkt, eine Box oder einen ganzen Baum in der Hand hat.
Ein Ordner mit Dateien hat dieselbe Form: Die Größe einer Datei ist ihre eigene, und die Größe eines Ordners ist die Summe von allem darin, auch der Ordner darin.
Sieh es dir an
Die Summe geht bis ganz unten und kommt zurück
Das Produkt und die Box. Die Box nimmt beliebig viele Teile (...items):
class Product { constructor(name, price) { this.name = name; this.price = price; }
total() { return this.price; }}
class Box { constructor(name, packaging, ...items) { this.name = name; this.packaging = packaging; this.items = items; }
total() { let sum = this.packaging; for (const item of this.items) { sum += item.total(); } return sum; }}Der Baum wird als geschachtelte Konstruktoren geschrieben, ein Teil pro Zeile:
const order = new Box("Bestellung", 0, new Product("Kerze", 5), new Box("Weihnachtsbox", 2, new Product("Tee", 4.5), new Product("Honig", 6), new Box("Kleine Box", 1, new Product("Bonbons", 3.5), new Product("Schokolade", 2.5))));console.log(`Gesamt: ${order.total().toFixed(2)} €`);Gesamt: 24.50 €const order = new Box("Bestellung", 0, new Product("Kerze", 5), new Box("Weihnachtsbox", 2, new Product("Tee", 4.5), new Product("Honig", 6), new Box("Kleine Box", 1, new Product("Bonbons", 3.5), new Product("Schokolade", 2.5))));console.log(`Gesamt: ${order.total().toFixed(2)} €`);Schritt für Schritt, in Worten
- Der Baum wird zusammengesetzt: Jeder Artikel steht in einer eigenen Zeile, und jede Box hält ihre Kinder.
- total() fragt die Wurzel. Die Bestellung weiß nicht, was drin ist: Sie beginnt bei ihrer Verpackung und ruft total() bei jedem Kind auf.
- Kerze ist ein Produkt: Es antwortet mit seinem Preis, 5.00, und die Antwort geht wieder nach oben.
- Weihnachtsbox ist eine Box: Sie macht genau dasselbe, erst die Verpackung, dann ein Kind nach dem anderen.
- Kleine Box ist auf der dritten Ebene. Hier verschwindet nichts: Sie fragt ihre eigenen Kinder.
- Kleine Box: 1.00 + 3.50 + 2.50 = 7.00. Ihre Summe geht hoch zum Elternteil.
- Weihnachtsbox: 2.00 + 4.50 + 6.00 + 7.00 = 19.50.
- Bestellung: 0.00 + 5.00 + 19.50 = 24.50. Das gibt das Programm aus.
Nirgends gibt es eine Typprüfung und nirgends eine Zahl von Ebenen. Die Box tut auf jeder Ebene dasselbe: die Verpackung plus die Summe der Kinder.
Sieh es dir an
Eine neue Art von Teil, ohne die Box anzufassen
Der Shop verkauft jetzt Kaffeebohnen für 24 € pro Kilo. Ihr Preis ist der Preis pro Kilo mal die Kilos. Das ist eine neue Klasse mit derselben Form:
class WeighedProduct { constructor(name, perKg, kg) { this.name = name; this.perKg = perKg; this.kg = kg; }
total() { return this.perKg * this.kg; }}const coffee = new Box("Kaffeebox", 1, new WeighedProduct("Kaffeebohnen", 24, 0.25), new Product("Tasse", 8));console.log(`Kaffeebox: ${coffee.total().toFixed(2)} €`);Kaffeebox: 15.00 €Keine einzige Zeile von Box hat sich geändert. Die Box ruft total() beim Kaffee auf wie bei jedem anderen Teil.
Hier nimmt die Box alles. Text an der Stelle eines Produkts geht ohne Fehler hinein, und das Programm bricht erst bei der Summe ab:
const box = new Box("Box", 1, "Tee");console.log(box.total());TypeError: item.total is not a functionDas ist immer noch besser als im Problem, wo so ein Teil stillschweigend übersprungen wurde: Hier zeigt sich der Fehler, sobald jemand nach der Summe fragt.
Genauer, für Neugierige
Eine neue Art von Teil ist leicht: eine Klasse. Eine neue Operation kostet mehr: Brauchst du neben der Summe auch ein Gewicht für den Kurier, kommt die Methode weight() in jede Klasse. Composite ist stark dort, wo die Arten von Teilen wachsen und die Operationen wenige sind.
Probier es aus
Sag die Ausgabe voraus
Die Box enthält eine leere Box und eine Kerze. Was gibt das Programm aus?
const box = new Box("Box", 1, new Box("Leer", 0.5), new Product("Kerze", 5));console.log(`Box: ${box.total().toFixed(2)} €`);Probier es aus
Finde den Fehler
Jemand schreibt die Box um, damit sie „sicherer“ wird:
class Box { constructor(name, packaging, ...items) { this.name = name; this.packaging = packaging; this.items = items; }
total() { let sum = this.packaging; for (const item of this.items) { if (item instanceof Product) { sum += item.total(); } } return sum; }}const gift = new Box("Weihnachtsbox", 2, new Product("Tee", 4.5), new Product("Honig", 6), new Box("Kleine Box", 1, new Product("Bonbons", 3.5), new Product("Schokolade", 2.5)));console.log(`Weihnachtsbox: ${gift.total().toFixed(2)} €`);Weihnachtsbox: 12.50 €Die Weihnachtsbox kostet 19,50 €, und das Programm sagt 12,50 €. In welcher Zeile steckt der Fehler?
Wähle die Zeile mit dem Fehler.
Die Prüfung lässt nur Produkte durch. Die kleine Box darin ist kein Produkt, also verschwinden sie, die Bonbons und die Schokolade (7 €) aus der Summe. Das ist derselbe Fehler wie im Problem, nur im Inneren der Box versteckt. Der Container in Composite fragt nicht, was das Kind ist: Er ruft bei jedem total() auf. Entferne die Prüfung, und die Summe ist 19,50 €.
Die Idee
Wann du es nicht brauchst
Composite hat einen Preis: eine gemeinsame Form und eine Rekursion, die du lesen musst, um eine Summe zu verstehen. Es lohnt sich nicht, wenn:
- die Dinge in einer flachen Liste liegen. Ein Warenkorb ohne Boxen ist eine Liste, und die Summe ist eine Schleife;
- es genau zwei Ebenen gibt und es dabei bleibt: eine Bestellung mit Positionen. Eine Bestellposition enthält keine Bestellung;
- die Operationen zahlreich werden und es nur zwei Arten von Teilen gibt. Jede neue Operation wird in jeder Klasse geschrieben. Dann ist es manchmal klarer, wenn eine Funktion den Baum mit einer Prüfung der Art durchläuft, aber an einer Stelle.
Es lohnt sich, wenn Teile und Ganze auf beliebig vielen Ebenen ineinander geschachtelt sind: Boxen, Ordner, Kategorien in einem Katalog, Menüs mit Untermenüs, Elemente auf einer Seite.
Übung
Der Kassenbon
Löse die Aufgabe in deinem Editor.
Der Kassenbon als Baum. Füge dem Produkt und der Box eine Methode print(indent = "") hinzu. Das Produkt gibt seinen Namen und seinen Preis aus. Die Box gibt ihren Namen und ihre Summe aus und ruft dann print bei jedem Kind mit zwei Leerzeichen mehr auf. Gib die Bestellung aus der Karte „Die Summe geht bis ganz unten und kommt zurück“ aus:
Bestellung: 24.50 € Kerze: 5.00 € Weihnachtsbox: 19.50 € Tee: 4.50 € Honig: 6.00 € Kleine Box: 7.00 € Bonbons: 3.50 € Schokolade: 2.50 €Weitere Aufgaben
- Wie viele Produkte. Füge
count()hinzu: Ein Produkt ist 1, und eine Box ist die Summe ihrer Kinder. Die Verpackung ist kein Produkt. Die Bestellung hat 5. - Ordner und Dateien. Baue eine Datei mit Namen und Größe in KB und einen Ordner mit Dateien und Ordnern. Gib die Größe jedes Ordners aus. Was hat das mit den Boxen gemeinsam?
- Das teuerste Produkt. Finde den Preis des teuersten Produkts im ganzen Baum. Überlege, was eine leere Box zurückgibt.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Exceptions · Arrays: push, splice und sort · Daten, switch und Destrukturierung · Entwurfsmuster
- Geht weiter in: Command
- Aufgaben zum Thema: Entwurfsmuster