Skip to content

Class diagram

Het uittekenen van onze klassenmodellen doen we met ‘blokken’ en ‘pijltjes’ zoals bij zoveel diagrammen. Je hebt misschien al eens gezien hoe je dit doet voor een datamodel of voor een functiestroomdiagram. Al deze modellen kunnen we weergeven op dezelfde manier, zodat we met elkaar makkelijker kunnen praten over die modellen en dezelfde taal daarin spreken. Hiervoor is een algemene manier van schrijven en tekenen bedacht die heet “Unified Modeling Language” (UML). Voor klassenmodellen is dit toegepast met speciale symbolen die hier belangrijk zijn. Dan kunnen we een “Class model” maken.

graph BT
    UML[UML Diagram]
    Structural
    Behavioral
    Interaction
    Class
    Object
    Component
    CompositeStructure
    Deployment
    Package
    UseCase
    Activity
    StateMachine
    Communication
    InteractionOverview
    Sequence
    Timing

    Structural --> UML
    Behavioral --> UML

    Interaction --> Behavioral
    Class --> Structural
    Object --> Structural
    Component --> Structural
    CompositeStructure --> Structural
    Deployment --> Structural
    Package --> Structural

    UseCase --> Behavioral
    Activity --> Behavioral
    StateMachine --> Behavioral

    Communication --> Interaction
    InteractionOverview --> Interaction
    Sequence --> Interaction
    Timing --> Interaction

Met een uitgedacht “class diagram” als structuur/ontwerp kan een developer code gaan schrijven voor een software project.

Een class beschrijven met UML

Met behulp van UML kunnen we dus ook onze klassen op een uniforme manier definiëren. Het voorbeeld staat hieronder. De naam staat altijd boven, gecentreerd, en iets groter en dikker. De eigenschappen volgen daarna. Voor elke eigenschap definiëren we ook het datatype. Als laatste beschrijven we de methodes. Daarin zie je de naam van de methode en de benodigde argumenten. Het return/resultaat type (bijvoorbeeld ‘void’ of ‘string’) kun je benoemen naast de method.

classDiagram
    class BankAccount{
        owner: string
        balance: number = 0
        deposit(amount: number)
        withdraw(amount: number)
        calculateInterest() number
    }
En zo kan dit class diagram er in TypeScript uitzien:
class BankAccount {
    owner: string;
    balance: number;

    constructor(owner: string) {
        this.owner = owner;
        this.balance = 0;
    }

    deposit(amount: number): void {
        if (amount > 0) {
            this.balance += amount;
        }
    }

    withdraw(amount: number): void {
        if (amount > 0 && this.balance >= amount) {
            this.balance -= amount;
        }
    }

    calculateInterest(): number {
        return this.balance * 0.01;
    }
}

// Voorbeeldgebruik van de BankAccount klasse
const myAccount = new BankAccount("John Doe");
myAccount.deposit(1000);
myAccount.withdraw(500);
const interest = myAccount.calculateInterest() number;
console.log(`Balance: ${myAccount.balance}, Interest: ${interest}`);

Access modifiers

In UML kunnen we ook aangeven of een eigenschap of methode publiek, privaat of beschermd is. Dit doen we met een plus, min of hekje voor de naam van de eigenschap of methode:

  • ’+’ = public
  • ’-’ = private
  • ’#’ = protected
classDiagram
    class BankAccount{
        + owner: string
        - balance: number = 0
        + deposit(amount: number)
        + withdraw(amount: number)
        - calculateInterest() number
    }

In het bovenstaande voorbeeld is de eigenschap owner publiek, en de eigenschap balance privaat. De methodes deposit en withdraw zijn publiek, en de methode calculateInterest is privaat.

Static

In UML kunnen we ook aangeven of een eigenschap of methode statisch is. Dit doen we met een onderstreepte naam.

classDiagram
    class BankAccount{
        + owner: string
        - balance: number = 0
        + deposit(amount: number)
        + withdraw(amount: number)
        - calculateInterest() number
        + numberOfAccounts: number $
    }

In het bovenstaande voorbeeld is de eigenschap numberOfAccounts statisch en publiek.

Relaties tussen klassen

In UML kunnen we ook aangeven hoe klassen met elkaar samenwerken. Dit doen we met lijnen tussen de klassen. De lijnen hebben een bepaalde betekenis.

