Wat leer je in dit hoofdstuk?

Na dit hoofdstuk kan je:

uitleggen waarom OOP code modulair en onderhoudsvriendelijk maakt

het verschil tussen een klasse en een object beschrijven

abstractie en encapsulatie situeren (het black-box-principe)

een klasse maken met methoden en instantievariabelen

access modifiers (public/private) correct gebruiken

objecten hun staat laten beschermen via properties (full en auto)

werken met de ingebouwde DateTime-klasse

Van gestructureerd naar OOP

Tot nu toe programmeerden we gestructureerd (procedureel): loops, methoden, beslissingen.

Gestructureerd

werkt prima, maar grote projecten verzanden in spaghetti

OOP

bouwt hierop voort, maar maakt code modulair, leesbaar en onderhoudsvriendelijk

Alles draait voortaan rond klassen en objecten.

OOP in het derde jaar

Toen ik OOP kreeg, in C++, was dat pas in het

derde jaar

bij de ingenieurs. Jij krijgt het in je eerste.

Paratrooper-reboot D&D over “het internet” MFC32, iemand?

Pong zonder OOP

int balX = 20; int balY = 20;
int vX = 2;    int vY = 1;
while (true) { /* posities updaten, tekenen ... */ }

Waarschuwing

Twee balletjes? Dan moet je bijna elke lijn verdubbelen (bal2X, v2X, …). Bij honderd balletjes wordt het een ramp.

Pong met OOP: de klasse

internal class Balletje
{
    public int X { get; set; }
    public int Y { get; set; }
    public int VX { get; set; }
    public int VY { get; set; }

    public void Update() { /* botst tegen de randen */ }
    public void TekenOpScherm()
    {
        Console.SetCursorPosition(X, Y);
        Console.Write("O");
    }
}

We stoppen de data en het gedrag van een balletje samen in één klasse.

Pong met OOP: de Main

Balletje bal1 = new Balletje();
bal1.X = 20; bal1.Y = 20; bal1.VX = 2; bal1.VY = 1;

Balletje bal2 = new Balletje();
bal2.X = 10; bal2.Y = 8; bal2.VX = 2; bal2.VY = -1;

while (true)
{
    bal1.Update();  bal2.Update();
    bal1.TekenOpScherm();  bal2.TekenOpScherm();
    Thread.Sleep(50); Console.Clear();
}

Tip

Honderd balletjes? Een array van Balletje en twee loopjes. Zo simpel.

De kracht van OOP

De logica zit in de objecten zelf. Elk object is verantwoordelijk voor zijn eigen gedrag, op basis van zijn eigen interne toestand.

In de Main zeg je gewoon:

“update jezelf”

bal1.Update();

“teken jezelf”

bal1.TekenOpScherm();

Even doorbijten

Duizend mammoeten en sabeltandtijgers! Ik doe een voorspelling: van alle hoofdstukken wordt dit het hoofdstuk waar je het meest je tanden op stukbijt.

Hou vol, geef niet te snel op, en kom hier geregeld terug.

Klassen en objecten

Klasse versus object

Klasse

een blauwdruk die het gedrag en de toestand van objecten beschrijft

Object

een instantie van die klasse, met een eigen toestand en identiteit

Tip

Het grondplan van een huis is de klasse. De tien huizen die je ermee bouwt zijn de objecten: elk met eigen bakstenen en rolluiken.

Toestand, gedrag, identiteit

Een object heeft:

Gedrag

beschreven door de methoden van de klasse

Toestand

bepaald door zijn instantievariabelen en properties

Identiteit

een unieke naam zodat andere objecten ermee kunnen werken

Tip

Zoek in een opgave naar de zelfstandige naamwoorden: dat zijn vaak je klassen.

Abstractie en encapsulatie

Het black-box-principe, denk aan een auto:

Encapsulatie

alle onderdelen die samenhoren bundel je tot één geheel

Abstractie

naar buiten toe vereenvoudig je de complexiteit tot een interface (stuur, pedalen, dashboard)

Hoe minder de buitenwereld moet weten om met een object te werken, hoe beter.

Tijd voor taart: A PIE

De vier pijlers van OOP:

A Abstractie

P Polymorfisme

hoofdstuk 16

I Inheritance / overerving

hoofdstuk 13

E Encapsulatie

Opmerking

Steve Jobs over objecten: “They encapsulate complexity, and the interfaces to that complexity are high level.”

OOP in C#

Een klasse maken

internal class Auto
{

}

Een klassenaam begint met een hoofdletter

Zet elke klasse in een apart .cs-bestand (Project - Add class…)

VS zet er de access modifier internal voor

Objecten aanmaken met new

