Agregacja a kompozycja w UML i C# - jak je rozróżnić?

Bruno Krawczyk 13 czerwca 2026
Diagram klas pokazujący agregację vs kompozycję. Agregacja: Nauczyciel ma przedmioty. Kompozycja: Szkoła ma sale lekcyjne.

Spis treści

Projektując model domenowy, szybko trafiam na pytanie, czy jeden obiekt tylko korzysta z innych, czy naprawdę je posiada. To właśnie sedno różnicy między agregacją a kompozycją, dwóch relacji często mylonych w UML i kodzie obiektowym. Pokażę, jak rozpoznać każdą z nich, jak wpływa na cykl życia obiektów oraz jak zapisać ten wybór w C# i .NET.

O własności obiektu decyduje jego cykl życia

  • Agregacja oznacza luźną relację całość-część, w której element może istnieć niezależnie.
  • Kompozycja wiąże część z właścicielem znacznie silniej, także pod względem cyklu życia.
  • W UML agregację oznacza pusty romb, a kompozycję wypełniony romb.
  • Najlepszym testem jest pytanie, kto tworzy, usuwa i kontroluje część.
  • W C# sam typ referencji nie rozstrzyga relacji. Decydują reguły domenowe i publiczne API klas.

Aggregation vs composition w praktyce projektowania

W najprostszym ujęciu obie relacje opisują układ „całość ma części”. Różnica polega na sile tego związku. Przy agregacji obiekt nadrzędny przechowuje lub wykorzystuje inne obiekty, ale nie musi być ich właścicielem. Przy kompozycji część jest znacznie mocniej związana z całością i zwykle nie ma sensu poza nią.

Agregacja oznacza luźne posiadanie

Dobrym przykładem jest drużyna i zawodnicy. Drużyna może mieć listę zawodników, ale zawodnik istnieje także poza konkretną drużyną. Może zmienić klub, zostać wypożyczony albo być użyty w innym kontekście. Usunięcie drużyny nie powinno usuwać zawodnika.

W UML agregację pokazuje się pustym rombem umieszczonym przy klasie reprezentującej całość. W praktyce programistycznej wiele zespołów świadomie pomija ten symbol, ponieważ zwykła asocjacja często wystarcza i nie narzuca dodatkowych założeń dotyczących własności.

Kompozycja oznacza silną własność

Przy kompozycji część należy do jednej, konkretnej całości. Pozycja zamówienia jest częścią zamówienia, a akapit może być częścią konkretnego dokumentu. Bez obiektu nadrzędnego część traci swoje znaczenie biznesowe.

W UML kompozycję oznacza wypełniony romb. Część nie powinna jednocześnie należeć do dwóch kompozytów, a jej cykl życia jest kontrolowany przez właściciela. To nie jest jednak automatyczne „kasowanie obiektu” przez język programowania. W C# pamięć zwalnia garbage collector, natomiast zasady usuwania wynikają z modelu domenowego i warstwy persystencji.

Najważniejsza różnica to własność i cykl życia

Najprościej rozstrzygam ten dylemat, zadając trzy pytania. Kto tworzy część? Kto może ją zmienić lub przenieść? Co powinno się z nią stać po usunięciu obiektu nadrzędnego? Odpowiedzi często od razu pokazują, czy mamy do czynienia z agregacją, czy kompozycją.

Kryterium Agregacja Kompozycja
Samodzielne istnienie części Zwykle możliwe Zwykle nie ma sensu
Własność Luźna lub współdzielona Silna, przypisana do jednej całości
Tworzenie części Część może powstać niezależnie Najczęściej tworzy ją właściciel
Usunięcie całości Część nadal może istnieć Część zwykle powinna zostać usunięta lub unieważniona
Przenoszenie części Naturalne Ograniczone regułami domeny
Przykład Zespół i pracownik Zamówienie i pozycja zamówienia

Trzeba przy tym zachować ostrożność z przykładami. Ten sam zestaw klas może oznaczać różne relacje w różnych systemach. Pracownik może być niezależnym obiektem agregowanym przez zespół, ale w prostym systemie kadrowym rekord pracownika może być częścią konkretnego dokumentu zatrudnienia. Nie nazwa klas decyduje o relacji, tylko reguły biznesowe.

Warto też odróżnić kompozycję UML od ogólnego hasła „composition over inheritance”. To drugie oznacza budowanie zachowania z obiektów zamiast rozbudowywania hierarchii dziedziczenia. Jest to szersza technika projektowa. Kompozycja w UML opisuje natomiast przede wszystkim relację własności między częściami i całością.

Przykłady, które dobrze pokazują różnicę

Zespół i zawodnik jako agregacja

Zawodnik może istnieć bez zespołu, ponieważ ma własną tożsamość, historię i cykl życia. Zespół jedynie utrzymuje relację do zawodników. Taki model pasuje również do projektów, użytkowników i ról, a także kursów i zapisanych na nie studentów.

