UML
What is UML?
UML, short for Unified Modeling Language, is a notation for describing the structure and behavior of software. A diagram can show relationships between types, messages exchanged by objects, or the conditions under which a state changes.1
UML is neither a programming language nor a development methodology. It is also separate from the tool used to draw it. You can express the same UML relationship by hand, in a diagram editor, or with a text-based drawing tool.
Why use it?
Consider a small diagnostic reporter. The error data stays the same, but producing a readable sentence and producing JSON require different formatting. A formatter is the component that converts data into one of these representations.
If the reporter knows every formatting rule, each new format requires a change to the reporter. Instead, the reporter can depend on a shared contract and delegate the conversion to a supplied formatter. Here, the contract is a calling rule: accept a diagnostic and return a string.
A UML diagram lets us inspect who references whom and what must change when a new format is added, before reading all the code. It also gives reviewers a shared picture to discuss. A correct diagram, however, does not establish that its implementation is correct.
Choose a diagram for the question
Question | Useful diagram |
|---|---|
What are the types, their responsibilities, and their relationships? | Class diagram |
Who calls whom, and in what order, in one scenario? | Sequence diagram |
Which events are allowed in a state, and what changes next? | State machine diagram |
Where does a workflow branch, repeat, or run in parallel? | Activity diagram |
How do larger software components connect? | Component diagram |
Where are artifacts deployed across devices and execution environments? | Deployment diagram |
- Question
What are the types, their responsibilities, and their relationships?
- Useful diagram
Class diagram
- Question
Who calls whom, and in what order, in one scenario?
- Useful diagram
Sequence diagram
- Question
Which events are allowed in a state, and what changes next?
- Useful diagram
State machine diagram
- Question
Where does a workflow branch, repeat, or run in parallel?
- Useful diagram
Activity diagram
- Question
How do larger software components connect?
- Useful diagram
Component diagram
- Question
Where are artifacts deployed across devices and execution environments?
- Useful diagram
Deployment diagram
This article covers class, sequence, and state machine diagrams, which appear often in pattern explanations. Start with the meaning of a box and its connecting lines.
Read a class box from top to bottom
DiagnosticReporteris the type name.- formatter: IDiagnosticFormatteris a private property named formatter. Its type follows the colon.+ report(d: Diagnostic): Stringis a public operation that accepts d of type Diagnostic and returns a string. An implementation usually represents it as a method.
Before a member, + means public, - private, # protected, and ~ package visibility. A UML package groups elements in a namespace. Do not automatically equate it with a directory or a particular language's internal visibility.
The keyword «interface» identifies an interface. Abstract types and operations may be italicized; static features are underlined. Diagrams often omit compartments or individual members, so an item missing from a diagram is not necessarily absent from the code.2
Read the line and its endpoint together
Generalization: a solid line with a hollow triangle points from a more specific type to a more general type. Class inheritance is a common example.
Realization: a dashed line with a hollow triangle points from an implementation to its contract. It can show that a class implements an interface.
Dependency: a dashed line with an open arrow points from a client to the supplier it needs. Using a type as a method parameter is one example.
Association: a solid line represents a relationship between instances. An open arrowhead marks a navigable end. Storing an object reference in a field is one way to implement such a relationship.
The triangle points to the general type or contract. Thinking of extends or implements helps with the direction. An association without arrowheads does not, by itself, establish bidirectional navigation: the diagram may simply omit navigability.
A hollow diamond marks shared aggregation; a filled diamond marks composition. Both place the diamond at the whole end. Shared aggregation identifies a whole-part relationship, but its precise ownership and lifetime rules must be defined by the model.3
In a composition, a part belongs to at most one composite whole at a time. Deleting the whole also deletes its parts in the model, though a part may be detached beforehand and survive. The symbol does not itself implement memory cleanup or a cascading database delete.
Read the formatter example as a whole
The dashed line and hollow triangle from
HumanFormattertoIDiagnosticFormattershow that the readable-text formatter implements the shared contract.The solid line from
DiagnosticReportertoIDiagnosticFormattershows that the reporter references its formatter. The role name formatter identifies that property.The dashed arrow from
DiagnosticReportertoDiagnosticshows that report uses the diagnostic passed to it. It does not assert that the reporter stores it in a field.
Numbers at the ends of an association are multiplicities. For each reporter, 1 at the formatter end means exactly one formatter. In the other direction, 0..* means that zero or more reporters may reference a formatter. Multiple reporters are allowed to share the same formatter.
0..1 means zero or one; 1..* means one or more. Read the number as the permitted count at this end for one instance at the opposite end. If multiplicity is omitted from an association diagram, do not infer that it is one.4
The same structure in code
These two programs have the same structure and output. The diagram omits constructors and shows the formatter property on the association. String denotes a string type; {readOnly} marks a property that cannot be directly changed after initialization. Operation names follow TypeScript camelCase; the C# code uses Format, Report, and Message according to its naming conventions.
using System;
// Keep the data separate from its display format.
public sealed record Diagnostic(string Message);
// This is the contract that a reporter can call.
public interface IDiagnosticFormatter
{
string Format(Diagnostic d);
}
public sealed class HumanFormatter : IDiagnosticFormatter
{
public string Format(Diagnostic d) => "Error: " + d.Message;
}
public sealed class DiagnosticReporter
{
private readonly IDiagnosticFormatter formatter;
// Store the supplied formatter instead of constructing one here.
public DiagnosticReporter(IDiagnosticFormatter formatter)
{
this.formatter = formatter;
}
// Delegate formatting and return the resulting text.
public string Report(Diagnostic d) => formatter.Format(d);
}
public static class Program
{
public static void Main()
{
var reporter = new DiagnosticReporter(new HumanFormatter());
Console.WriteLine(reporter.Report(new Diagnostic("Name not found.")));
}
}// Keep the data separate from its display format.
class Diagnostic {
constructor(public readonly message: string) {}
}
// This is the contract that a reporter can call.
interface IDiagnosticFormatter {
format(d: Diagnostic): string;
}
class HumanFormatter implements IDiagnosticFormatter {
format(d: Diagnostic): string {
return "Error: " + d.message;
}
}
class DiagnosticReporter {
// Store the supplied formatter instead of constructing one here.
constructor(private readonly formatter: IDiagnosticFormatter) {}
// Delegate formatting and return the resulting text.
report(d: Diagnostic): string {
return this.formatter.format(d);
}
}
const reporter = new DiagnosticReporter(new HumanFormatter());
console.log(reporter.report(new Diagnostic("Name not found.")));Both programs print:
Error: Name not found.The reporter does not construct a formatter. It stores the one supplied to its constructor and delegates conversion to it. A new formatting class can be added without changing Report or report. The Strategy pattern develops this separation in more detail.
A sequence diagram follows one execution
Now draw one call to report in the same example. The class diagram showed relationships between types. This diagram shows calls and replies between the participating objects.
The boxes at the top name participants.
reporter: DiagnosticReporteridentifies an object named reporter of type DiagnosticReporter.The dashed vertical line is a lifeline, which represents a participant in this interaction.
Here, a solid line with a filled arrowhead is a synchronous call: its caller waits for it to return. A dashed line with an open arrowhead is a reply.
A narrow rectangle on a lifeline marks an execution. The call to human occurs within the time that reporter is handling its own call.
For this example, follow the calls and replies from top to bottom. Independent events on different lifelines do not necessarily have one total order. Without an explicit timing notation, vertical distance does not measure elapsed time.5
A state machine diagram shows allowed changes
Use a document review as a separate example. Suppose a draft may be submitted, and a document under review may be approved or rejected. The same document permits different requests depending on its current state.
The small filled circle marks the initial entry. This document starts in Draft.
Rounded boxes represent states: Draft, InReview, and Published.
An arrow is a transition. submit moves Draft to InReview; approve moves InReview to Published; reject(reason) returns it to Draft.
A transition may be labeled event [guard] / effect. For example, submit [contentNotEmpty] / lockEditing() means that a submit event moves the document to the next state only if the content is nonempty, locking editing as an effect. Omit guards or effects when they are not needed.6
A state diagram describes behavior rules. The State pattern is a way to organize code by placing state-specific behavior in objects. A state diagram does not require that pattern: a small model may be implemented with an enum and conditionals.
When drawing your own
Choose one question first. Who references whom and who calls whom in what order need different views.
Include only the types or objects needed to answer it. You do not need to list every constructor and helper method.
Check the meaning of solid lines, dashed lines, triangles, and diamonds, then add the necessary role names and multiplicities. Explain any nonstandard notation in a legend.
Compare the diagram with the code. Is a reference stored, is a type used only as a parameter, or is an interface implemented? Update explanatory diagrams when the code changes.
Example environment: C# was run on .NET 10; TypeScript was compiled for ES2022 and run with Node.js.
Related reading
Strategy pattern: interfaces and interchangeable implementations
State pattern: state-specific behavior and transitions
OMG UML 2.5.1: specification and version record
Footnotes
- OMG, What is UML?, purpose and scope. The detailed notation in this article follows the UML 2.5.1 specification. ↩
- OMG UML 2.5.1, §9.2.4 Classifier notation, §9.4.4 Feature notation, §9.5.4 Property notation. ↩
- OMG UML 2.5.1, §7.7.4 Dependencies, §9.2.4.2 Generalization, §9.5.3 AggregationKind, §11.5.4 Associations. ↩
- OMG UML 2.5.1, §7.5.4 Multiplicity notation, §11.5.4 Associations. Omission on an association diagram differs from defaults in textual property notation. ↩
- OMG UML 2.5.1, §17.1.3 Partial ordering, §17.3.4 Lifelines, §17.4.4.1 Messages. ↩
- OMG UML 2.5.1, §14.2.4 States, Pseudostates, Transitions. ↩