OOD Handboek

1UML-klassendiagram

Een klassendiagram is de blauwdruk van je code ("draw before coding"). Alles wat in je code staat, moet in het diagram terug te vinden zijn, en andersom.

#1.1 Opbouw van een klasse

Student-_cijfers: List<double>+Naam: string {get; private set}+Leeftijd: int {get; set}+AantalStudenten: int/Gemiddelde: double {get}+Student(naam: string, leeftijd: int)+VoegCijferToe(cijfer: double): void#Valideer(): bool+ToString(): string
VakInhoud
1. NaamKlassenaam, vet. Eventueel stereotype erboven: «abstract», «interface», «enumeration». Abstract: naam cursief.
2. AttributenVelden en properties: zichtbaarheid naam: Type. Properties krijgen {get; set}.
3. OperatiesConstructors en methodes: zichtbaarheid Naam(param: Type): ReturnType. Constructor heeft geen returntype.
TekenBetekenisC#
+publicpublic
-privateprivate
#protectedprotected
~package / internalinternal
onderstreeptstaticstatic
cursiefabstractabstract
/naamafgeleid / berekendget-only property met berekening

#1.2 Notatie-spiekbrief

UMLC#
-_saldo: decimalprivate decimal _saldo;
+Naam: string {get; set}public string Naam { get; set; }
+Id: string {get}public string Id { get; } (alleen in constructor te zetten)
+Naam: string {get; private set}public string Naam { get; private set; }
+Zone: Zone? {get; internal set}public Zone? Zone { get; internal set; } (? = mag null zijn)
+Voertuigen: IReadOnlyList<Voertuig> {get}public IReadOnlyList<Voertuig> Voertuigen => _voertuigen;
+Fiets(id: string, merk: string)public Fiets(string id, string merk) : base(id, merk) { }
#Voertuig(id: string)protected Voertuig(string id) (constructor van abstracte klasse)
+BerekenPrijs(minuten: int): decimalpublic abstract decimal BerekenPrijs(int minuten);
+LaadOp(): voidpublic void LaadOp() { ... }
+Instance: Verhuurbedrijf {get}public static Verhuurbedrijf Instance { get; }
-MAX: int = 100 {readOnly}private const int MAX = 100;
Doos<T> (of T in stippelvakje rechtsboven)class Doos<T>
«enumeration» Ernst: Informational, Warning, Criticalenum Ernst { Informational, Warning, Critical }

Twee schrijfwijzen voor types kom je tegen: UML-stijl naam: Type en C#-stijl Type naam. Kies er één en wees consequent. Werk je verder op een bestaand diagram, volg dan de stijl daarvan.

#1.3 Relaties (pijlen)

TekeningNaam en betekenisIn C#
KlantOrder10..*Associatie
Kennen elkaar (doorgetrokken lijn).
Property/veld naar het andere object.
BedrijfVoertuig10..*Gerichte associatie
A kent B, B kent A niet. Open pijlpunt bij B.
List<Voertuig> in Bedrijf
ZoneSensor0..10..*Aggregatie
Geheel-deel; deel kan los bestaan. Open ruit bij het geheel.
Deel komt van buiten binnen: VoegToe(sensor)
GebouwZone11..*Compositie
Geheel-deel; deel bestaat niet zonder het geheel. Gevulde ruit bij het geheel.
Geheel maakt het deel zelf: new Zone(...) binnenin
HondDierOvererving (generalisatie)
Is-een. Doorgetrokken lijn, open driehoek bij de ouder.
class Hond : Dier
ScooterIOplaad.Realisatie
Implementeert interface. Stippellijn, open driehoek bij de interface.
class Scooter : IOplaadbaar
BedrijfExceptionAfhankelijkheid
Gebruikt tijdelijk (parameter, lokale variabele, throw). Stippellijn met open pijl, evt. «throws»/«create».
throw new ...Exception()
Pijlrichting onthouden
Driehoek wijst altijd naar de ouder of de interface (het algemene). Ruit staat altijd aan de kant van het geheel. Open pijlpunt wijst naar de klasse die gekend/gebruikt wordt.

#1.4 Multipliciteit

NotatieBetekenisC#
1precies éénverplichte property (niet null)
0..1nul of éénType? (nullable)
* of 0..*nul of meerList<T>
1..*minstens éénlijst + controle op leeg
2..5bereiklijst + controle op aantal

Zet de multipliciteit aan beide uiteinden. Lees zo: "één Bedrijf heeft 0..* Voertuigen" (getal bij Voertuig).

#1.5 Compositie of aggregatie?

Vraag: bestaat het deel nog als het geheel weg is?

  • Nee → compositie. Een Zone zonder Gebouw bestaat niet. Gebouw maakt de Zone: VoegZoneToe(naam) doet new Zone(naam).
  • Ja → aggregatie. Een sensor kan naar een andere zone verplaatst worden. Zone krijgt hem binnen: VoegComponentToe(component).
  • Geen geheel-deel, alleen "kent"? → associatie.
CampusGebouwZoneHardwareComponent11..*11..*0..10..*
Voorbeeld (domotica op een campus): compositie Campus → Gebouw → Zone, aggregatie Zone → HardwareComponent.

#1.6 Abstracte klasse, interface en compositie in één voorbeeld

«abstract»Dier+Naam: string {get; set}#Leeftijd: int#Dier(naam: string)+MaakGeluid(): void+Adem(): void«interface»IVerzorgbaar+Verzorg(): voidHond+Halsband: Halsband {get}+Hond(naam: string)+MaakGeluid(): void+Verzorg(): voidHalsband+Kleur: string {get; set}is-a (erft van)realiseerthas-a1
public abstract class Dier
{
    public string Naam { get; set; }
    protected int Leeftijd;