public sealed class Team
{
    private readonly List _players = new();

    public IReadOnlyCollection Players => _players;

    public void AddPlayer(Player player)
    {
        if (!_players.Contains(player))
        {
            _players.Add(player);
        }
    }

    public void RemovePlayer(Player player)
    {
        _players.Remove(player);
    }
}

public sealed class Player
{
    public Player(Guid id, string name)
    {
        Id = id;
        Name = name;
    }

    public Guid Id { get; }
    public string Name { get; }
}

Obiekt Player może zostać utworzony poza klasą Team, a później przypisany do innej drużyny. Sama lista nie robi jeszcze z relacji agregacji, ale dobrze pokazuje jej typowy kształt. Zawodnik ma własną tożsamość i może przeżyć zmianę właściciela.

Zamówienie i pozycje jako kompozycja

Pozycja zamówienia zwykle nie jest samodzielnym bytem. Ma znaczenie dlatego, że należy do konkretnego zamówienia, przechowuje jego kontekst i nie powinna być przypadkowo współdzielona z innym zamówieniem.

public sealed class Order
{
    private readonly List _lines = new();

    public IReadOnlyCollection Lines => _lines;

    public void AddProduct(Product product, int quantity)
    {
        if (quantity <= 0)
        {
            throw new ArgumentOutOfRangeException(nameof(quantity));
        }

        _lines.Add(new OrderLine(
            product.Id,
            product.Name,
            product.Price,
            quantity));
    }
}

public sealed record OrderLine(
    Guid ProductId,
    string ProductName,
    decimal UnitPrice,
    int Quantity);

W tym modelu zamówienie tworzy pozycję i wystawia na zewnątrz tylko odczyt kolekcji. To ogranicza przypadkowe modyfikacje oraz wzmacnia kontrolę właściciela. Produkt jest niezależnym obiektem, ale jego konkretna pozycja na zamówieniu już nie.

W bazie danych trzeba tę regułę odwzorować osobno. Sama kompozycja w kodzie nie zagwarantuje usunięcia rekordów potomnych. Potrzebne mogą być ograniczenia kluczy obcych, kaskadowe usuwanie albo jawna obsługa transakcji. W EF Core podobny model można dodatkowo zaznaczyć jako typ należący do właściciela, ale decyzja nadal powinna wynikać z domeny, a nie z wygody mapowania.

Jak zapisać właściwy wybór w C# i .NET

W C# agregację często implementuję przez przekazanie gotowych obiektów do konstruktora lub metody. Klasa nadrzędna zna kontrakt zależności, ale nie musi zarządzać całym jej życiem. Tak działa wiele relacji między serwisami, użytkownikami, zespołami i zasobami.

Kompozycję wzmacniam przez ukrycie konstruktorów części, ograniczenie setterów oraz operacje wykonywane przez właściciela. Zamiast pozwalać dowolnemu kodowi dodać pozycję, adres czy element konfiguracji, udostępniam metody, które pilnują reguł.

public sealed class Document
{
    private readonly List _paragraphs = new();

    public IReadOnlyList Paragraphs => _paragraphs;

    public void AddParagraph(string text)
    {
        if (string.IsNullOrWhiteSpace(text))
        {
            throw new ArgumentException("Treść nie może być pusta.", nameof(text));
        }

        _paragraphs.Add(Paragraph.Create(text));
    }
}

public sealed class Paragraph
{
    private Paragraph(string text)
    {
        Text = text;
    }

    public string Text { get; }

    internal static Paragraph Create(string text)
        => new(text);
}

To nadal nie jest magiczna gwarancja kompozycji. Jeśli inna część systemu może przechwycić obiekt Paragraph, zapisać go niezależnie i używać po usunięciu dokumentu, model staje się słabszy. Enkapsulacja pomaga utrzymać decyzję projektową, ale nie zastępuje świadomych reguł w repozytorium i bazie danych.

Podobna ostrożność przydaje się przy dependency injection. Wstrzykiwanie zależności jest formą składania obiektów i poprawia testowalność, ale nie oznacza automatycznie kompozycji UML. Serwis może zostać wstrzyknięty do wielu klientów, więc jego cykl życia jest niezależny. To typowy przykład luźnej zależności, a nie relacji całość-część.

Typowe błędy przy rozróżnianiu obu relacji

Traktowanie każdego pola kolekcji jako kompozycji

Fakt, że klasa ma właściwość typu List, niczego jeszcze nie przesądza. Lista może przechowywać niezależne obiekty, na przykład użytkowników przypisanych do projektu. Może też zawierać elementy należące wyłącznie do właściciela. Struktura kodu jest wskazówką, nie dowodem.

Mylenie agregacji z dziedziczeniem