Unidirectioneel of bidirectioneel?

Een relatie kan van beide kanten komen, maar het kan ook eenrichtingsverkeer zijn.

Unidirectioneel

Een unidirectionele relatie is een relatie waarin de ene klasse de andere klasse kent, maar niet andersom.

classDiagram
    class Order {
        - orderId: string
        - customer: Customer
        - orderDate: Date
        + getCustomer() Customer
        + setCustomer(customer: Customer) void
    }

    class Customer {
        - customerId: string
        - name: string
        + getName() string
    }

    Order "1" --> "1" Customer : places

Bidirectioneel

Een bidirectionele relatie is een relatie waarin beide klassen elkaar kennen.

classDiagram
    class Teacher {
        - name: string
        - courses: Course[]
        + addCourse(course: Course) void
        + getCourses() Course[]
    }

    class Course {
        - title: string
        - teacher: Teacher
        - students: Student[]
        + setTeacher(teacher: Teacher) void
        + addStudent(student: Student) void
        + getStudents() Student[]
    }

    class Student {
        - name: string
        - courses: Course[]
        + enrollCourse(course: Course) void
        + getCourses() Course[]
    }

    Teacher "1" <--> "1..*" Course : teaches
    Course "1..*" <--> "*" Student : enrolled in

Inheritance

Een inheritance is een relatie tussen twee klassen. Deze relatie is altijd unidirectioneel. De lijn heeft een lege driehoek. De lege driehoek staat bij de parent/super klasse. Deze wijst dus als een pijl van de child/sub klasse naar de parent/super klasse. En de parent/super klasse staat altijd boven de child/sub klassen.

classDiagram
    class BankAccount{        
        - balance: number = 0
        + deposit(amount: number) void
        + withdraw(amount: number) void
        - calculateInterest() number
        numberOfAccounts: number $
    }

    class SavingsAccount{
        - interestRate: number
        + calculateInterest() number
    }

    BankAccount <|-- SavingsAccount

In het bovenstaande voorbeeld is de relatie tussen BankAccount en SavingsAccount een inheritance. De SavingsAccount erft van BankAccount. De BankAccount kent de SavingsAccount niet.

En zo kan dit class diagram er in TypeScript uitzien:

class BankAccount {
    private balance: number;
    public static numberOfAccounts: number = 0;

    constructor() {
        this.balance = 0;
        BankAccount.numberOfAccounts++;
    }

    public deposit(amount: number): void {
        if (amount > 0) {
            this.balance += amount;
        }
    }

    public withdraw(amount: number): void {
        if (amount > 0 && this.balance >= amount) {
            this.balance -= amount;
        }
    }

    private calculateInterest() number: number {
        return this.balance * 0.01;
    }
}

class SavingsAccount extends BankAccount {
    private interestRate: number;

    constructor(interestRate: number) {
        super();
        this.interestRate = interestRate;
    }

    public calculateInterest(): number {
        return this.balance * this.interestRate;
    }
}

// Voorbeeldgebruik van de BankAccount en SavingsAccount klassen
const myAccount = new BankAccount();
myAccount.deposit(1000);
myAccount.withdraw(500);

const savingsAccount = new SavingsAccount(0.05);
savingsAccount.deposit(5000);
const interest = savingsAccount.calculateInterest() number;
console.log(`Balance: ${savingsAccount.balance}, Interest: ${interest}`);

Abstraction

Als je een abstracte klasse wilt gebruiken als basis/parent klasse voor andere klassen, dan kun je dit aangeven door de klassenaam in italic, zo te noteren <<klassenaam>> of het boven de klassenaam te beschrijven <<Abstract>>.

classDiagram
    class Musician {
        <<Abstract>> 
        - artistName: string,
        - bandName: string,
        - bankAccount: string
        + getName() void
        + play()* void
    } 

    class Drummer{
        - playIntro() void
    }

    class Guitarist{
        - volumeGuitar: number
        - playSolo() void
    }
    Musician <|-- Drummer
    Musician <|-- Guitarist

En zo kan dit class diagram er in TypeScript uitzien:

abstract class Musician {
    protected artistName: string;
    protected bandName: string;
    protected bankAccount: string;

    constructor(artistName: string, bandName: string, bankAccount: string) {
        this.artistName = artistName;
        this.bandName = bandName;
        this.bankAccount = bankAccount;
    }

