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 is Box) { foreach (var child in item.Children) ... }. 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

RollAnsvar
Component InterfaceGemensamt gränssnitt för löv och grenar.
LeafEnkelt objekt utan barn — utför faktiskt arbete.
CompositeSammansatt objekt med barn — delegerar till barnen.

Exempel — Orderberäkning (C# / .NET)

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.

using System.Collections.Generic;

// ── Component Interface ───────────────────────────────────

public interface IOrderItem
{
    string Name     { get; }
    decimal GetPrice();
    void Print(string indent = "");
}

// ── Leaf ──────────────────────────────────────────────────

public class Product : IOrderItem
{
    private readonly decimal _price;

    public string Name { get; }

    public Product(string name, decimal price)
    {
        Name   = name;
        _price = price;
    }

    public decimal GetPrice() => _price;

    public void Print(string indent = "") =>
        Console.WriteLine($"{indent}📦 {Name}: {_price:C}");
}

// ── Composite ─────────────────────────────────────────────

public class Box : IOrderItem
{
    private readonly List<IOrderItem> _children = [];
    private readonly decimal          _packagingCost;

    public string Name { get; }

    public Box(string name, decimal packagingCost = 0m)
    {
        Name           = name;
        _packagingCost = packagingCost;
    }

    public void Add(IOrderItem item) => _children.Add(item);

    public void Remove(IOrderItem item) => _children.Remove(item);

    // Rekursiv beräkning — delegerar till alla barn
    public decimal GetPrice()
    {
        decimal total = _packagingCost;
        foreach (IOrderItem child in _children)
            total += child.GetPrice();
        return total;
    }

    public void Print(string indent = "")
    {
        Console.WriteLine($"{indent}🗃️  {Name} (förpackning: {_packagingCost:C})");
        foreach (IOrderItem child in _children)
            child.Print(indent + "   ");
        Console.WriteLine($"{indent}   Delsumma: {GetPrice():C}");
    }
}

// ── Klientkod ─────────────────────────────────────────────

// Enskilda produkter
Product laptop    = new("Laptop",     12999.00m);
Product mouse     = new("Mus",          299.00m);
Product keyboard  = new("Tangentbord",  699.00m);
Product hdmi      = new("HDMI-kabel",    99.00m);
Product usb       = new("USB-hubb",     349.00m);

// En låda med tillbehör
Box accessoriesBox = new("Tillbehörslåda", packagingCost: 29.00m);
accessoriesBox.Add(mouse);
accessoriesBox.Add(keyboard);
accessoriesBox.Add(hdmi);
accessoriesBox.Add(usb);

// Huvudlåda som innehåller laptop + tillbehörslådan
Box mainBox = new("Huvudlåda", packagingCost: 49.00m);
mainBox.Add(laptop);
mainBox.Add(accessoriesBox);

// Klientkoden bryr sig inte om strukturen — bara GetPrice()
Console.WriteLine("=== Orderöversikt ===\n");
mainBox.Print();

Console.WriteLine($"\nTotalt att betala: {mainBox.GetPrice():C}");

// Enskild produkt och komposition behandlas identiskt
IOrderItem enEllerFler = mainBox; // Eller: new Product("Kabel", 49m)
Console.WriteLine($"\nPris via gränssnitt: {enEllerFler.GetPrice():C}");

Output:

=== Orderöversikt ===

🗃️  Huvudlåda (förpackning: 49,00 kr)
   📦 Laptop: 12 999,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: 1 475,00 kr
   Delsumma: 14 523,00 kr

Totalt att betala: 14 523,00 kr

Pris via gränssnitt: 14 523,00 kr

Fördelar

  • Klientkoden är enkel — ingen if/else 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