Skip to main content

Class Diagrams in UML

Class diagrams are the backbone of object-oriented design and development. They provide a structural view of a system that showcases classes, their attributes, methods, and the relationships among different classes. In this comprehensive guide, we'll explore everything from the basics to advanced concepts of class diagrams, complete with JavaScript examples to help you understand how these diagrams translate into code. Class Diagrams in UML

1. Basics of Class Diagrams

A. What is a Class Diagram?

A class diagram is a type of static structure diagram in the Unified Modeling Language (UML) that describes the structure of a system by showing the system's classes, their attributes, methods, and the relationships among objects. Think of it as a blueprint for the objects that will be instantiated in your application.

Class Diagrams

Class diagrams are essential in object-oriented modeling because they:

  • Define the classes within a system
  • Provide a clear visual representation of the application's structure
  • Show relationships between different classes
  • Help in planning and communicating the architecture before implementation

B. Purpose of Class Diagrams

Purpose of Class Diagrams

Class diagrams serve multiple purposes throughout the software development lifecycle:

  1. Analysis and Design: They help in analyzing the requirements and designing the system structure.
  2. Documentation: They document the architecture of the system for future reference.
  3. Communication: They provide a common language for developers, stakeholders, and business analysts.
  4. Code Generation: They can be used as a basis for generating code skeletons.
  5. Reverse Engineering: Existing code can be visualized through class diagrams to understand the structure.

C. UML Notation for Class Diagrams

UML (Unified Modeling Language) provides standardized notation for creating class diagrams. The notation includes:

