1. What a Class Diagram Shows
A class diagram is the workhorse of structural UML. It shows the types a system
defines - their data, their operations, and the relationships between them. Where a
package diagram shows compilation boundaries, a class diagram shows what lives inside
those boundaries: which types carry which state, what interface each type exposes, and
how types depend on, own, or extend each other.
Class diagrams are language-independent in intent but language-specific in detail.
A "class" in the diagram maps to a struct, a class, a trait, an interface, or an
abstract base depending on the language. The diagram does not care which - it expresses
the design contract; the language expresses the mechanism.
2. The Class Box
Each type appears as a rectangle with up to three compartments stacked vertically.
The top compartment holds the type name, optionally preceded by a stereotype. The
middle compartment lists attributes (fields). The bottom compartment lists operations
(methods). Either or both of the lower compartments may be omitted when the detail
is not relevant to the diagram's purpose.
Visibility prefixes on members follow a consistent convention across all languages:
| Prefix | Visibility | Meaning |
+ |
public |
Accessible to any caller. |
- |
private |
Accessible only within the type itself. |
# |
protected |
Accessible within the type and its subclasses. |
~ |
package |
Accessible within the enclosing package (Java/C# internal). |
Stereotypes label the kind of type: <<interface>>,
<<abstract>>, <<trait>>,
<<enum>>. An abstract operation is shown in italics;
Mermaid approximates this with an asterisk suffix.
3. Relationships
Relationships are the most important part of a class diagram - the boxes alone say
little. Each relationship type carries a distinct meaning about how one type depends
on or contains another.
classDiagram
direction LR
class Owner
class Part
class Whole
class Member
class IFace {
<<interface>>
}
class Concrete
class User
class Target
class Child
class Parent
Owner --* Part : composition - Owner owns Part lifetime
Whole --o Member : aggregation - Member outlives Whole
Concrete ..|> IFace : realization - Concrete implements IFace
Child --|> Parent : inheritance - Child is-a Parent
User --> Target : association - User holds a reference to Target
Figure 1. The five principal relationship types.
Arrow direction always reads "source depends on target."
| Relationship | Line | Meaning |
| Composition |
solid, filled diamond at owner |
Owner controls the lifetime of the part. The part cannot exist
independently - when the owner is destroyed, the part is destroyed with it.
Use for data members owned by value or by unique pointer.
|
| Aggregation |
solid, hollow diamond at whole |
A "has-a" relationship where the member has an independent lifetime.
The whole holds a reference or shared pointer; destruction of the whole
does not destroy the member.
|
| Realization |
dashed, hollow triangle at interface |
A type fulfills the contract declared by an interface or trait.
All abstract operations in the interface must be provided by the concrete type.
|
| Inheritance |
solid, hollow triangle at parent |
The child type is substitutable for the parent. It inherits data and
operations, and may override virtual ones. Absent in Rust; present in
C++, C#, and Python.
|
| Association |
solid arrow |
One type holds a reference to another without owning its lifetime.
The most general structural link - use it when none of the stronger
relationships apply.
|
| Dependency |
dashed arrow |
One type uses another transiently - as a function parameter, a local
variable, or a return value - but does not store a reference to it.
The weakest structural link.
|
4. TextFinder as a Worked Example
The Rust TextFinder implementation provides a clean class diagram subject because
it uses a trait (DirEvent) to decouple directory traversal from the
application logic. The five types, their members, and their relationships are
shown below. The C++, C#, and Python implementations express the same design with
language-specific mechanisms.
classDiagram
class DirEvent {
<<trait>>
+do_dir(d: &str)*
+do_file(f: &str)*
}
class TfAppl {
-curr_dir: String
-hide: bool
-tf: TextFinder
+new(cl: &CmdLine) TfAppl
+do_dir(d: &str)
+do_file(f: &str)
+get_hide() bool
}
class DirNav {
-skip_dirs: Vec~String~
-patterns: Vec~String~
+new() DirNav
+add_pattern(p: &str)
+search(path: &str, app: &mut dyn DirEvent)
}
class TextFinder {
-regex: Option~Regex~
-last_path: String
+new() TextFinder
+regex(pattern: &str)
+find(fqf: &str) bool
+get_last_path() ~&str~
+last_path(p: &str)
}
class CmdLine {
-options: HashMap~String, Vec~String~~
+new() CmdLine
+parse(args: Vec~String~)
+get(key: &str) Option~&str~
+get_all(key: &str) Vec~&str~
}
DirEvent <|.. TfAppl : implements
DirNav --> DirEvent : calls via trait object
TfAppl *-- TextFinder : owns
TfAppl --> CmdLine : reads options from
Figure 2. Rust TextFinder class diagram.
TfAppl is the only type that connects all four others - it implements
the DirEvent trait, owns a TextFinder, and reads from
CmdLine. DirNav never sees the concrete type, only the trait.
5. Reading the Diagram
Start at the relationships, not the boxes. Four arrows leave or arrive at
TfAppl:
-
TfAppl ..|> DirEvent - realization. TfAppl provides both abstract
operations declared by the trait. DirNav calls through the trait object;
it never names TfAppl.
-
TfAppl --* TextFinder - composition. TextFinder is a data member;
its lifetime is bound to TfAppl. When TfAppl is dropped, TextFinder is dropped.
-
TfAppl --> CmdLine - association. TfAppl holds a reference to a
CmdLine that was parsed by EntryPoint. TfAppl reads option values; it does not
own the CmdLine.
-
DirNav --> DirEvent - association through a trait object. DirNav's
search method accepts &mut dyn DirEvent, making it
generic over any type that satisfies the trait. The concrete type is invisible to
DirNav at compile time and resolved at the call site.
The class diagram does not show EntryPoint because it contains no persistent state -
it is a wiring function, not a type. That is a design choice: types with only
procedural logic often do not need class boxes.
6. Cross-Language Type Mapping
The class diagram is language-neutral. The same five-type structure appears in all
four TextFinder languages, with each language translating the trait pattern into its
own mechanism.
| Concept |
Rust |
C++ |
C# |
Python |
| Callback contract |
trait DirEvent |
abstract base class or std::function |
interface IDirEvent or Action<> |
callable protocol or ABC |
| Realization |
impl DirEvent for TfAppl |
override virtual methods |
class TfAppl : IDirEvent |
implement do_dir, do_file |
| Ownership of TextFinder |
value field in struct |
value member or unique_ptr |
field (reference type, GC-managed) |
instance attribute |
| Dispatch to TextFinder |
direct method call |
direct method call |
direct method call |
direct method call |
The realization relationship is the one that varies most. Rust requires an explicit
impl block; C++ uses virtual function override; C# uses interface
declaration syntax; Python relies on duck typing and optionally an abstract base class.
All four express the same class diagram relationship: a concrete type fulfills a
declared contract.
7. When to Draw One
Draw a class diagram when the question involves what types exist and how they
relate, not where they live or what order events occur in. Specific triggers:
-
Designing a new subsystem: sketch the types, their responsibilities, and the
relationships before writing code. Cycles and over-coupling are much cheaper
to fix on paper than in code.
-
Documenting a public API: show the types callers interact with, the interfaces
they depend on, and the ownership model. Omit private implementation types
to keep the diagram focused.
-
Cross-language comparison: the same class diagram can document parallel
implementations and make structural equivalences visible despite syntactic
differences.
-
Code review: a class diagram drawn from the actual code makes inheritance
tangles, god classes, and inappropriate intimacy immediately visible.
Avoid drawing every class in a large codebase. Class diagrams become unreadable past
about a dozen types. Show only the types relevant to the decision or question at hand,
and label the diagram accordingly.
8. References