Relacja „ma” nie jest tym samym co relacja „jest”. Samochód ma silnik, więc rozważamy relację części i całości. Samochód jest pojazdem, więc mówimy o dziedziczeniu lub generalizacji. Pomieszanie tych dwóch pytań prowadzi do hierarchii klas, które szybko stają się trudne do zmiany.

Wymuszanie kompozycji tam, gdzie potrzebna jest współdzielona niezależność

Silne zamykanie obiektów wewnątrz właściciela bywa kuszące, ale nie zawsze jest dobre. Jeśli ten sam obiekt ma być używany przez kilka modułów albo przenoszony między kontekstami, kompozycja stworzy niepotrzebne ograniczenia. W takim przypadku lepsza jest asocjacja lub agregacja.

Przeczytaj również: Data fabric w praktyce - warstwy, wzorce i wdrożenie w Azure

Wiara, że UML rozwiąże problem za zespół

Diagram pomaga porozumieć się co do intencji, ale nie zastąpi decyzji o transakcjach, usuwaniu i odpowiedzialności. W dużych systemach szczególnie ważne jest ustalenie granic agregatów domenowych. Jeżeli zespół nie potrafi opisać cyklu życia obiektu słowami, sam romb na diagramie niewiele pomoże.

Moja praktyczna reguła jest prosta. Wybieram kompozycję, gdy część nie ma własnej tożsamości poza całością i właściciel powinien kontrolować wszystkie operacje. Wybieram agregację, gdy element żyje samodzielnie, ma własny identyfikator albo może zmienić kontekst użycia.

Najlepszy test przed dodaniem rombu do diagramu

Przed oznaczeniem relacji odpowiadam na jedno konkretne pytanie: czy część może sensownie istnieć po usunięciu całości? Jeśli tak, zaczynam od zwykłej asocjacji lub agregacji. Jeśli nie, sprawdzam, czy właściciel tworzy część, kontroluje jej modyfikację i odpowiada za jej usunięcie. Wtedy kompozycja jest uzasadniona.

Nie traktuję przy tym pustego i wypełnionego rombu jako celu samego w sobie. Najważniejszy jest czytelny model, szczelne granice odpowiedzialności i kod, który nie pozwala łatwo złamać reguł domeny. Dobrze dobrana relacja sprawia, że późniejsze decyzje w C#, EF Core i bazie danych wynikają z jednego spójnego założenia, zamiast być zbiorem przypadkowych wyjątków.

FAQ - Najczęstsze pytania

Agregacja oznacza luźną relację, w której część może istnieć niezależnie od całości, na przykład zawodnik może zmienić drużynę. Kompozycja wiąże część z jedną konkretną całością, jak pozycję zamówienia z zamówieniem. W UML agregację oznacza pusty romb, a kompozycję wypełniony romb.

Sprawdź, kto tworzy część, kto kontroluje jej modyfikację oraz co dzieje się z nią po usunięciu obiektu nadrzędnego. Jeśli część ma własną tożsamość i może zmienić kontekst, pasuje agregacja. Jeśli właściciel tworzy ją, ogranicza dostęp do niej i odpowiada za jej cykl życia, uzasadniona jest kompozycja.

Nie. Właściwość typu List<T> może przechowywać niezależne obiekty, takie jak użytkownicy przypisani do projektu, albo elementy należące wyłącznie do właściciela. O relacji decydują reguły domenowe, a nie sama struktura kodu.

W C# warto ukryć konstruktory części, ograniczyć settery i udostępnić operacje wykonywane przez właściciela, na przykład metodę dodającą pozycję zamówienia. W bazie danych regułę trzeba odwzorować osobno za pomocą kluczy obcych, kaskadowego usuwania lub jawnej obsługi transakcji. Garbage collector nie zapewnia automatycznie usunięcia powiązanych rekordów.

Oceń artykuł

Ocena: 0.00 Liczba głosów: 0

Tagi

agregacja
kompozycja
uml
c#
ef core
Autor Bruno Krawczyk
Bruno Krawczyk
Mam na imię Bruno i od 8 lat zgłębiam tajniki programowania w ekosystemie .NET, chmury Azure oraz sztucznej inteligencji. Moja przygoda z technologią zaczęła się od ciekawości, jak złożone systemy mogą ułatwiać codzienne życie i rozwiązywać realne problemy. Dziś moją misją jest dzielenie się tą wiedzą, starając się przybliżyć nawet najbardziej skomplikowane zagadnienia w sposób zrozumiały i przystępny dla każdego. W moich artykułach na kursdotnet.pl skupiam się na praktycznych aspektach, analizuję najnowsze trendy i weryfikuję informacje, aby dostarczyć Wam treści, które są nie tylko dokładne i aktualne, ale przede wszystkim użyteczne w Waszej własnej ścieżce rozwoju technologicznego.

Udostępnij artykuł

Napisz komentarz