    protected Dier(string naam) { Naam = naam; }

    public abstract void MaakGeluid();      // cursief in UML
    public void Adem() => Console.WriteLine("Ademt.");
}
Uitleg

Een abstracte basisklasse: je kunt geen new Dier() doen, alleen subklassen maken. MaakGeluid() heeft geen body, dus elke subklasse moet hem zelf invullen. Adem() is gewone code die alle dieren gratis erven. De constructor is protected omdat alleen subklassen hem aanroepen.

Functies en keywords
abstract
Klasse kan niet met new gemaakt worden; een abstracte methode heeft geen body en moet in de subklasse worden ingevuld.
protected
Zichtbaar in deze klasse en in subklassen, niet daarbuiten.
=>
Korte schrijfwijze voor een methode of property die één expressie teruggeeft; in LINQ ook de pijl van een lambda.
public interface IVerzorgbaar { void Verzorg(); }

public class Hond : Dier, IVerzorgbaar   // eerst klasse, dan interfaces
{
    public Halsband Halsband { get; } = new Halsband(); // compositie
    public Hond(string naam) : base(naam) { }
    public override void MaakGeluid() => Console.WriteLine("Woef!");
    public void Verzorg() => Console.WriteLine($"{Naam} wordt geborsteld.");
}
Uitleg

Hond erft van Dier (is-een) en belooft via de interface dat hij Verzorg() kan. In C# komt de basisklasse altijd eerst na de dubbele punt, daarna de interfaces. De Halsband wordt in de klasse zelf aangemaakt: dat is compositie (heeft-een).

Functies en keywords
base(...)
Roept de constructor van de ouderklasse aan, vóór de eigen constructor draait.
interface
Contract: een lijst met leden die een klasse belooft te leveren, zonder implementatie.
override
Vervangt een virtual of abstracte methode van de ouder; tijdens runtime wordt de versie van het echte object gekozen.
{ get; }
Alleen-lezen property; de waarde wordt in de constructor gezet.
=>
Korte schrijfwijze voor een methode of property die één expressie teruggeeft; in LINQ ook de pijl van een lambda.
$"...{x}"
String-interpolatie: zet de waarde van x direct in de tekst.

#1.7 Regels en valkuilen bij het tekenen

  • Geërfde leden niet herhalen in de subklasse. Een methode zet je alleen opnieuw in de subklasse als die hem overridet.
  • Methodes uit een interface staan wel in de klasse die ze implementeert (die klasse levert de code).
  • Constructors in het diagram zetten, met parameters. Constructor van een abstracte klasse is # (protected).
  • Een lijst mag als attribuut (-_voertuigen: List<Voertuig>) en als pijl met 0..*. Beide is gangbaar; wees consequent.
  • Exception = gewone klasse die erft van Exception. Waar hij gegooid wordt: stippelpijl «throws» of een notitie.
  • Singleton herken je aan: private constructor (-), static (onderstreept) veld _instance en property Instance.
  • Gebruik notities (geel blaadje met omgevouwen hoek) voor regels als "accu 0 t/m 100" of "gooit exception als ...".
  • Types altijd invullen, ook void. Gebruik decimal voor bedragen.

#1.8 Volledig uitgewerkt voorbeeld: DeelMobiel

Een verhuurbedrijf met fietsen, scooters en elektrische steps. Het diagram combineert alles uit dit hoofdstuk: abstracte klassen, interfaces, overerving, realisatie, associatie met multipliciteit, een eigen exception, logging via een interface en een Singleton. De code staat in hoofdstuk 12.

«interface»ILogger+Info(melding: string): void+Fout(melding: string): voidConsoleLogger+Info(melding: string): void+Fout(melding: string): voidVerhuurbedrijf-_instance: Verhuurbedrijf?-_voertuigen: List<Voertuig>+Instance: Verhuurbedrijf {get}+Logger: ILogger {get; set}+Voertuigen: IReadOnlyList<Voertuig> {get}-Verhuurbedrijf()+VoegToe(voertuig: Voertuig): void+Huur(id: string): void+Retourneer(id: string, minuten: int): decimal+LaadAllesOp(): voidExceptionVoertuigNietBeschikbaarException+VoertuigId: string {get}+VoertuigNietBeschikbaarException( voertuigId: string, melding: string)«abstract»Voertuig+Id: string {get}+Merk: string {get}+IsBeschikbaar: bool {get; set}#Voertuig(id: string, merk: string)+BerekenPrijs(minuten: int): decimal+ToString(): string«interface»IOplaadbaar+AccuPercentage: int {get}+LaadOp(): voidFiets+Versnellingen: int {get}+Fiets(id: string, merk: string, versnellingen: int)+BerekenPrijs(minuten: int): decimal+ToString(): string«abstract»ElektrischVoertuig-_accuPercentage: int+AccuPercentage: int {get; set}#ElektrischVoertuig(id: string, merk: string, accuPercentage: int)+LaadOp(): void+ToString(): stringScooter+Kenteken: string {get}+Scooter(id, merk, kenteken: string, accuPercentage: int)+BerekenPrijs(minuten: int): decimal+ToString(): stringElektrischeStep+MaxSnelheid: int {get}+ElektrischeStep(id, merk, maxSnelheid: int, accuPercentage: int)+BerekenPrijs(minuten: int): decimal+ToString(): string1Logger«throws»0..*1Singleton: private constructor,static _instance en Instance(statisch = onderstreept).Cursief = abstract.Gestippeld + opendriehoek = realisatie.