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 : contains
Aggregation: “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 : employs
Composition: 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 : has
Inheritance: “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 <|-- Truck
Dependency 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 : uses
Advanced 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 : offers
Design 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 > 0
Integration 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.