UML Notation for Class Diagrams

  • Classes: Represented as rectangles divided into three compartments (name, attributes, and methods)
  • Relationships: Represented by different types of lines and arrows
  • Multiplicity: Represented by numbers and symbols near the connection lines
  • Visibility: Represented by symbols (+, -, #, ~) preceding attributes and methods

2. Class Components

Every class in a class diagram consists of three main components: Class Components

A. Class Name

The top compartment contains the name of the class. By convention, class names are:

  • Nouns
  • Written in PascalCase (each word starts with a capital letter)
  • Descriptive of the entity they represent

For example: Person, BankAccount, ShoppingCart.

B. Attributes (Properties/Fields)

The middle compartment contains the attributes of the class, which represent the data that each object of the class will hold. Attributes are typically written as:

visibility name: type [multiplicity] = defaultValue

Where:

  • visibility: Indicates who can access the attribute (+, -, #, ~)
  • name: The name of the attribute
  • type: The data type of the attribute
  • multiplicity: Indicates how many instances of the attribute exist (optional)
  • defaultValue: Initial value of the attribute (optional)

For example:

  • - name: String
  • + balance: Number = 0
  • # addresses: Address[0..*]

C. Methods (Operations/Functions)

The bottom compartment contains the methods or operations that the class can perform. Methods are written as:

visibility name(parameterList): returnType

Where:

  • visibility: Indicates who can access the method
  • name: The name of the method
  • parameterList: The parameters the method accepts
  • returnType: The type of value returned by the method

For example:

  • + deposit(amount: Number): void
  • - calculateInterest(): Number
  • # validateAddress(address: Address): Boolean

D. Visibility (Public +, Private -, Protected #, Package ~)

Visibility modifiers define the access level of class members:

  • Public (+): Accessible from any other class
  • Private (-): Accessible only within the class
  • Protected (#): Accessible within the class and its subclasses
  • Package (~): Accessible within the same package (or namespace)

E. Static and Abstract Members

  • Static members: Underlined in UML, these belong to the class itself, not to instances of the class.
  • Abstract members: Written in italics, these are incomplete implementations that must be overridden in subclasses.

Here's a JavaScript example implementing a simple Person class based on UML class notation:

class Person {
// Private attributes
#id;
#name;
#age;

// Static attribute
static count = 0;

constructor(name, age) {
this.#id = ++Person.count;
this.#name = name;
this.#age = age;
}

// Public methods
getName() {
return this.#name;
}

getAge() {
return this.#age;
}

// Protected method (JavaScript doesn't have true protected, but we use convention)
#validateAge(age) {
return age > 0 && age < 150;
}

// Static method
static getPersonCount() {
return Person.count;
}
}

// Usage
const john = new Person("John Doe", 30);
const jane = new Person("Jane Smith", 28);

console.log(`Person count: ${Person.getPersonCount()}`); // 2
console.log(`${john.getName()} is ${john.getAge()} years old`);

Class based on UML class notation

3. Relationships Between Classes

Classes in object-oriented systems rarely exist in isolation. The relationships between classes are what make the system functional and cohesive.

Relationships Between Classes

A. Association

Association represents a relationship between two independent classes. It's drawn as a solid line between classes and indicates that objects of one class are somehow related to objects of another class.

  • Bidirectional: Both classes are aware of each other
  • Unidirectional: Only one class knows about the other

Example in UML: Student ──────── Course

In JavaScript:

class Student {
constructor(name) {
this.name = name;
this.courses = []; // Association with Course objects
}

enrollInCourse(course) {
this.courses.push(course);
course.addStudent(this);
}
}

class Course {
constructor(title) {
this.title = title;
this.students = []; // Association with Student objects
}

addStudent(student) {
this.students.push(student);
}
}

// Usage
const john = new Student("John");
const mathCourse = new Course("Mathematics");
john.enrollInCourse(mathCourse);

Class Association Diagram

B. Multiplicity

Multiplicity specifies how many instances of one class can be associated with one instance of another class. It's denoted by numbers and symbols near the association line.

Multiplicity

Common multiplicity indicators:

  • 1: Exactly one
  • 0..1: Zero or one
  • 0..*: Zero or more
  • 1..*: One or more
  • 5: Exactly five
  • 1..5: One to five

Example: Student "1" ──────── "0..*" Course (A student can take many courses, but each course instance belongs to one student)

C. Aggregation (Has-A relationship)

Aggregation represents a "whole-part" relationship where the "part" can exist independently of the "whole." It's shown by a line with an empty diamond at the "whole" end.

Example in UML: Department ◇────── Professor (A department has professors, but professors can exist without a department)

In JavaScript:

class Professor {
constructor(name, specialization) {
this.name = name;
this.specialization = specialization;
}
}

class Department {
constructor(name) {
this.name = name;
this.professors = []; // Aggregation - professors can exist independently
}

addProfessor(professor) {
this.professors.push(professor);
}
}

// Usage
const profJones = new Professor("Dr. Jones", "Quantum Physics");
const scienceDept = new Department("Science");
scienceDept.addProfessor(profJones);

// The professor can exist even if the department is deleted
const profList = [profJones]; // Still accessible

Aggregation (Has-A relationship)

D. Composition (Strong ownership)

Composition is a stronger form of aggregation where the "part" cannot exist without the "whole." When the "whole" is destroyed, the "parts" are also destroyed. It's represented by a filled diamond at the "whole" end.

Example in UML: Car ◆────── Engine (An engine is part of a car and doesn't exist separately)

In JavaScript:

class Engine {
constructor(type, horsepower) {
this.type = type;
this.horsepower = horsepower;
}
}

class Car {
constructor(make, model, engineType, horsepower) {
this.make = make;
this.model = model;
// Composition - the Engine is created as part of the Car
this.engine = new Engine(engineType, horsepower);
}
}

// Usage
const myCar = new Car("Toyota", "Corolla", "V6", 200);
// The engine doesn't exist independently of the car
console.log(myCar.engine.type); // V6

Composition (Strong ownership)

E. Dependency (Uses relationship)

Dependency indicates that one class depends on another class, but it's not a permanent relationship. It's shown as a dashed arrow from the dependent class to the class being used.

Example in UML: PaymentProcessor ----> Receipt (The PaymentProcessor uses a Receipt temporarily)

In JavaScript:

class Receipt {
constructor(amount, date) {
this.amount = amount;
this.date = date;
this.id = Math.floor(Math.random() * 10000);
}

print() {
return `Receipt #${this.id}: $${this.amount} on ${this.date}`;
}
}

class PaymentProcessor {
processPayment(amount) {
// Create and use a Receipt (dependency)
const receipt = new Receipt(amount, new Date().toLocaleDateString());
console.log(`Payment processed. ${receipt.print()}`);
return true;
}
}

// Usage
const processor = new PaymentProcessor();
processor.processPayment(99.99);

Dependency (Uses relationship))

F. Generalization/Inheritance (Is-A relationship)

Inheritance represents an "is-a" relationship where one class (subclass) inherits attributes and methods from another class (superclass). It's shown as a solid line with a hollow triangle pointing to the superclass.

Example in UML: Employee ◁───── Manager (A Manager is an Employee)

In JavaScript:

class Employee {
constructor(name, id, salary) {
this.name = name;
this.id = id;
this.salary = salary;
}

calculatePay() {
return this.salary / 12; // Monthly salary
}
}

class Manager extends Employee {
constructor(name, id, salary, department) {
super(name, id, salary);
this.department = department;
}

calculatePay() {
return super.calculatePay() + 500; // Manager bonus
}
}

// Usage
const employee = new Employee("John", "E001", 60000);
const manager = new Manager("Lisa", "M001", 84000, "Marketing");

console.log(employee.calculatePay()); // 5000
console.log(manager.calculatePay()); // 7500

Generalization/Inheritance

G. Realization (Interface implementation)

Realization indicates that a class implements an interface, promising to implement all the methods defined by that interface. It's shown as a dashed line with a hollow triangle pointing to the interface.

Example in UML: PaymentMethod <|... CreditCardPayment (CreditCardPayment implements the PaymentMethod interface)

In JavaScript (using TypeScript-like comments for interfaces):

// JavaScript doesn't have built-in interfaces, but we can simulate them
// Interface: PaymentMethod
// Methods: processPayment(amount)
// refundPayment(amount)

class CreditCardPayment {
constructor(cardNumber, expiryDate) {
this.cardNumber = cardNumber;
this.expiryDate = expiryDate;
}

// Implementing the interface methods
processPayment(amount) {
console.log(`Processing $${amount} via Credit Card ${this.cardNumber.substr(-4)}`);
return true;
}

refundPayment(amount) {
console.log(`Refunding $${amount} to Credit Card ${this.cardNumber.substr(-4)}`);
return true;
}
}

class PayPalPayment {
constructor(email) {
this.email = email;
}

// Implementing the interface methods
processPayment(amount) {
console.log(`Processing $${amount} via PayPal account ${this.email}`);
return true;
}

refundPayment(amount) {
console.log(`Refunding $${amount} to PayPal account ${this.email}`);
return true;
}
}

// Usage
function checkoutCart(cart, paymentMethod) {
// We can use any payment method that implements the interface
paymentMethod.processPayment(cart.total);
}

const cart = { items: ["Book", "Laptop"], total: 1299.99 };
const creditCard = new CreditCardPayment("4111-1111-1111-1111", "12/25");
const paypal = new PayPalPayment("user@example.com");

checkoutCart(cart, creditCard);
checkoutCart(cart, paypal);

Realization (Interface implementation)

4. Advanced Features

A. Abstract Classes and Interfaces

Abstract Classes are classes that cannot be instantiated directly and are meant to be subclassed. They can contain a mixture of concrete methods and abstract methods (methods without a body that must be implemented by subclasses).

In UML, abstract classes and methods are shown in italics or with the {abstract} property.

Interfaces define a contract of methods that implementing classes must fulfill. They contain only method signatures without implementations.

In JavaScript:

// Abstract class simulation in JavaScript
class Shape {
constructor() {
if (this.constructor === Shape) {
throw new Error("Abstract classes can't be instantiated.");
}
}

// Abstract method
calculateArea() {
throw new Error("Method 'calculateArea()' must be implemented.");
}

// Concrete method
printArea() {
console.log(`The area is: ${this.calculateArea()}`);
}
}

class Circle extends Shape {
constructor(radius) {
super();
this.radius = radius;
}

calculateArea() {
return Math.PI * this.radius * this.radius;
}
}

// Usage
const circle = new Circle(5);
circle.printArea(); // The area is: 78.53981633974483

try {
const shape = new Shape(); // Error: Abstract classes can't be instantiated
} catch (error) {
console.log(error.message);
}

Abstract Classes and Interfaces

B. Enumerations (Enums)

Enumerations represent a finite set of related values. In UML, they're shown as classes with the <<enumeration>> stereotype.

In JavaScript:

// Basic enum using JavaScript objects
const PaymentStatus = Object.freeze({
PENDING: 'PENDING',
PROCESSING: 'PROCESSING',
COMPLETED: 'COMPLETED',
FAILED: 'FAILED',
REFUNDED: 'REFUNDED'
});

// Using the enum
class Payment {
constructor(amount) {
this.amount = amount;
this.status = PaymentStatus.PENDING;
}

process() {
this.status = PaymentStatus.PROCESSING;
// Payment processing logic
if (Math.random() > 0.1) {
this.status = PaymentStatus.COMPLETED;
return true;
} else {
this.status = PaymentStatus.FAILED;
return false;
}
}

refund() {
if (this.status === PaymentStatus.COMPLETED) {
this.status = PaymentStatus.REFUNDED;
return true;
}
return false;
}
}

// Usage
const payment = new Payment(125.50);
console.log(payment.status); // PENDING
payment.process();
console.log(payment.status); // COMPLETED or FAILED

Enumerations (Enums)

C. Constraints and Notes

Constraints are rules that objects must follow, usually expressed in braces {}. They can apply to classes, attributes, methods, or relationships.

Notes are additional information attached to any element in the diagram. They're shown as rectangles with a folded corner.

In UML notation:

  • Constraint: {age >= 18}
  • Note: Connected to an element with a dashed line

D. Packages and Namespaces

Packages group related classes and can be represented in class diagrams. They help organize larger systems into logical units.

In UML, packages are shown as folders containing classes.

In JavaScript (using ES modules):

// file: models/User.js
export class User {
constructor(username, email) {
this.username = username;
this.email = email;
}
}

// file: models/Product.js
export class Product {
constructor(name, price) {
this.name = name;
this.price = price;
}
}

// file: services/UserService.js
import { User } from '../models/User.js';

export class UserService {
createUser(username, email) {
return new User(username, email);
}
}

// file: app.js
import { UserService } from './services/UserService.js';
import { Product } from './models/Product.js';

const userService = new UserService();
const user = userService.createUser('john_doe', 'john@example.com');
const product = new Product('Laptop', 999.99);

Packages and Namespaces

E. Class Responsibility Collaborator (CRC Cards)

CRC cards are a technique used in object-oriented design to identify the responsibilities of classes and the collaborations between them. While not strictly part of UML, they are often used alongside class diagrams.

A CRC card typically includes:

  • Class name
  • Responsibilities (what the class knows and does)
  • Collaborators (other classes it interacts with)

Example for a ShoppingCart class:

+-----------------------------------------------------+
| Class: ShoppingCart |
+---------------------+-------------------------------+
| Responsibilities: | Collaborators: |
| - Add items | - Product |
| - Remove items | - Customer |
| - Calculate total | - Discount |
| - Apply discounts | - PaymentProcessor |
| - Checkout | |
+---------------------+-------------------------------+

5. Class Diagram Examples

A. Real-world modeling: E-commerce System

Let's design an e-commerce system with the following classes:

  1. User (Customer)
  2. Product
  3. Order
  4. ShoppingCart
  5. PaymentMethod
  6. ShippingMethod

Here's a JavaScript implementation of this system:

class User {
constructor(id, name, email) {
this.id = id;
this.name = name;
this.email = email;
this.shippingAddresses = [];
this.orders = [];
}

addShippingAddress(address) {
this.shippingAddresses.push(address);
}

placeOrder(cart, paymentMethod, shippingMethod, shippingAddress) {
const order = new Order(
this,
cart.items,
cart.calculateTotal(),
paymentMethod,
shippingMethod,
shippingAddress
);

if (order.processPayment()) {
this.orders.push(order);
cart.clear();
return order;
}
return null;
}
}

class Product {
constructor(id, name, price, description, stockQuantity) {
this.id = id;
this.name = name;
this.price = price;
this.description = description;
this.stockQuantity = stockQuantity;
}

isInStock() {
return this.stockQuantity > 0;
}

decreaseStock(quantity) {
if (quantity <= this.stockQuantity) {
this.stockQuantity -= quantity;
return true;
}
return false;
}
}

class CartItem {
constructor(product, quantity) {
this.product = product;
this.quantity = quantity;
}

getSubtotal() {
return this.product.price * this.quantity;
}
}

class ShoppingCart {
constructor() {
this.items = [];
}

addItem(product, quantity = 1) {
if (!product.isInStock() || product.stockQuantity < quantity) {
throw new Error("Product not available in requested quantity");
}

const existingItem = this.items.find(item => item.product.id === product.id);

if (existingItem) {
existingItem.quantity += quantity;
} else {
this.items.push(new CartItem(product, quantity));
}
}

removeItem(productId) {
this.items = this.items.filter(item => item.product.id !== productId);
}

updateQuantity(productId, quantity) {
const item = this.items.find(item => item.product.id === productId);
if (item) {
if (quantity <= 0) {
this.removeItem(productId);
} else if (quantity <= item.product.stockQuantity) {
item.quantity = quantity;
} else {
throw new Error("Requested quantity exceeds stock");
}
}
}

calculateTotal() {
return this.items.reduce((total, item) => total + item.getSubtotal(), 0);
}

clear() {
this.items = [];
}
}

class Order {
#status;

constructor(user, items, total, paymentMethod, shippingMethod, shippingAddress) {
this.id = Math.floor(Math.random() * 1000000);
this.user = user;
this.items = [...items]; // Copy cart items
this.total = total;
this.paymentMethod = paymentMethod;
this.shippingMethod = shippingMethod;
this.shippingAddress = shippingAddress;
this.orderDate = new Date();
this.#status = "PENDING";
}

getStatus() {
return this.#status;
}

processPayment() {
try {
this.paymentMethod.processPayment(this.total);
this.#status = "PAID";
this.#updateProductStock();
return true;
} catch (error) {
this.#status = "PAYMENT_FAILED";
return false;
}
}

#updateProductStock() {
for (const item of this.items) {
item.product.decreaseStock(item.quantity);
}
}

ship() {
if (this.#status === "PAID") {
this.shippingMethod.ship(this);
this.#status = "SHIPPED";
return true;
}
return false;
}

cancel() {
if (["PENDING", "PAID"].includes(this.#status)) {
this.#status = "CANCELLED";
// Return items to stock if already paid
if (this.#status === "PAID") {
for (const item of this.items) {
item.product.stockQuantity += item.quantity;
}
}
return true;
}
return false;
}
}

class CreditCardPayment {
constructor(cardNumber, cardholderName, expiryDate, cvv) {
this.cardNumber = cardNumber;
this.cardholderName = cardholderName;
this.expiryDate = expiryDate;
this.cvv = cvv;
}

processPayment(amount) {
console.log(`Processing $${amount} payment via Credit Card ending in ${this.cardNumber.slice(-4)}`);
// In a real system, this would connect to a payment gateway
return true;
}

refundPayment(amount) {
console.log(`Refunding $${amount} to Credit Card ending in ${this.cardNumber.slice(-4)}`);
return true;
}
}

class StandardShipping {
constructor(cost, estimatedDays) {
this.cost = cost;
this.estimatedDays = estimatedDays;
}

ship(order) {
console.log(`Shipping order #${order.id} via Standard Shipping. Estimated delivery: ${this.estimatedDays} days`);
return true;
}
}

// Usage example
const laptop = new Product(1, "Laptop", 999.99, "High-performance laptop", 10);
const headphones = new Product(2, "Headphones", 99.99, "Wireless headphones", 20);

const user = new User(1, "John Doe", "john@example.com");
user.addShippingAddress({
street: "123 Main St",
city: "Anytown",
state: "CA",
zipCode: "12345",
country: "USA"
});

const cart = new ShoppingCart();
cart.addItem(laptop, 1);
cart.addItem(headphones, 2);

console.log(`Cart total: $${cart.calculateTotal()}`);

const payment = new CreditCardPayment("4111-1111-1111-1111", "John Doe", "12/25", "123");
const shipping = new StandardShipping(15.99, 3);

const order = user.placeOrder(
cart,
payment,
shipping,
user.shippingAddresses[0]
);

if (order) {
console.log(`Order #${order.id} placed successfully`);
order.ship();
}

B. Case Study: School Management System

Another real-world example would be a School Management System with classes like:

  • Student
  • Teacher
  • Course
  • Department
  • Grade

This diagram would show relationships such as:

  • A Student can enroll in multiple Courses (many-to-many)
  • A Teacher can teach multiple Courses (one-to-many)
  • A Department has multiple Teachers (one-to-many)
  • A Course belongs to a Department (many-to-one)
  • A Grade is associated with a Student and a Course (many-to-many with attributes)

6. Tools for Creating Class Diagrams

There are numerous tools available for creating class diagrams, each with its own strengths:

  1. Visual Paradigm: Professional UML tool with extensive features for enterprise-level modeling
  2. Lucidchart: Web-based diagramming tool with real-time collaboration
  3. StarUML: Open-source modeling tool with comprehensive UML support
  4. draw.io (diagrams.net): Free online diagram software with UML capabilities
  5. PlantUML: Text-based UML diagramming tool that generates diagrams from simple scripts
  6. Enterprise Architect: Comprehensive modeling platform for large-scale systems
  7. Microsoft Visio: Popular diagramming software with UML templates

Each tool has its own learning curve and price point, so the choice depends on your specific needs and budget.

7. Best Practices

Best Practices

A. Naming Conventions

  • Classes: Use nouns in PascalCase (e.g., Customer, OrderProcessor)
  • Attributes: Use descriptive nouns in camelCase (e.g., firstName, accountBalance)
  • Methods: Use verbs or verb phrases in camelCase (e.g., calculateTotal(), processPayment())
  • Interfaces: Use adjectives or nouns, often prefixed with "I" (e.g., IComparable, IPayable)

B. Keeping Diagrams Readable

  • Layout: Arrange classes to minimize crossing lines
  • Grouping: Group related classes together, possibly using packages
  • Spacing: Leave enough space between classes for relationship lines
  • Consistency: Use consistent notation throughout the diagram
  • Size: Break large diagrams into smaller, more manageable ones
  • Color: Use color sparingly to highlight important elements or group related ones

C. Modeling Only What's Necessary

  • Level of Detail: Include only the details relevant to your audience and purpose
  • Scope: Focus on the most important classes and relationships
  • Abstraction: Use higher-level abstractions for overview diagrams
  • Progressive Disclosure: Start with a high-level view and create detailed diagrams for specific areas
  • Relevance: Exclude implementation details that don't affect the architecture

Conclusion

info

Class diagrams are an indispensable tool in object-oriented development. They provide a clear visual representation of a system's structure, helping developers plan, communicate, and document their designs. By understanding the notation, components, relationships, and best practices of class diagrams, you can create models that effectively bridge the gap between conceptual design and actual implementation.

Whether you're designing a small application or a complex enterprise system, class diagrams help you visualize your object-oriented design before writing any code, potentially saving significant time and resources in the development process.

Remember that UML class diagrams are meant to be helpful tools, not rigid constraints. The goal is clear communication and understanding, so adapt the level of detail and formality to suit your specific needs and audience.