Loading learning content...
So far, we've focused on instance-level design: attributes and methods that belong to individual objects. Each Customer has its own name, each Order has its own items. But real OO systems need more than instance members.
Sometimes, data or behavior belongs to the class itself, not to any particular instance. A counter of all orders ever created, a factory method that returns new instances, a constant that all instances share—these are static members.
Other times, we need to specify what must exist without specifying how it works. An abstract method defines a contract: "Any subclass must implement this." An abstract class cannot be instantiated directly—it exists to be extended.
This page covers the UML notation for both concepts, equipping you to model complete, production-ready class hierarchies.
By the end of this page, you will understand how to notate static members in UML, represent abstract classes and methods, distinguish when to use each, and model realistic class hierarchies that include factories, singletons, and template methods.
Static members belong to the class itself, not to instances of the class. There's exactly one copy of a static member, shared across all instances (or accessible even with no instances).
The conceptual difference:
| Aspect | Instance Member | Static Member |
|---|---|---|
| Belongs to | Each object | The class itself |
| Copies exist | One per instance | Exactly one, shared |
| Access requires | An instance reference | Just the class name |
| Can access | Instance and static members | Only static members |
| Memory lifetime | Lives with the object | Lives with the class |
Common uses for static members:
Math.PI, Integer.MAX_VALUE.Order.createFromCart(cart).DatabaseConnection.getInstance().StringUtils.isEmpty(str).Static members create global state, which complicates testing and can introduce hidden dependencies. Use them judiciously. Prefer instance members when there's no compelling reason for static.
In UML, static members are indicated by underlining the member name. This visual distinction immediately signals "belongs to the class, not instances."
┌─────────────────────────────────────────────────────┐
│ Order │
├─────────────────────────────────────────────────────┤
│ - id: UUID │
│ - customer: Customer │
│ - status: OrderStatus = PENDING │
│ - totalCount: Integer ← STATIC │
│ ̲-̲ ̲M̲A̲X̲_̲I̲T̲E̲M̲S̲:̲ ̲I̲n̲t̲e̲g̲e̲r̲ ̲=̲ ̲1̲0̲0̲ ← STATIC │
├─────────────────────────────────────────────────────┤
│ + submit(): Boolean │
│ + cancel(): Boolean │
│ + ̲g̲e̲t̲T̲o̲t̲a̲l̲C̲o̲u̲n̲t̲(̲)̲:̲ ̲I̲n̲t̲e̲g̲e̲r̲ ← STATIC │
│ + ̲c̲r̲e̲a̲t̲e̲F̲r̲o̲m̲C̲a̲r̲t̲(̲c̲a̲r̲t̲:̲ ̲C̲a̲r̲t̲)̲:̲ ̲O̲r̲d̲e̲r̲ ← STATIC │
└─────────────────────────────────────────────────────┘
Note: In ASCII representations like the above, underlining is difficult to show. In actual UML tools, static members appear with a clear underline:
- totalCount: Integer appears as - t̲o̲t̲a̲l̲C̲o̲u̲n̲t̲:̲ ̲I̲n̲t̲e̲g̲e̲r̲
Textual alternatives when underlining isn't available:
When diagrams are drawn in text (e.g., in documentation or chat), common conventions include:
- _totalCount: Integer- totalCount: Integer {static}- MAX_ITEMS: Integer| Notation Style | Example | When to Use |
|---|---|---|
| Underline (standard) | ̲g̲e̲t̲I̲n̲s̲t̲a̲n̲c̲e̲(̲)̲ | Formal UML diagrams, tools that support it |
| {static} property | getInstance() {static} | When underlining isn't available |
| Underscore prefix | _getInstance() | ASCII diagrams, informal documentation |
| «static» stereotype | «static» getInstance() | Rare; more explicit but verbose |
Most UML diagramming tools (Enterprise Architect, Lucidchart, PlantUML) use underlining for static members. When you see an underlined member, immediately think: "class-level, not instance-level."
Let's examine how static members appear in common design patterns:
Pattern 1: Singleton
The Singleton pattern ensures only one instance of a class exists. Static members are essential:
┌─────────────────────────────────────────────────────┐
│ DatabasePool │
├─────────────────────────────────────────────────────┤
│ - ̲i̲n̲s̲t̲a̲n̲c̲e̲:̲ ̲D̲a̲t̲a̲b̲a̲s̲e̲P̲o̲o̲l̲ │
│ - connections: List<Connection> │
│ - maxConnections: Integer │
├─────────────────────────────────────────────────────┤
│ - DatabasePool() │
│ + ̲g̲e̲t̲I̲n̲s̲t̲a̲n̲c̲e̲(̲)̲:̲ ̲D̲a̲t̲a̲b̲a̲s̲e̲P̲o̲o̲l̲ │
│ + getConnection(): Connection │
│ + releaseConnection(conn: Connection): void │
└─────────────────────────────────────────────────────┘
instance (static): Holds the one-and-only instancegetInstance() (static): Returns that instance (creating it if needed)Pattern 2: Factory Method
Factory methods are static methods that create instances with encapsulated logic:
┌─────────────────────────────────────────────────────┐
│ User │
├─────────────────────────────────────────────────────┤
│ - id: UUID │
│ - email: String │
│ - passwordHash: String │
│ - role: UserRole │
├─────────────────────────────────────────────────────┤
│ - User(id, email, hash, role) │
│ + ̲c̲r̲e̲a̲t̲e̲(̲e̲m̲a̲i̲l̲,̲ ̲p̲a̲s̲s̲w̲o̲r̲d̲)̲:̲ ̲U̲s̲e̲r̲ │
│ + ̲c̲r̲e̲a̲t̲e̲A̲d̲m̲i̲n̲(̲e̲m̲a̲i̲l̲,̲ ̲p̲a̲s̲s̲w̲o̲r̲d̲)̲:̲ ̲U̲s̲e̲r̲ │
│ + ̲f̲r̲o̲m̲O̲A̲u̲t̲h̲(̲p̲r̲o̲v̲i̲d̲e̲r̲,̲ ̲t̲o̲k̲e̲n̲)̲:̲ ̲U̲s̲e̲r̲ │
│ + getEmail(): String │
│ + checkPassword(password: String): Boolean │
└─────────────────────────────────────────────────────┘
create(), createAdmin(), fromOAuth() (static): Different creation pathsPattern 3: Utility/Helper Class
Some classes contain only static methods—they're collections of related utilities:
┌─────────────────────────────────────────────────────┐
│ «utility» │
│ StringUtils │
├─────────────────────────────────────────────────────┤
│ │
├─────────────────────────────────────────────────────┤
│ + ̲i̲s̲E̲m̲p̲t̲y̲(̲s̲:̲ ̲S̲t̲r̲i̲n̲g̲)̲:̲ ̲B̲o̲o̲l̲e̲a̲n̲ │
│ + ̲i̲s̲B̲l̲a̲n̲k̲(̲s̲:̲ ̲S̲t̲r̲i̲n̲g̲)̲:̲ ̲B̲o̲o̲l̲e̲a̲n̲ │
│ + ̲t̲r̲u̲n̲c̲a̲t̲e̲(̲s̲:̲ ̲S̲t̲r̲i̲n̲g̲,̲ ̲l̲e̲n̲:̲ ̲I̲n̲t̲)̲:̲ ̲S̲t̲r̲i̲n̲g̲ │
│ + ̲c̲a̲p̲i̲t̲a̲l̲i̲z̲e̲(̲s̲:̲ ̲S̲t̲r̲i̲n̲g̲)̲:̲ ̲S̲t̲r̲i̲n̲g̲ │
│ + ̲t̲o̲S̲l̲u̲g̲(̲s̲:̲ ̲S̲t̲r̲i̲n̲g̲)̲:̲ ̲S̲t̲r̲i̲n̲g̲ │
└─────────────────────────────────────────────────────┘
Note:
«utility» stereotype indicates this class is never instantiatedStatic constants (final/readonly static attributes) are typically named in ALL_CAPS: MAX_CONNECTIONS, DEFAULT_TIMEOUT, PI. This convention comes from C and is nearly universal in OO languages. In UML, they're both underlined AND read-only.
Abstract elements define contracts without implementation. They specify what must exist without specifying how it works.
Abstract classes:
new AbstractClass()Abstract methods:
Shape with Circle, Rectangle, Triangle.| Aspect | Abstract | Concrete |
|---|---|---|
| Class instantiation | Cannot create instances directly | Can create instances with new |
| Method implementation | May have no body | Must have a body |
| Must be extended? | Yes, to be useful | No, can be used directly |
| Can have state? | Yes, attributes allowed | Yes, attributes allowed |
| Can have concrete methods? | Yes, can mix abstract and concrete | Yes, all methods are concrete |
In UML, abstract elements are indicated by italics or by the «abstract» stereotype. Both are valid; italics are more compact but stereotypes are more explicit.
Abstract class notation:
┌─────────────────────────────────────────────────────┐
│ «abstract» │
│ Shape │
│ or │
│ 𝘚𝘩𝘢𝘱𝘦 (italics) │
├─────────────────────────────────────────────────────┤
│ # x: Integer │
│ # y: Integer │
│ # color: Color │
├─────────────────────────────────────────────────────┤
│ + 𝘨𝘦𝘵𝘈𝘳𝘦𝘢(): Double {abstract} │
│ + 𝘨𝘦𝘵𝘗𝘦𝘳𝘪𝘮𝘦𝘵𝘦𝘳(): Double {abstract} │
│ + move(dx: Integer, dy: Integer): void │
│ + setColor(color: Color): void │
└─────────────────────────────────────────────────────┘
Key observations:
«abstract» stereotype (both indicate abstract class)getArea() and getPerimeter() are abstract (italicized, marked {abstract})move() and setColor() are concrete—implemented in Shape, inherited by subclasses#)—subclasses can access themnew Shape()—only concrete subclasses like Circle can be instantiatedComplete hierarchy example:
┌──────────────────────┐
│ «abstract» │
│ Shape │
├──────────────────────┤
│ # x: Integer │
│ # y: Integer │
├──────────────────────┤
│ + getArea() │
│ + getPerimeter() │
│ + move() │
└──────────────────────┘
△
│
┌───────────┴───────────┐
│ │
┌──────────────────┐ ┌──────────────────┐
│ Circle │ │ Rectangle │
├──────────────────┤ ├──────────────────┤
│ - radius: Double│ │ - width: Double │
│ │ │ - height: Double│
├──────────────────┤ ├──────────────────┤
│ + getArea() │ │ + getArea() │
│ + getPerimeter()│ │ + getPerimeter()│
│ + getRadius() │ │ + getDiagonal() │
└──────────────────┘ └──────────────────┘
Shape is abstract (cannot instantiate)Circle and Rectangle are concrete (can instantiate)getArea() and getPerimeter()move() from ShapegetRadius(), getDiagonal())| Notation Style | Example | When to Use |
|---|---|---|
| Italics (standard) | 𝘚𝘩𝘢𝘱𝘦, 𝘨𝘦𝘵𝘈𝘳𝘦𝘢() | Formal diagrams, tools that support formatting |
| {abstract} property | getArea() {abstract} | Methods in text, when italics unavailable |
| «abstract» stereotype | «abstract» Shape | Classes, especially when italics unavailable |
| Abstract compartment | Separate section listing abstract ops | Rare; for very explicit documentation |
When drawing class hierarchies, place abstract classes at the top. Concrete classes extend downward. This follows the intuitive reading direction and matches inheritance direction (subclasses below parents).
Abstract classes appear in many important design patterns. Let's examine key examples:
Pattern 1: Template Method
The Template Method pattern defines an algorithm skeleton, with abstract methods as "hooks" for subclasses:
┌─────────────────────────────────────────────────────┐
│ «abstract» │
│ DataExporter │
├─────────────────────────────────────────────────────┤
│ # data: List<Record> │
├─────────────────────────────────────────────────────┤
│ + export(): Bytes // concrete │
│ # 𝘧𝘰𝘳𝘮𝘢𝘵𝘏𝘦𝘢𝘥𝘦𝘳(): String // abstract │
│ # 𝘧𝘰𝘳𝘮𝘢𝘵𝘙𝘦𝘤𝘰𝘳𝘥(record: Record): String // abstract │
│ # 𝘧𝘰𝘳𝘮𝘢𝘵𝘍𝘰𝘰𝘵𝘦𝘳(): String // abstract │
│ # prepareData(): List<Record> // concrete │
└─────────────────────────────────────────────────────┘
△
│
┌────┴────┐
│ │
┌─────────┐ ┌─────────┐
│CsvExport│ │JsonExport│
└─────────┘ └─────────┘
export() is the template method: it calls the abstract hooks in sequenceformatHeader(), formatRecord(), formatFooter() are abstract hooksCsvExporter and JsonExporter implement these hooks differentlyPattern 2: Abstract Factory (partial)
┌─────────────────────────────────────────────────────┐
│ «abstract» │
│ UIWidgetFactory │
├─────────────────────────────────────────────────────┤
├─────────────────────────────────────────────────────┤
│ + 𝘤𝘳𝘦𝘢𝘵𝘦𝘉𝘶𝘵𝘵𝘰𝘯(): Button │
│ + 𝘤𝘳𝘦𝘢𝘵𝘦𝘛𝘦𝘹𝘵𝘍𝘪𝘦𝘭𝘥(): TextField │
│ + 𝘤𝘳𝘦𝘢𝘵𝘦𝘊𝘩𝘦𝘤𝘬𝘣𝘰𝘹(): Checkbox │
└─────────────────────────────────────────────────────┘
△
│
┌────┴────┐
│ │
┌────────────────┐ ┌────────────────┐
│ WindowsFactory │ │ MacOSFactory │
└────────────────┘ └────────────────┘
UIWidgetFactory defines what widgets can be createdWindowsFactory and MacOSFactory define how to create platform-specific versionsPattern 3: Domain Entity Hierarchies
┌─────────────────────────────────────────────────────┐
│ «abstract» │
│ Payment │
├─────────────────────────────────────────────────────┤
│ # id: UUID │
│ # amount: Money │
│ # timestamp: DateTime │
│ # status: PaymentStatus │
├─────────────────────────────────────────────────────┤
│ + 𝘱𝘳𝘰𝘤𝘦𝘴𝘴(): PaymentResult │
│ + 𝘳𝘦𝘧𝘶𝘯𝘥(): RefundResult │
│ + 𝘨𝘦𝘵𝘍𝘦𝘦𝘴(): Money │
│ + getAmount(): Money │
│ + getStatus(): PaymentStatus │
└─────────────────────────────────────────────────────┘
△
│
┌─────────┼─────────┐
│ │ │
┌────────┐ ┌────────┐ ┌──────────┐
│CreditCard│ │PayPal │ │BankTrans │
│Payment │ │Payment │ │Payment │
└────────┘ └────────┘ └──────────┘
Each payment type has different processing requirements, fee structures, and refund mechanisms. The abstract Payment class ensures all payment types conform to a common interface while allowing each to implement specifics.
Interfaces (marked «interface») define contracts without any implementation. Abstract classes can provide partial implementation. Use interfaces when you need multiple inheritance of contracts, abstract classes when you want to share code.
Static and abstract can appear together in meaningful ways. Here are important combinations and rules:
What's allowed:
What's NOT allowed:
Why can't a method be both abstract and static? Static methods belong to the class, not instances. They're resolved at compile time based on the type reference, not at runtime based on the actual object. Abstract methods are meant to be overridden and resolved at runtime based on the actual subclass. These two concepts are fundamentally incompatible.
Example of a well-designed combination:
┌─────────────────────────────────────────────────────┐
│ «abstract» │
│ Document │
├─────────────────────────────────────────────────────┤
│ - ̲s̲u̲p̲p̲o̲r̲t̲e̲d̲F̲o̲r̲m̲a̲t̲s̲:̲ ̲S̲t̲r̲i̲n̲g̲[̲*̲]̲ // static array │
│ # content: String │
│ # metadata: Map<String, String> │
├─────────────────────────────────────────────────────┤
│ + ̲g̲e̲t̲S̲u̲p̲p̲o̲r̲t̲e̲d̲F̲o̲r̲m̲a̲t̲s̲(̲)̲:̲ ̲S̲t̲r̲i̲n̲g̲[̲*̲]̲ // static │
│ + ̲p̲a̲r̲s̲e̲(̲d̲a̲t̲a̲:̲ ̲B̲y̲t̲e̲s̲)̲:̲ ̲D̲o̲c̲u̲m̲e̲n̲t̲ // static factory│
│ + 𝘳𝘦𝘯𝘥𝘦𝘳(): String // abstract │
│ + 𝘷𝘢𝘭𝘪𝘥𝘢𝘵𝘦(): Boolean // abstract │
│ + getMetadata(): Map<String, String> // concrete │
└─────────────────────────────────────────────────────┘
This pattern combines:
supportedFormats): Shared data about supported typesparse()): Creates appropriate Document subclassrender(), validate()): Subclass-specific behaviorgetMetadata()): Shared implementationThe static factory might return different Document subclasses based on the input format, with each implementing render() and validate() appropriately.
A static factory in an abstract class is a powerful pattern but creates a dependency from parent to children. The factory must know about all concrete subclasses. This is fine for closed hierarchies but problematic if you need extensibility.
Let's see how static and abstract notation translates to actual code:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
// Abstract class with static membersabstract class Shape { // Static constant static readonly MAX_SIZE: number = 10000; // Static counter private static instanceCount: number = 0; // Protected instance attributes protected x: number; protected y: number; protected color: string; constructor(x: number, y: number, color: string = 'black') { this.x = x; this.y = y; this.color = color; Shape.instanceCount++; } // Abstract methods - no implementation abstract getArea(): number; abstract getPerimeter(): number; // Concrete method - has implementation public move(dx: number, dy: number): void { this.x += dx; this.y += dy; } // Static method public static getInstanceCount(): number { return Shape.instanceCount; }} // Concrete subclassclass Circle extends Shape { private radius: number; constructor(x: number, y: number, radius: number, color?: string) { super(x, y, color); this.radius = radius; } // Must implement abstract methods getArea(): number { return Math.PI * this.radius * this.radius; } getPerimeter(): number { return 2 * Math.PI * this.radius; } // Circle-specific method getRadius(): number { return this.radius; }} // Usage// const shape = new Shape(0, 0); // ERROR: Cannot instantiate abstract classconst circle = new Circle(0, 0, 5, 'red');console.log(circle.getArea()); // 78.54...console.log(Shape.getInstanceCount()); // 1console.log(Shape.MAX_SIZE); // 10000We've covered two essential extensions to basic class notation. Let's consolidate:
{static} property when underlining isn't available.«abstract» stereotype.What's next:
With single class notation complete, we'll move to reading and creating complete class diagrams. This means arranging multiple classes, showing their relationships, and creating diagrams that communicate entire system structures at a glance.
You now have full command of individual class notation: structure, visibility, static, and abstract elements. In the next page, we'll assemble these into complete, interconnected class diagrams.