zhuang@linux:~/notes/software-design/uml-classes-relationships/$ cat
UML Class Relationships: A Practical Guide
UML class diagrams describe both the static structure of a system and the ways in which its types interact. The most common relationships are dependency, association, aggregation, composition, generalization, and realization.
At a Glance
| Relationship | UML notation | Meaning | Typical code evidence |
|---|---|---|---|
| Dependency | Dashed arrow | One element temporarily uses another | Parameter, local variable, return type, or static method call |
| Association | Solid line, optionally with an arrow | One object knows about or retains another | Field or property referencing another object |
| Aggregation | Solid line with a hollow diamond at the whole | A weak whole-part association | A retained part that can exist independently and may be shared |
| Composition | Solid line with a filled diamond at the whole | A strong whole-part association | A part exclusively owned and lifetime-controlled by the whole |
| Generalization | Solid line with a hollow triangle pointing to the parent | An is-a relationship between a subtype and a supertype | Class inheritance |
| Realization | Dashed line with a hollow triangle pointing to the contract | A type implements a specification | Interface implementation |
The arrow or triangle points toward the element being depended on, navigated to, inherited from, or implemented. A diamond is different: it is placed at the whole end of a whole-part relationship.
Dependency
A dependency means that a change to one element may affect another because the latter uses it. The use is normally temporary and does not imply that one object stores the other.
ReportService - - - > ReportFormatterclass ReportService {
public:
Report createReport(const ReportFormatter& formatter) {
return formatter.format(loadData());
}
};ReportService depends on ReportFormatter because it receives and uses one during the method call, but it does not retain it as part of its state.
Common sources of dependencies include:
- Method parameters and return types
- Local variables
- Object construction
- Static method calls
- Exceptions and annotations
Dependency is the weakest relationship in this list. Avoid drawing every possible dependency, because doing so can make a class diagram unreadable. Include only dependencies that communicate something important about the design.
Association
An association is a structural relationship between instances. It usually means that one object keeps a reference to another object and can communicate with it over time.
Customer 1 ───── 0..* Orderclass Order {
private:
Customer* customer;
};An association may specify:
- Multiplicity:
1,0..1,*,0..*, or1..* - Navigability: which object can reach the other
- Role names: how each end participates in the relationship
- Association name: a verb or phrase describing the connection
A plain solid line can represent bidirectional or unspecified navigability. An open arrowhead on an association can show one-way navigability. Whether an association also implies ownership depends on the model; association alone does not define object lifetime.
Aggregation
Aggregation is a specialized association representing a weak whole-part relationship. The part can exist without the whole and can, in principle, be shared by more than one whole.
Team ◇───── PlayerThe hollow diamond is attached to Team, the whole.
class Team {
public:
explicit Team(std::vector<Player*> players)
: players_(std::move(players)) {}
private:
std::vector<Player*> players_; // Non-owning pointers.
};The Player objects are supplied from outside. Deleting a Team does not conceptually delete its players, and a player may move to another team.
Aggregation is often overused. Its semantics are less precise than composition, and many teams omit it in favor of a normal association plus multiplicities and explanatory role names. Use aggregation only when the weak whole-part meaning adds useful information.
Composition
Composition is a strong whole-part relationship. A part belongs to at most one composite at a time, and its lifecycle is normally controlled by that composite.
House ◆───── RoomThe filled diamond is attached to House, the whole.
class House {
public:
explicit House(std::size_t roomCount)
: rooms_(roomCount) {}
private:
std::vector<Room> rooms_;
};The rooms are created as parts of the house, are not shared between houses, and do not have a meaningful independent lifecycle in this model. If the house is destroyed, its rooms cease to be part of the model as well.
Composition is a modeling concept rather than a direct statement about memory management. Garbage collection, database cascading, and C++ destructors may implement the lifecycle rule, but none of them alone proves that the conceptual relationship is composition.
Generalization
Generalization represents an is-a relationship. A specialized class inherits the structure and behavior of a more general class and should be substitutable for it.
SavingsAccount ─────▷ Accountclass Account {
public:
Money balance;
};
class SavingsAccount : public Account {
public:
InterestRate rate;
};The hollow triangle points to the general type, Account.
Generalization expresses more than code reuse. It promises that code expecting the parent can correctly use an instance of the child. If this substitutability does not hold, composition or delegation is usually a better design than inheritance.
Realization
Realization means that a classifier implements a contract described by another classifier, most commonly an interface.
StripePaymentGateway - - -▷ PaymentGatewayclass PaymentGateway {
public:
virtual ~PaymentGateway() = default;
virtual Receipt charge(Money amount) = 0;
};
class StripePaymentGateway : public PaymentGateway {
public:
Receipt charge(Money amount) override {
// Call the payment provider.
}
};The dashed line distinguishes realization from class inheritance. As with generalization, the hollow triangle points toward the more abstract element.
Association, Aggregation, and Composition
These three relationships are frequently confused because aggregation and composition are specialized forms of association.
| Question | Association | Aggregation | Composition |
|---|---|---|---|
| Is it structural? | Yes | Yes | Yes |
| Is it explicitly whole-part? | Not necessarily | Yes | Yes |
| Can the part exist independently? | Not specified | Yes | Usually no, within the model |
| Can the part be shared? | Not specified | Yes | No |
| Does the whole control the part’s lifecycle? | Not specified | No | Yes |
| Diamond | None | Hollow | Filled |
Consider a Car model:
Carassociated withDriver: the car knows its current driver, but neither owns the other.CaraggregatingPassenger: the passengers can exist independently and can leave the car.Carcomposed ofEnginePart: if the model treats an engine part as an exclusive component whose lifecycle belongs to that car, composition may be appropriate.
The correct relationship depends on the domain and the boundary of the model, not on the nouns alone. A Room may be composed into a House in a building-design model but exist independently in a property-management database.
Dependency vs. Association
The practical distinction is whether the relationship is temporary or retained:
class CheckoutService {
private:
// Association: retained as object state.
OrderRepository& orders_;
public:
// Dependency: used only during this operation.
Receipt checkout(PaymentMethod& paymentMethod) {
return paymentMethod.pay(/* ... */);
}
};CheckoutService is associated with OrderRepository and depends on PaymentMethod. An association also creates a dependency at a broader semantic level, but UML uses the solid association when the structural connection is what matters.
Generalization vs. Realization
Both relationships point from a concrete or specialized type toward an abstraction:
- Generalization inherits from an existing classifier, often including implementation and state.
- Realization fulfills a specification or contract, normally without inheriting its implementation state.
In C++, public inheritance from a concrete base class usually maps to generalization. Public inheritance from an abstract base class with pure virtual functions usually maps to realization. Language syntax is a useful clue, but the UML model should primarily communicate design intent.
Choosing a Relationship
Use these questions in order:
- Is one type a substitutable subtype of another? Use generalization.
- Does a type implement a contract? Use realization.
- Does one object retain a reference to another? Use association.
- Is that retained object an exclusive part whose lifecycle belongs to the whole? Use composition.
- Is it a non-exclusive, independently living part and is the whole-part distinction useful? Use aggregation.
- Does one element only use another temporarily? Use dependency.
Use the weakest relationship that accurately communicates the design. Stronger relationships introduce stronger promises about structure, substitutability, ownership, or lifecycle.
Common Mistakes
- Putting an aggregation or composition diamond at the part end rather than the whole end
- Treating every field as composition even when the referenced object is externally owned
- Choosing aggregation solely because a class contains a collection
- Assuming deletion in a database automatically proves composition
- Using inheritance for code reuse when the subtype is not substitutable for the parent
- Drawing every dependency and obscuring the important architectural relationships
- Omitting multiplicities when cardinality is important to the domain
References
- Object Management Group. OMG Unified Modeling Language (OMG UML), Version 2.5.1. 2017.
- Martin Fowler. UML Distilled: A Brief Guide to the Standard Object Modeling Language. 3rd ed., Addison-Wesley, 2003.
zhuang@linux:~/notes/software-design/uml-classes-relationships/$ comments