Wprowadzenie
Obiekty mają relacje między sobą, zarówno w prawdziwym życiu jak i w programowaniu. Czasami trudno jest zrozumieć lub zaimplementować te relacje.
W tym poradniku, skupimy się na trzech typach relacji w Javie: kompozycji, agregacji i asocjacji.
Kompozycja
Kompozycja jest relacją typu „należy do”. Oznacza to, że jeden z obiektów jest logicznie większą strukturą, która zawiera drugi obiekt. Innymi słowy, jest on częścią lub członkiem innego obiektu.
Alternatywnie, często nazywamy to relacją „ma-a” (w przeciwieństwie do relacji „jest-a”, która jest dziedziczeniem).
Na przykład, pokój należy do budynku, lub innymi słowy budynek ma pokój. Więc w zasadzie, czy nazywamy to „belongs-to” czy „has-a” jest tylko kwestią punktu widzenia.
Kompozycja jest silnym rodzajem relacji „has-a”, ponieważ obiekt zawierający jest jej właścicielem. Dlatego cykle życia obiektów są powiązane. Oznacza to, że jeśli zniszczymy obiekt będący właścicielem, jego członkowie również zostaną zniszczeni wraz z nim. Na przykład, pokój zostanie zniszczony wraz z budynkiem w naszym poprzednim przykładzie.
Zauważ, że nie oznacza to, że obiekt zawierający nie może istnieć bez żadnej ze swoich części. Na przykład, możemy zburzyć wszystkie ściany wewnątrz budynku, a więc zniszczyć pokoje. Ale budynek nadal będzie istniał.
Pod względem kardynalności, obiekt zawierający może mieć tyle części, ile chcemy. Jednak wszystkie części muszą mieć dokładnie jeden kontener.
2.1. UML
W UML-u kompozycję oznaczamy następującym symbolem:
Zauważ, że diament znajduje się przy obiekcie zawierającym i jest podstawą linii, a nie grotem strzałki. Dla jasności, często rysujemy również grot strzałki:
Więc, możemy użyć tej konstrukcji UML dla naszego przykładu Building-Room:
2.2. Kod źródłowy
W Javie, możemy to zamodelować za pomocą niestatycznej klasy wewnętrznej:
class Building { class Room {} }
Alternatywnie, możemy zadeklarować tę klasę również w ciele metody. Nie ma znaczenia czy jest to klasa nazwana, anonimowa czy lambda:
class Building { Room createAnonymousRoom() { return new Room() { @Override void doInRoom() {} }; } Room createInlineRoom() { class InlineRoom implements Room { @Override void doInRoom() {} } return new InlineRoom(); } Room createLambdaRoom() { return () -> {}; } interface Room { void doInRoom(); }}
Zauważ, że istotne jest, aby nasza klasa wewnętrzna była niestatyczna, ponieważ wiąże ona wszystkie swoje instancje z klasą zawierającą.
Zwykle obiekt zawierający chce mieć dostęp do swoich członków. Dlatego powinniśmy przechowywać ich referencje:
class Building { List<Room> rooms; class Room {} }
Zauważ, że wszystkie obiekty klasy wewnętrznej przechowują niejawną referencję do swojego obiektu zawierającego. Dzięki temu, nie musimy przechowywać jej ręcznie, aby mieć do niej dostęp:
class Building { String address; class Room { String getBuildingAddress() { return Building.this.address; } } }
Agregacja
Agregacja jest również relacją typu „ma-a”. To, co odróżnia ją od kompozycji, to fakt, że nie wiąże się z posiadaniem. Dzięki temu, cykle życia obiektów nie są powiązane: każdy z nich może istnieć niezależnie od siebie.
Na przykład, samochód i jego koła. Możemy zdjąć koła, a one nadal będą istnieć. Możemy zamontować inne (wcześniej istniejące) koła, lub zainstalować je do innego samochodu i wszystko będzie działać dobrze.
Oczywiście, samochód bez kół lub z oderwanym kołem nie będzie tak użyteczny jak samochód z kołami. Ale właśnie dlatego ta relacja istniała od samego początku: aby złożyć części w większą konstrukcję, która jest w stanie zrobić więcej rzeczy niż jej części.
Ponieważ agregacja nie wiąże się z posiadaniem, członek nie musi być związany tylko z jednym kontenerem. Na przykład, trójkąt jest zbudowany z odcinków. Ale trójkąty mogą dzielić odcinki jako swoje boki.
3.1. UML
Agregacja jest bardzo podobna do kompozycji. Jedyną logiczną różnicą jest to, że agregacja jest słabszą relacją.
W związku z tym reprezentacje UML są również bardzo podobne. Jedyną różnicą jest to, że diament jest pusty:
Dla samochodów i kół zrobilibyśmy więc:
3.2. Kod źródłowy
W Javie, możemy modelować agregację za pomocą zwykłej starej referencji:
class Wheel {}class Car { List<Wheel> wheels;}
Członkiem może być dowolny typ klasy, z wyjątkiem niestatycznej klasy wewnętrznej.
W powyższym wycinku kodu obie klasy mają swój osobny plik źródłowy. Jednakże, możemy również użyć statycznej klasy wewnętrznej:
class Car { List<Wheel> wheels; static class Wheel {}}
Zauważ, że Java utworzy niejawną referencję tylko w niestatycznych klasach wewnętrznych. Z tego powodu musimy ręcznie utrzymywać relację tam, gdzie jej potrzebujemy:
class Wheel { Car car;}class Car { List<Wheel> wheels;}
Asocjacja
Asocjacja jest najsłabszą relacją pomiędzy tymi trzema. To nie jest relacja „ma-a”, żaden z obiektów nie jest częścią lub członkiem innego.
Asocjacja oznacza tylko, że obiekty „znają” się nawzajem. Na przykład, matka i jej dziecko.
4.1. UML
W UML-u możemy oznaczyć asocjację za pomocą strzałki:
Jeśli asocjacja jest dwukierunkowa, możemy użyć dwóch strzałek, strzałki z grotem na obu końcach lub linii bez grotów:
W UML-u możemy przedstawić matkę i jej dziecko, wtedy:
4.2. Kod źródłowy
W Javie możemy modelować asocjację w taki sam sposób jak agregację:
class Child {}class Mother { List<Child> children;}
Ale zaraz, jak możemy stwierdzić, czy referencja oznacza agregację czy asocjację?
No cóż, nie możemy. Różnica jest tylko logiczna: czy jeden z obiektów jest częścią drugiego, czy nie.
Ale musimy również ręcznie utrzymywać referencje na obu końcach, tak jak zrobiliśmy to z agregacją:
class Child { Mother mother;}class Mother { List<Child> children;}
UML Sidenote
Dla jasności, czasami chcemy zdefiniować kardynalność relacji na diagramie UML. Możemy to zrobić zapisując ją na końcach strzałek:
Zauważ, że nie ma sensu zapisywać zera jako kardynalności, ponieważ oznacza to, że nie ma żadnej relacji. Jedynym wyjątkiem jest sytuacja, gdy chcemy użyć zakresu do wskazania opcjonalnej relacji:
Zauważ też, że ponieważ w kompozycji jest dokładnie jeden właściciel, nie wskazujemy go na diagramach.
Złożony przykład
Zobaczmy (trochę) bardziej złożony przykład!
Zamodelujemy uniwersytet, który ma swoje wydziały. W każdym z wydziałów pracują profesorowie, którzy również mają między sobą przyjaciół.
Czy wydziały będą istniały po zamknięciu uniwersytetu? Oczywiście, że nie, więc jest to kompozycja.
Ale profesorowie będą istnieć (miejmy nadzieję). Musimy zdecydować, co jest bardziej logiczne: czy uważamy profesorów za część wydziałów, czy nie. Ewentualnie: czy są oni członkami wydziałów czy nie? Tak, są. Stąd jest to agregacja. Na dodatek, profesor może pracować na wielu wydziałach.
Związek między profesorami jest asocjacyjny, bo nie ma sensu mówić, że profesor jest częścią innego.
W rezultacie możemy zamodelować ten przykład za pomocą następującego diagramu UML:
A kod Java wygląda tak:
class University { List<Department> department; }class Department { List<Professor> professors;}class Professor { List<Department> department; List<Professor> friends;}
Zauważmy, że jeśli oprzemy się na pojęciach „has-a”, „belongs-to”, „member-of”, „part-of” i tak dalej, możemy łatwiej zidentyfikować relacje między naszymi obiektami.
Podsumowanie
W tym artykule poznaliśmy właściwości i reprezentację kompozycji, agregacji i asocjacji. Zobaczyliśmy również jak modelować te relacje w UMLu i Javie.
Jak zwykle, przykłady są dostępne na GitHubie.
Zacznij przygodę ze Spring 5 i Spring Boot 2, dzięki kursowi Learn Spring:
>> CHECK OUT THE COURSE