Composite (Sammansatt)
Introduktion
Composite är ett strukturmönster som låter dig komponera objekt i trädstrukturer för att representera del-helhet-hierarkier. Det låter klienter behandla enskilda objekt och sammansättningar av objekt på ett enhetligt sätt.
Problem
Du bygger ett system för att beräkna totalkostnaden för en order. En order innehåller produkter — men en produkt kan vara en låda som i sin tur innehåller fler produkter och lådor.
Utan ett mönster behöver klientkoden skilja på enskilda produkter och sammansatta lådor: if (item instanceof Box) { for (OrderItem child : item.getChildren()) ... }. Logiken sprids ut och koden bryter ihop varje gång hierarkin djupnar.
Lösning
Composite definierar ett gemensamt gränssnitt för både löv (enskilda objekt) och grenar (sammansatta objekt). Klientkoden anropar getPrice() på ett objekt — det spelar ingen roll om det är en produkt eller en hel låda. Lådor rekurserar automatiskt ned i sina barn.
När ska Composite användas?
- När du behöver representera del-helhet-hierarkier (träd).
- När du vill att klientkoden ska kunna behandla enskilda objekt och kompositioner likadant.
- T.ex. filsystem, organisationsscheman, UI-komponenthierarkier, matematiska uttryck.
Struktur
| Roll | Ansvar |
|---|---|
| Component Interface | Gemensamt gränssnitt för löv och grenar. |
| Leaf | Enkelt objekt utan barn — utför faktiskt arbete. |
| Composite | Sammansatt objekt med barn — delegerar till barnen. |
Exempel — Orderberäkning (Java)
Scenariot: En order kan innehålla produkter direkt eller i förpackade lådor. getPrice() beräknar alltid rätt totalpris, oavsett hur djupt trädet är.
import java.util.ArrayList;
import java.util.List;
// ── Component Interface ───────────────────────────────────
public interface OrderItem {
String getName();
double getPrice();
void print(String indent);
}
// ── Leaf ──────────────────────────────────────────────────
public class Product implements OrderItem {
private final String name;
private final double price;
public Product(String name, double price) {
this.name = name;
this.price = price;
}
@Override public String getName() { return name; }
@Override public double getPrice() { return price; }
@Override
public void print(String indent) {
System.out.printf("%s📦 %s: %.2f kr%n", indent, name, price);
}
}
// ── Composite ─────────────────────────────────────────────
public class Box implements OrderItem {
private final String name;
private final double packagingCost;
private final List<OrderItem> children = new ArrayList<>();
public Box(String name, double packagingCost) {
this.name = name;
this.packagingCost = packagingCost;
}
public void add(OrderItem item) { children.add(item); }
public void remove(OrderItem item) { children.remove(item); }
@Override public String getName() { return name; }
// Rekursiv beräkning — delegerar till alla barn
@Override
public double getPrice() {
double total = packagingCost;
for (OrderItem child : children) {
total += child.getPrice();
}
return total;
}
@Override
public void print(String indent) {
System.out.printf("%s🗃️ %s (förpackning: %.2f kr)%n", indent, name, packagingCost);
for (OrderItem child : children) {
child.print(indent + " ");
}
System.out.printf("%s Delsumma: %.2f kr%n", indent, getPrice());
}
}
// ── Klientkod ─────────────────────────────────────────────
public class Application {
public static void main(String[] args) {
// Enskilda produkter
Product laptop = new Product("Laptop", 12999.00);
Product mouse = new Product("Mus", 299.00);
Product keyboard = new Product("Tangentbord", 699.00);
Product hdmi = new Product("HDMI-kabel", 99.00);
Product usb = new Product("USB-hubb", 349.00);
// En låda med tillbehör
Box accessoriesBox = new Box("Tillbehörslåda", 29.00);
accessoriesBox.add(mouse);
accessoriesBox.add(keyboard);
accessoriesBox.add(hdmi);
accessoriesBox.add(usb);
// Huvudlåda som innehåller laptop + tillbehörslådan
Box mainBox = new Box("Huvudlåda", 49.00);
mainBox.add(laptop);
mainBox.add(accessoriesBox);
// Klientkoden bryr sig inte om strukturen — bara getPrice()
System.out.println("=== Orderöversikt ===\n");
mainBox.print("");
System.out.printf("%nTotalt att betala: %.2f kr%n", mainBox.getPrice());
// Enskild produkt och komposition behandlas identiskt
OrderItem enEllerFler = mainBox; // Eller: new Product("Kabel", 49.0)
System.out.printf("Pris via gränssnitt: %.2f kr%n", enEllerFler.getPrice());
}
}
Output:
=== Orderöversikt ===
🗃️ Huvudlåda (förpackning: 49.00 kr)
📦 Laptop: 12999.00 kr
🗃️ Tillbehörslåda (förpackning: 29.00 kr)
📦 Mus: 299.00 kr
📦 Tangentbord: 699.00 kr
📦 HDMI-kabel: 99.00 kr
📦 USB-hubb: 349.00 kr
Delsumma: 1475.00 kr
Delsumma: 14523.00 kr
Totalt att betala: 14523.00 kr
Pris via gränssnitt: 14523.00 kr
Fördelar
- Klientkoden är enkel — ingen
instanceof-kontroll för att skilja på löv och grenar. - Open/Closed Principle — Lägg till nya typer av komponenter utan att ändra klientkoden.
- Träd kan byggas upp dynamiskt vid runtime i valfri djuplek.
Nackdelar
- Det kan vara svårt att begränsa vilka typer av komponenter som kan läggas i ett Composite.
- Gränssnittet måste vara tillräckligt generellt för att täcka både löv och grenar, vilket ibland leder till metoder som inte är meningsfulla för båda typerna.
Av Victor Hernandez från Bytebase.se