UML Class Diagrams: Master Object-Oriented Design (Part 2)

UML Class Diagrams: Master Object-Oriented Design (Part 2)

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.

Written by:

342 Posts

View All Posts
Follow Me :

2 thoughts on “UML Class Diagrams: Master Object-Oriented Design (Part 2)

Comments are closed.