    public getName(): string {
        return this.artistName;
    }

    abstract play(): void;
}

class Drummer extends Musician {
    constructor(artistName: string, bandName: string, bankAccount: string) {
        super(artistName, bandName, bankAccount);
    }

    private playIntro(): void {
        console.log(`${this.artistName} is playing the intro on drums.`);
    }

    play(): void {
        this.playIntro();
    }
}

class Guitarist extends Musician {
    private volumeGuitar: number;

    constructor(artistName: string, bandName: string, bankAccount: string, volumeGuitar: number) {
        super(artistName, bandName, bankAccount);
        this.volumeGuitar = volumeGuitar;
    }

    private playSolo(): void {
        console.log(`${this.artistName} is playing a solo on guitar at volume ${this.volumeGuitar}.`);
    }

    play(): void {
        this.playSolo();
    }
}

// Voorbeeldgebruik van de Drummer en Guitarist klassen
const drummer = new Drummer("John Bonham", "Led Zeppelin", "123456789",);
drummer.play();

const guitarist = new Guitarist("Jimi Hendrix", "The Jimi Hendrix Experience", "987654321", 10);
guitarist.play();

Association

Een association is een relatie tussen twee klassen. Deze relatie is altijd bidirectioneel. De lijn heeft geen pijlpunt aan beide kanten. Met een tekst op de verbindende lijn kun je relatie beschrijven.

classDiagram
    class BankAccount{
        + owner: Owner
        - balance: number = 0
        + deposit(amount: number)
        + withdraw(amount: number)
        - calculateInterest() number
        numberOfAccounts: number $
    }

    class Owner{
        + name: string
        - address: string
        + phoneNumber: string
        + getAddress() string
    }

    BankAccount -- Owner : has a

In het bovenstaande voorbeeld is de relatie tussen BankAccount en Owner een association.

Aggregation

Een aggregation is een relatie tussen twee klassen. Deze relatie geeft aan dat de verzamelklasse (de klasse die wordt geraakt door de lege ruit) op de een of andere manier het “geheel” is. En dat de andere klasse in de relatie op de een of andere manier “deel” uitmaakt van dat geheel. De lijn heeft een leeg ruitje aan de kant van de klasse die de andere klasse bevat.

classDiagram
    class BankAccount{
        + owner: Owner
        - balance: number
        + deposit(amount: number)
        + withdraw(amount: number)
        - calculateInterest() number
        numberOfAccounts: number $
    }

    class Owner{
        + name: string
        - address: string
        + phoneNumber: string
        + getAddress() string
    }

    BankAccount o-- Owner

In het bovenstaande voorbeeld is de relatie tussen BankAccount en Owner een aggregation. De BankAccount bevat een Owner. De Owner kan bestaan zonder BankAccount.

Composition

Een composition is een relatie tussen twee klassen. En in deze relatie is er een afhankelijkheid. De ene klasse kan niet bestaan zonder de andere klasse. De lijn heeft een gevuld ruitje aan de kant van de klasse die de andere klasse bevat.

classDiagram
    class Car {
        - engine: Engine
        + name: string
        + drive() void
        + brake() void
    }

    class Engine {
        + name: string
        + type: string
        + start() void
        + stop() void
    }

    Car *-- Engine

In het bovenstaande voorbeeld is de relatie tussen Car en Engine een composition. De Car bevat een Engine. De Engine kan niet bestaan zonder Car.

Cardinaliteit

Cardinaliteit (of multipliciteit) in klassen diagrammen geeft het aantal instanties van de ene klasse dat gekoppeld kan worden aan een instantie van de andere klasse. Hieronder een aantal voorbeelden:

Bedrijf - werknemer

Elk bedrijf heeft bijvoorbeeld één of meer werknemers (niet nul), en elke werknemer werkt momenteel voor nul of één bedrijf.

classDiagram
    class Company {
        - employees: Employee[]
        + addEmployee() void
        + removeEmployee() void
        + getEmployees() Employee[]
        + hasMinimumEmployees() boolean
    }
    class Employee {
        - company: Company
        + getName() string
        + getCompany() Company | null
        + setCompany() void
    }
    Company "0--1" -- "1..*" Employee 

TypeScript code voorbeeld:

class Company {
    private employees: Employee[] = [];

    constructor() {
        // Company kan 0 of 1 keer voorkomen per Employee
    }