Auto mijnEersteAuto = new Auto();
Auto mijnAndereAuto = new Auto();

Objecten maak je met het new keyword (zoals eerder bij Random):

new

maakt het object in het geheugen

geeft het adres terug

Tip

Klassen zijn dus gewoon een nieuw, krachtiger datatype: variabelen die meerdere waarden én methoden bundelen.

new en null

Zonder new bestaat je object niet: de variabele is dan null (een lege doos).

Auto auto;            // null
Console.WriteLine(auto); // use of unassigned local variable

Waarschuwing

Een null-object tóch gebruiken geeft een NullReferenceException: dé klassieker bij beginnende OOP’ers. Je vergat new.

Object-methoden

internal class Mens
{
    public void Praat()
    {
        Console.WriteLine("Ik ben een mens!");
    }
}
Mens joske = new Mens();
joske.Praat();   // via de dot-operator
Ik ben een mens!

Geen static voor methoden in zo’n klasse

public maakt de methode bruikbaar voor de buitenwereld

Access modifiers

private

enkel zichtbaar binnen de klasse zelf (de standaard als je niets schrijft)

public

iedereen kan eraan

internal

enkel binnen het huidige project

protected

voor overerving (later)

Belangrijk

Zet altijd expliciet een access modifier. Niets schrijven betekent private, maar dat expliciet maken leest veel duidelijker.

Waarom private?

Interne hulpcode scherm je af zodat anderen ze niet per ongeluk aanroepen. Binnen de klasse mag je ze wél gebruiken:

public void Praat()
{
    Console.WriteLine("Ik ben een mens!");
    VertelGeheim();        // mag: zelfde klasse
}
private void VertelGeheim()
{
    Console.WriteLine("Ik ben verliefd op Anneke");
}
Ik ben een mens!
Ik ben verliefd op Anneke

Instantievariabelen

Elke instantie houdt zijn eigen toestand bij in een instantievariabele:

internal class Mens
{
    private int geboorteJaar = 1970;

    public void Praat()
    {
        Console.WriteLine($"Ik ben geboren in {geboorteJaar}.");
    }
}

Verjong je het ene object, dan blijft het andere ongemoeid: elk object heeft zijn eigen geboorteJaar.

Houd instantievariabelen privé

Ik zag je denken: “zal ik die instantievariabele eens public maken?” Niet doen. Simpel.

Een public instantievariabele laat de buitenwereld om het even welke waarde toekennen (adil.geboortejaar = -12000;) en breekt zo je klasse.

Belangrijk

Toegang regel je via properties en methoden, nooit via een publieke instantievariabele.

Gecontroleerde toegang

Een methode kan illegale waarden tegenhouden:

public void VeranderGeboortejaar(int nieuw)
{
    if (nieuw >= 1900)
        geboorteJaar = nieuw;
}

Een object beschermt zichzelf: de buitenwereld kan zijn werking niet zomaar om zeep helpen.

Properties

Properties: het idee

Darth Vader is een SithLord met een geheime naam en een hoeveelheid energie (nooit onder 0):

internal class SithLord
{
    private int energie;
    private string sithName;
}

Tip

Properties zijn de moderne C#-manier om de interne staat gecontroleerd in en uit te lezen, in plaats van losse methoden.

Full property

private int energie;

public int Energie
{
    get { return energie; }
    set { energie = value; }
}
Vader.Energie = 20;                  // set
Console.WriteLine(Vader.Energie);    // get
20

Een property is altijd public en begint met een hoofdletter

In de set zit de waarde in het keyword value

Full property met controle

public int Energie
{
    get { return energie; }
    set
    {
        if (value >= 0)
            energie = value;
    }
}

De property treedt op als wachter.

palpatine.Energie = -1;

heeft nu geen effect: negatieve energie wordt geweigerd.

Property-variaties

Write-only

geen get (bv. een pincode)

Read-only

geen set

Read-only met private set

enkel intern aanpasbaar

Transformerend

berekent data in de get

Tip

Pas, ook binnen de klasse, je instantievariabele aan via de property, zodat je controles niet omzeilt.

De full-property-varianten.

Transformerende property

Een read-only property die data berekent (geen eigen instantievariabele):

internal class Persoon
{
    public string Voornaam { get; set; }
    public string Achternaam { get; set; }

    public string VolledigeNaam
    {
        get { return $"{Voornaam} {Achternaam}"; }
    }
}

Methode of property?

Tip

  • Gaat het om een actie of gedrag (iets doen, tonen, berekenen dat tijd kost)? Gebruik een methode.
  • Gaat het om een eigenschap met data die snel verkregen wordt? Gebruik een property.

Auto-properties

Geen extra controle nodig? Dan volstaat een auto-property, zonder zichtbare instantievariabele:

