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