    public addEmployee(employee: Employee): void {
        if (employee.getCompany() !== null) {
            throw new Error("Employee already has a company");
        }
        this.employees.push(employee);
        employee.setCompany(this);
    }

    public removeEmployee(employee: Employee): void {
        const index = this.employees.indexOf(employee);
        if (index > -1) {
            this.employees.splice(index, 1);
            employee.setCompany(null);
        }
    }

    public getEmployees(): Employee[] {
        return this.employees;
    }

    public hasMinimumEmployees(): boolean {
        return this.employees.length >= 1; // Minimaal 1 employee vereist
    }
}

class Employee {
    private company: Company | null = null;

    constructor(private name: string) {}

    public getName(): string {
        return this.name;
    }

    public getCompany(): Company | null {
        return this.company;
    }

    public setCompany(company: Company | null): void {
        this.company = company;
    }
}

const company = new Company();
const employee1 = new Employee("John Doe");
const employee2 = new Employee("Jane Smith");
company.addEmployee(employee1); // Company heeft minimaal 1 employee
company.addEmployee(employee2);

Klant - ticket

1 klant kan meerdere tickets kopen.

classDiagram
    Customer "1" -- "*" Ticket

TypeScript code voorbeeld:

class Customer {
    private tickets: Ticket[] = [];

    constructor(private name: string) {
        // Customer moet altijd bestaan (1)
    }

    public addTicket(ticket: Ticket): void {
        this.tickets.push(ticket);
        ticket.setCustomer(this);
    }

    public removeTicket(ticket: Ticket): void {
        const index = this.tickets.indexOf(ticket);
        if (index > -1) {
            this.tickets.splice(index, 1);
        }
    }

    public getTickets(): Ticket[] {
        return this.tickets;
    }

    public getName(): string {
        return this.name;
    }
}

class Ticket {
    private customer: Customer;

    constructor(private description: string, customer: Customer) {
        this.customer = customer;
        customer.addTicket(this);
    }

    public getDescription(): string {
        return this.description;
    }

    public getCustomer(): Customer {
        return this.customer;
    }

    public setCustomer(customer: Customer): void {
        this.customer = customer;
    }
}

const customer = new Customer("Alice Johnson");
const ticket1 = new Ticket("Fix the bug in the system", customer); // Ticket moet altijd een customer hebben
const ticket2 = new Ticket("Add new feature", customer); // Customer kan 0 of meer tickets hebben

Student - cursus

1 student kan 1 of meerdere cursussen volgen.

classDiagram
    Student "1" -- "1..*" Course

TypeScript code voorbeeld:

class Student {
    private courses: Course[] = [];

    constructor(private name: string) {
        // Student moet altijd bestaan (1)
    }

    public enrollCourse(course: Course): void {
        if (!this.courses.includes(course)) {
            this.courses.push(course);
            course.addStudent(this);
        }
    }

    public dropCourse(course: Course): void {
        if (this.courses.length <= 1) {
            throw new Error("Student must be enrolled in at least one course");
        }
        const index = this.courses.indexOf(course);
        if (index > -1) {
            this.courses.splice(index, 1);
            course.removeStudent(this);
        }
    }

    public getCourses(): Course[] {
        return this.courses;
    }

    public getName(): string {
        return this.name;
    }

    public hasMinimumCourses(): boolean {
        return this.courses.length >= 1; // Minimaal 1 course vereist
    }
}

class Course {
    private students: Student[] = [];

    constructor(private name: string) {}

    public getName(): string {
        return this.name;
    }

    public addStudent(student: Student): void {
        if (!this.students.includes(student)) {
            this.students.push(student);
        }
    }

    public removeStudent(student: Student): void {
        const index = this.students.indexOf(student);
        if (index > -1) {
            this.students.splice(index, 1);
        }
    }

    public getStudents(): Student[] {
        return this.students;
    }
}

const student = new Student("Bob Keyboard");
const course1 = new Course("Computer Science 101");
const course2 = new Course("Mathematics 201");
student.enrollCourse(course1); // Student moet minimaal 1 course hebben
student.enrollCourse(course2);

Bronnen

Voor dit document zijn de volgende bronnen gebruikt:

  • https://en.wikipedia.org/wiki/Class_diagram
  • https://mermaid.ai/open-source/syntax/classDiagram.html