internal class Persoon
{
    public string Voornaam { get; set; }
    public int Geboortejaar { get; set; } = 2002;   // beginwaarde
    public string Naam { get; private set; }          // read-only naar buiten
}

Tip

Begin vaak met auto-properties; vervang ze door full properties zodra je controle of transformatie nodig hebt. (prop + tab + tab in VS.)

OOP in de praktijk: DateTime

DateTime: een ingebouwde klasse

DateTime nu = DateTime.Now;                  // huidige datum en tijd
DateTime dag = new DateTime(2017, 4, 21);    // jaar, maand, dag

DateTime.Now is een static property

new DateTime(...) gebruikt een constructor (hoofdstuk 11) om het object meteen in te stellen

DateTime: methoden en properties

DateTime einde = trouwMoment.AddDays(35);
Console.WriteLine(einde.Month);      // 5
Console.WriteLine(einde.DayOfWeek);  // Friday

AddDays, AddHours, AddYears, … geven telkens een nieuw DateTime-object terug

Properties zoals Month en DayOfWeek zijn read-only

DateTime via de dot-operator.

DateTime: TimeSpan

Twee DateTime-objecten aftrekken geeft een TimeSpan:

DateTime vandaag = DateTime.Today;
DateTime geboorte = new DateTime(2009, 6, 17);
TimeSpan verschil = vandaag - geboorte;
Console.WriteLine($"{verschil.TotalDays} dagen oud.");

Optellen van twee datums kan niet

Aftrekken wel

Uitdaging: Personal Trainer

Bouw een mini-fitness-app waarin een sporter zijn trainingen bijhoudt. Een klasse Loper met:

Naam

AantalLopen, TotaleAfstand en TotaleTijd (alleen leesbaar naar buiten)

een methode RegistreerTraining(afstand, tijd)

Tip

Gebruik properties met begrenzing (clamping): een negatieve afstand of tijd weiger je in de set. Zo beschermt het object zichzelf, precies de kern van dit hoofdstuk.

Terugblik op een stevig hoofdstuk

We doorliepen vier fasen:

  1. Pong toonde waarom OOP helpt
  2. de theorie (klasse vs object) verwarde even
  3. de praktijk: methoden en properties als kern van een klasse
  4. DateTime als voorproefje van een krachtige klasse

Conclusie

OOP bundelt data en gedrag in klassen; objecten zijn de concrete instanties (met new)

Een object beheert zijn eigen toestand; instantievariabelen blijven privé

Properties geven gecontroleerde toegang: full (met get/set/value) of auto

Vergeet je new, dan is je object null en krijg je een NullReferenceException

DateTime toont hoe krachtig een goed ontworpen klasse is

Tip

Volgende halte: geheugenmanagement en uitzonderingen (exceptions).

Meer weten

Kennisclips

Oefeningen H9 · Quizlet flashcards

Zie verder

Hoe doen andere programmeertalen hetzelfde? En een korte zoek-de-fout, om zo kritisch mogelijk naar code te leren kijken.

Zie verder: moet alles in een klasse?

Dat alle code in een klasse moet, deelt C# met Java. C# zit aan het strenge eind.

Python en JavaScript zijn losser: daar mag je gewoon losse code en functies schrijven. OOP is daar een keuze, geen verplichting.

In C bestaat het begrip klasse zelfs niet.

# een volledig geldig programma, geen klasse in zicht
print("Hallo wereld")

Zie verder: properties elders

Java

Java kent geen properties: daar schrijf je met de hand een aparte getter en setter:

sith.setEnergie(20);
System.out.println(sith.getEnergie());

Python

Python zit dichter bij C# met de @property-decorator: methoden die je van buiten gebruikt alsof het een gewone variabele is.

In beide gevallen verberg je de instantievariabele en regel je de toegang.

Zie verder: zoek de fout

Onderstaande Python-klasse wil de energie afschermen tegen negatieve waarden, maar de check werkt niet. Wat loopt er mis?

class SithLord:
    @property
    def energie(self):
        return self._energie

    @energie.setter
    def energie(self, waarde):
        if waarde >= 0:
            self.energie = waarde

Opmerking

In de setter staat self.energie = waarde in plaats van self._energie = waarde. Daardoor roept de setter zichzelf opnieuw aan (de property heet immers energie) en krijg je oneindige recursie. Je moet de achterliggende variabele _energie toewijzen, niet de property zelf. Hetzelfde gevaar bestaat in C# wanneer je in een set per ongeluk de property aanspreekt in plaats van de instantievariabele.

Samenvattende poster

Een handige samenvattende poster (Opgelet: gemaakt m.b.v. ChatGPT Image Gen 2).