Class diagrams form the cornerstone of UML and object-oriented design, providing the most detailed and widely-used view of system structure. As the foundation upon which all other UML diagrams build, mastering class diagrams is essential for any software engineer working with object-oriented systems. These diagrams capture not just what classes exist, but how they relate, inherit, and collaborate to create robust software architectures.
In this second part of our UML series, we’ll explore class diagrams in depth, from basic syntax to advanced design patterns, with practical examples you can immediately apply to your projects.
Anatomy of a UML Class
A UML class is represented by a rectangle divided into three compartments, each serving a specific purpose in defining the class structure:
classDiagram
class BankAccount {
-string accountNumber
-decimal balance
-Date createdDate
#string accountType
+string customerName
+deposit(amount: decimal) boolean
+withdraw(amount: decimal) boolean
+getBalance() decimal
-calculateInterest() decimal
#validateTransaction(amount: decimal) boolean
}Class Compartments Explained
- Name Compartment: Contains the class name, optionally with stereotypes like <<interface>> or <<abstract>>
- Attributes Compartment: Lists the class properties with visibility, type, and default values
- Operations Compartment: Defines the methods/functions the class provides
Visibility and Scope Modifiers
Understanding visibility is crucial for proper encapsulation design:
- + (Public): Accessible from anywhere
- – (Private): Only accessible within the same class
- # (Protected): Accessible within class hierarchy
- ~ (Package): Accessible within the same package/namespace
Class Relationships: The Heart of Object-Oriented Design
Class relationships define how objects interact and depend on each other. Understanding these relationships is crucial for creating maintainable, flexible designs.
Association Relationships
Associations represent general relationships between classes where objects of one class are connected to objects of another:
classDiagram
class Customer {
+string customerId
+string name
+string email
+placeOrder()
}
class Order {
+string orderId
+Date orderDate
+decimal totalAmount
+string status
+calculateTotal()
+ship()
}
class Product {
+string productId
+string name
+decimal price
+int stockQuantity
+updateStock()
}
Customer "1" --> "0..*" Order : places
Order "1" --> "1..*" Product : containsAggregation: “Has-A” Relationships
Aggregation represents a “whole-part” relationship where the part can exist independently of the whole. It’s shown with a hollow diamond:
classDiagram
class University {
+string name
+string address
+addDepartment()
+removeDepartment()
}
class Department {
+string departmentName
+string head
+int employeeCount
+addEmployee()
+getBudget()
}
class Professor {
+string professorId
+string name
+string specialization
+teach()
+research()
}
University o-- Department : contains
Department o-- Professor : employsComposition: Strong “Part-Of” Relationships
Composition represents a stronger “whole-part” relationship where parts cannot exist without the whole. It’s shown with a filled diamond:
classDiagram
class House {
+string address
+int squareFootage
+Date buildDate
+addRoom()
+calculateValue()
}
class Room {
+string roomType
+decimal length
+decimal width
+boolean hasWindow
+calculateArea()
}
class Door {
+string material
+decimal height
+decimal width
+boolean isLocked
+lock()
+unlock()
}
House *-- Room : contains
Room *-- Door : hasInheritance: “Is-A” Relationships
Inheritance represents generalization/specialization relationships where child classes inherit attributes and operations from parent classes:
classDiagram
class Vehicle {
#string licensePlate
#int year
#string make
#string model
+start()
+stop()
+getInfo()
}
class Car {
-int numberOfDoors
-string transmissionType
+openTrunk()
+lockDoors()
}
class Motorcycle {
-boolean hasSidecar
-string engineType
+wheelie()
+leanIntoTurn()
}
class Truck {
-decimal cargoCapacity
-int numberOfAxles
+loadCargo()
+unloadCargo()
}
Vehicle <|-- Car
Vehicle <|-- Motorcycle
Vehicle <|-- TruckDependency Relationships
Dependencies show that one class uses another class, typically as a parameter, local variable, or return type. These are shown with dashed arrows:
classDiagram
class PaymentProcessor {
+processPayment(order: Order, card: CreditCard) boolean
+refundPayment(transactionId: string) boolean
}
class Order {
+string orderId
+decimal amount
+Date orderDate
}
class CreditCard {
+string cardNumber
+string expiryDate
+string cvv
+boolean isValid()
}
class EmailService {
+sendConfirmation(email: string, order: Order)
+sendReceipt(email: string, transaction: Transaction)
}
PaymentProcessor ..> Order : uses
PaymentProcessor ..> CreditCard : uses
EmailService ..> Order : usesAdvanced Class Modeling Techniques
Abstract Classes and Interfaces
Abstract classes and interfaces define contracts that implementing classes must follow. Abstract classes are shown in italics or with <<abstract>> stereotype:
classDiagram
class Shape {
<>
#Point center
#string color
+getArea()* double
+getPerimeter()* double
+draw()
+setColor(color: string)
}
class Drawable {
<>
+render()
+hide()
+show()
}
class Circle {
-double radius
+getArea() double
+getPerimeter() double
}
class Rectangle {
-double width
-double height
+getArea() double
+getPerimeter() double
}
Shape <|-- Circle
Shape <|-- Rectangle
Drawable <|.. Circle
Drawable <|.. Rectangle Multiplicity and Role Names
Multiplicity specifies how many instances of one class relate to instances of another class:
- 1: Exactly one
- 0..1: Zero or one
- 0..*: Zero or more
- 1..*: One or more
- n..m: Between n and m
classDiagram
class Person {
+string name
+Date birthDate
+string socialSecurityNumber
}
class Company {
+string companyName
+string taxId
+string address
}
class Employment {
+Date startDate
+Date endDate
+decimal salary
+string position
+string department
}
Person "employee 0..*" --> "employer 0..*" Company : works for
Person "1" --> "0..*" Employment : has
Company "1" --> "0..*" Employment : offersDesign Patterns in Class Diagrams
Class diagrams excel at representing common design patterns. Let's explore some fundamental patterns:
Strategy Pattern
The Strategy pattern allows you to define a family of algorithms and make them interchangeable:
classDiagram
class PaymentContext {
-PaymentStrategy strategy
+setPaymentStrategy(strategy: PaymentStrategy)
+executePayment(amount: decimal) boolean
}
class PaymentStrategy {
<>
+pay(amount: decimal) boolean
+validate() boolean
}
class CreditCardPayment {
-string cardNumber
-string expiryDate
+pay(amount: decimal) boolean
+validate() boolean
}
class PayPalPayment {
-string email
-string password
+pay(amount: decimal) boolean
+validate() boolean
}
class BankTransferPayment {
-string accountNumber
-string routingNumber
+pay(amount: decimal) boolean
+validate() boolean
}
PaymentContext --> PaymentStrategy : uses
PaymentStrategy <|.. CreditCardPayment
PaymentStrategy <|.. PayPalPayment
PaymentStrategy <|.. BankTransferPayment Observer Pattern
The Observer pattern defines a one-to-many dependency between objects:
classDiagram
class Subject {
<>
+attach(observer: Observer)
+detach(observer: Observer)
+notifyObservers()
}
class Observer {
<>
+update(subject: Subject)
}
class WeatherStation {
-List~Observer~ observers
-float temperature
-float humidity
-float pressure
+attach(observer: Observer)
+detach(observer: Observer)
+notifyObservers()
+setMeasurements(temp: float, humidity: float, pressure: float)
}
class PhoneDisplay {
-WeatherStation weatherStation
+update(subject: Subject)
+display()
}
class WebDisplay {
-WeatherStation weatherStation
+update(subject: Subject)
+render()
}
Subject <|.. WeatherStation
Observer <|.. PhoneDisplay
Observer <|.. WebDisplay
WeatherStation "1" --> "0..*" Observer : notifies Practical Class Diagram: E-Learning Platform
Let's create a comprehensive class diagram for an e-learning platform that demonstrates multiple relationship types and design principles:
classDiagram
class User {
<>
#string userId
#string email
#string firstName
#string lastName
#Date registrationDate
+login() boolean
+logout()
+updateProfile()
}
class Student {
-List~Course~ enrolledCourses
-int totalCredits
+enrollInCourse(course: Course) boolean
+dropCourse(course: Course) boolean
+viewGrades() List~Grade~
+getTranscript() Transcript
}
class Instructor {
-List~Course~ taughtCourses
-string department
-string title
+createCourse(course: Course) boolean
+assignGrade(student: Student, assignment: Assignment, grade: Grade)
+viewStudentProgress(student: Student) Progress
}
class Course {
+string courseId
+string title
+string description
+int credits
+int maxStudents
+Date startDate
+Date endDate
+addStudent(student: Student) boolean
+removeStudent(student: Student) boolean
}
class Assignment {
+string assignmentId
+string title
+string description
+Date dueDate
+int maxPoints
+string type
+submit(submission: Submission) boolean
}
class Submission {
+string submissionId
+Date submittedDate
+string content
+List~Attachment~ attachments
+boolean isLate
+grade(points: int, feedback: string)
}
class Grade {
+string gradeId
+int points
+int maxPoints
+string letterGrade
+string feedback
+Date gradedDate
+calculatePercentage() float
}
class Enrollment {
+Date enrollmentDate
+string status
+float finalGrade
+boolean isCompleted
}
User <|-- Student
User <|-- Instructor
Student "0..*" --> "0..*" Course : enrolls
Instructor "1" --> "0..*" Course : teaches
Course "1" --> "0..*" Assignment : contains
Assignment "1" --> "0..*" Submission : receives
Submission "1" --> "0..1" Grade : receives
Student "1" --> "0..*" Submission : creates
Student "1" --> "0..*" Grade : receives
(Student, Course) .. Enrollment Class Diagram Best Practices
Naming Conventions
- Classes: Use PascalCase (User, BankAccount, PaymentProcessor)
- Attributes: Use camelCase (firstName, accountNumber, totalAmount)
- Methods: Use camelCase with verbs (calculateTotal, processPayment, validateInput)
- Constants: Use UPPER_SNAKE_CASE (MAX_ATTEMPTS, DEFAULT_TIMEOUT)
Design Principles
- Single Responsibility: Each class should have one reason to change
- Open/Closed Principle: Classes should be open for extension but closed for modification
- Favor Composition over Inheritance: Use composition when objects have different lifetimes
- Encapsulation: Keep internal details private and expose only necessary interfaces
Common Modeling Mistakes
- Over-Engineering: Creating too many small classes with single methods
- God Classes: Classes that do too much and violate single responsibility
- Inappropriate Inheritance: Using inheritance when composition would be better
- Missing Abstractions: Failing to identify common interfaces or base classes
From Class Diagrams to Code
Class diagrams translate directly to code structure. Here's how the BankAccount class from our earlier example might look in different programming languages:
Java Implementation
public class BankAccount {
private String accountNumber;
private BigDecimal balance;
private Date createdDate;
protected String accountType;
public String customerName;
public boolean deposit(BigDecimal amount) {
if (validateTransaction(amount)) {
balance = balance.add(amount);
return true;
}
return false;
}
public boolean withdraw(BigDecimal amount) {
if (validateTransaction(amount) && balance.compareTo(amount) >= 0) {
balance = balance.subtract(amount);
return true;
}
return false;
}
public BigDecimal getBalance() {
return balance;
}
private BigDecimal calculateInterest() {
// Implementation details
return BigDecimal.ZERO;
}
protected boolean validateTransaction(BigDecimal amount) {
return amount != null && amount.compareTo(BigDecimal.ZERO) > 0;
}
}Python Implementation
from datetime import datetime
from decimal import Decimal
from typing import Optional
class BankAccount:
def __init__(self, account_number: str, customer_name: str, account_type: str):
self._account_number = account_number # private
self._balance = Decimal('0.00') # private
self._created_date = datetime.now() # private
self._account_type = account_type # protected
self.customer_name = customer_name # public
def deposit(self, amount: Decimal) -> bool:
"""Public method to deposit money"""
if self._validate_transaction(amount):
self._balance += amount
return True
return False
def withdraw(self, amount: Decimal) -> bool:
"""Public method to withdraw money"""
if self._validate_transaction(amount) and self._balance >= amount:
self._balance -= amount
return True
return False
def get_balance(self) -> Decimal:
"""Public method to get current balance"""
return self._balance
def _calculate_interest(self) -> Decimal:
"""Private method to calculate interest"""
# Implementation details
return Decimal('0.00')
def _validate_transaction(self, amount: Decimal) -> bool:
"""Protected method to validate transaction"""
return amount is not None and amount > 0Integration with Development Tools
Modern IDEs and tools can generate class diagrams from code and vice versa:
- IntelliJ IDEA: Built-in UML plugin for generating diagrams from code
- Visual Studio: Class Designer for .NET projects
- Eclipse: Various UML plugins like Papyrus
- PlantUML: Text-based UML generation integrated with many tools
Conclusion: Building Robust Object-Oriented Designs
Class diagrams provide the structural foundation for all object-oriented systems. By mastering the relationships between classes—from simple associations to complex inheritance hierarchies—you can design systems that are both flexible and maintainable.
The key to effective class modeling lies in finding the right balance between simplicity and completeness. Start with the core entities and their primary relationships, then gradually add detail as your understanding of the system deepens. Remember that class diagrams are living documents that should evolve with your codebase.
In our next post, we'll explore Object and Component Diagrams, showing how class designs translate to runtime instances and system architecture. We'll see how the static structure captured in class diagrams comes alive in executing systems.

2 thoughts on “UML Class Diagrams: Master Object-Oriented Design (Part 2)”
Comments are closed.