Object georiënteerd programmeren (deel 1)

Basisprincipes Object georiënteerd programmeren

Object georiënteerd programmeren vs structureel programmeren

Vooraleer we beginnen met het uitleggen van de verschillende elementen van het object georiënteerd programmeren, gaan we eerst even de basiskenmerken van object georiënteerd programmeren en structureel programmeren overlopen. Structureel programmeren betekent dat we simpelweg een begin- en een eindpunt is ons programma hebben. Er is data-invoer, deze data wordt door een aantal methodes bewerkt en uiteindelijk krijgen we het resultaat hiervan terug als data (hetzij als tekst, hetzij als figuren of een ander soort data). Structureel programmeren draait om het bewerken van data. De nadruk in structureel programmeren ligt dan ook op de bewerkingen en niet zozeer op de data zelf. Een structurele programmeertaal bestaat vooral uit functies en methodes.

Hoewel er bij Object georiënteerd programmeren ook sprake is van een begin- en eindpunt ligt hier niet de nadruk op de bewerkingen. Bij object georiënteerd programmeren gaan we elementen uit de echte wereld voorstellen in de code. We modelleren onze code zo dat we een virtuele kopie hebben van de echte wereld. Zo konden we eerder zien dat we een klasse “Persoon” declareerden. Deze persoon had, net zoals in het echte leven, eigenschappen zoals een naam en een voornaam. Verder kunnen we ook acties, oftewel methodes, aan deze klasse koppelen, zoals “slapen”, of “eten”.

Terwijl het bij structureel programmeren vooral over een top-down aanpak gaat, probeert OO (object oriented) meer een bottom-up aanpak te bewerkstelligen. Het doet dit door een virtuele wereld te creëren met objecten die interactie hebben met elkaar. Zo kunnen we naast een klasse “persoon” ook een klasse “bankrekening” hebben. De klasse persoon kan dan bepaalde bewerkingen doen op de klasse “bankrekening”.

Hoewel object georiënteerd programmeren voor sommigen aanvankelijk moeilijker lijkt dan structureel programmeren biedt het uiteindelijk een hele hoop voordelen ten opzichte van structureel programmeren. Je zal zien dat, wanneer je concept beet hebt, OO eenvoudiger te begrijpen is, aangezien we de echte wereld gaan modelleren in software.

Verder biedt OO volgende voordelen voor ontwikkeling:

  • Modulariteit: Elk object vormt een entiteit waarvan de werking volledig ontkoppeld is van de rest van het systeem.
  • Aanpasbaarheid: Kleine aanpassingen zijn eenvoudig door te voeren aangezien het systeem uit losse modules bestaat.
  • Uitbreidbaarheid: Nieuwe features toevoegen is eenvoudig door het gebruik van overerving of door het invoeren van nieuwe objecten.
  • Onderhoudbaarheid: Het onderhouden van de code is eenvoudiger aangezien elk object op zichzelf onderhouden kan worden.
  • Herbruikbaarheid: Objecten kunnen herbruikt worden in verschillende systemen. Zo kan bijvoorbeeld een object van het type persoon herbruikt worden in eender welke applicatie die met deze entiteit moet werken.

Principes object georiënteerd programmeren

Wanneer we met object georiënteerd programmeren bezig zijn zullen heel vaak 3 kernwoorden zien terugkomen. De echte betekenis van deze woorden, of hoe dit in code weergegeven wordt, is nu misschien niet duidelijk. Het is toch nuttig om even deze principes mee te geven zodanig dat je ze kan herkennen wanneer we ze tegen komen.

Encapsulatie

Met encapsulatie wordt bedoeld dat elk object zijn innerlijke werking afschermt van de buitenwereld. We kunnen dit vergelijken met de echte wereld. Wanneer wij een rekenmachine gebruiken zijn we ook niet geïnteresseerd in hoe de rekenmachine onze bewerking uitvoert, we wensen enkel het resultaat te kennen. Dit kunnen we toepassen op elk object dat we tegenkomen. Hoe het gaspedaal van onze auto ervoor zorgt dat wij sneller vooruitgaan is voor ons, tenzij je een garagist bent, niet belangrijk, we willen dat als we op het gaspedaal duwen de auto vooruitgaat.

Polymorfisme

Polymorfisme is een term die aanduid dat we gelijksoortige objecten, op dezelfde manier kunnen behandelen. Ook dit kunnen we terugkoppelen naar de echte wereld. Hoewel er honderden verschillende soorten auto’s zijn, weten we dat we kunnen rijden met een auto door op het rechtse pedaal te duwen en terug tot stilstand kunnen komen door op het middelste pedaal te duwen. Dit komt omdat alle autofabrikanten een afspraak hebben gemaakt over de plaats en functie van de pedalen. Ditzelfde principe gaan wij toepassen bij het programmeren in OO.

Overerving (inheritance)

Inheritance oftewel overerving zorgt ervoor dat wij objecten kunnen maken die de eigenschappen hebben van andere objecten maar net iets anders zijn. Weer kunnen we dit vergelijken met de echte wereld. We weten bijvoorbeeld dat alle dieren zich kunnen voortbewegen. Maar tegelijkertijd weten we dat slangen kruipen, terwijl honden zich voortbewegen op 4 poten. We zeggen dan dat zowel een slang als een hond overerft van het
type dier, oftewel een slang is een dier en een hond is een dier.

Klasses

Wat is een klasse?

Tot nu toe hebben we gesproken over klasses en objecten. Wat is nu eigenlijk een klasse en wat is een object? We kunnen, net zoals alles in OO, dit opnieuw terugkoppelen naar de echte wereld. Een klasse kunnen we zien als een blauwdruk, een bouwplan. Een object kunnen we zien als een reëel ding, gebouwd volgens een blauwdruk of een klasse. We nemen even een voorbeeld ter illustratie: In de echte wereld hebben we bijvoorbeeld een tekening gemaakt door een architect en een huis dat gebouwd werd volgens dat bouwplan. Het plan is dan een klasse, terwijl het huis het reële object is. Het is mogelijk om nog honderden huizen te bouwen volgens dat zelfde bouwplan.

Nog even een ander voorbeeld om aan het concept “klasse” en het concept “object” te wennen. Wanneer er een nieuw type auto op de markt komt, zal daarvoor eerst een plan voor moeten ontwikkeld worden. Dit plan noemen we de klasse. De effectieve auto’s noemen we de objecten. In het echte leven refereren we hier ook naar. Bijvoorbeeld: ik heb een auto van het type (klasse) golf. Het type golf is een algemeen bekend type, er zijn echter meerdere mensen die over zo’n auto (object) beschikken.

Een andere analogie kunnen we leggen met voorbeeld van de dieren. We weten dat er verschillende klasses van dieren zijn, zoogdieren, reptielen, amfibieën, … We weten ook dat elk dier tot een klasse behoort, een mens is een bijvoorbeeld een dier van de klasse zoogdieren.

Indien we nu dit concept overhevelen naar de code-wereld komen uit bij het concept van een klasse. We kunnen zelf klasses gaan definiëren en daarna objecten maken van onze zelf gedefinieerde klasse. In de praktijk zal je merken dat we veel van de concepten uit de echte wereld zullen gaan kopiëren naar een klassedefinitie, dit is uiteindelijk de bedoeling van het OO-programmeren, een virtuele wereld creëren met objecten die eigenschappen en gedrag hebben.

Een object, zowel in de echte wereld als in de programmeerwereld, is opgebouwd uit twee elementen:

  • Staat: Een object bevindt zich steeds in een bepaalde toestand. Deze toestand kan onveranderlijk zijn, of kan variëren naargelang de tijd. Een auto heeft bijvoorbeeld een bepaalde kleur, motorinhoud, … Dit zijn in principe onveranderlijke dingen. Verder heeft een auto ook veranderlijke staat, zoals zijn huidige snelheid, huidige inhoud van de benzinetank.
  • Gedrag: Een object heeft steeds een aantal dingen die het kan doen. Met een auto kan je bijvoorbeeld gas geven. Dit gedrag beïnvloedt de staat. Bij het gas geven bij een auto zal bijvoorbeeld de huidige snelheid hoger worden en de inhoud van de benzinetank kleiner maken.

Een goede oefening om het OO-concept te leren begrijpen is een aantal objecten uit de echte wereld te nemen en hiervan hun staat en gedrag proberen uit af te leiden.

We nemen een het voorbeeld van een hond om het concept nog verder te verduidelijken. Een hond heeft staat en gedrag. De staat waarin een hond zich bevindt bestaat uit verschillende eigenschappen: energie, honger, dorst, kleur, … Verder heeft een hond ook gedrag: lopen, eten, drinken, … Dit gedrag beïnvloedt de staat waarin de hond zich bevindt: Als een hond eet, krijgt hij minder honger, als hij drinkt, minder dorst.

Dit principe is toepasbaar op elk object dat je kan terugvinden in de echte wereld. Sommige objecten zullen meer staat dan gedrag vertonen, anderen vertonen enkel staat, nog anderen vertonen enkel gedrag.

Tot zover het principe achter klasses en objecten. We gaan nu bekijken hoe we deze principes en ideeën kunnen omzetten in code. We starten met de declaratie van een klasse, en het aanmaken van een object van die klasse, in .NET terminologie het instantiëren van een object.

Zoals we reeds eerder zagen kunnen we als volgt een klasse definiëren:

public class Persoon
{ 
}

Hier definiëren we een klasse die we “Persoon” noemen. Wanneer we nu objecten willen maken, in .NET-taal instanties van deze klasse willen maken gebruiken we volgende declaratie:

Persoon Erdem = default(Persoon); 
Erdem = new Persoon();

We kunnen bovenstaande syntax verkorten en in één regel schrijven:

Persoon Kenneth = new Persoon();

Hierna beschikken we over een variabele, genaamd “Erdem”, die wijst naar een plaats in het geheugen waar data is opgeslagen van het type “Persoon”. De klasse noemt persoon, Kenneth is een object van het type “Persoon”. Analogie met echte wereld: De klasse noemt auto, een golf is een object van het type “Auto”.

Fields

Een object bevat zoals gezegd staat en gedrag. Zonder deze twee elementen heeft een object geen bestaansreden. Om staat en gedrag toe te voegen aan een object, moeten we ervoor zorgen dat dit in het bouwplan opgenomen is. Net zoals we wanneer we een huis tekenen we ervoor moeten zorgen dat de afmetingen in het plan zijn opgenomen, moeten we zorgen dat als we staat gaan bepalen voor een object, deze staat gedefinieerd is in het bouwplan.

Om staat te bewaren in een object moeten we velden gaan definiëren in een klasse. Wanneer we een plan voor een auto tekenen moeten we gaan bepalen dat deze auto een benzinetank zal hebben, waarvan de inhoud zal uitgedrukt worden in liter. Wanneer we een persoon de eigenschap “naam” willen geven, zullen we in de definitie van de klasse moeten vertellen dat een persoon een naam kan hebben en dat deze naam zal uitgedrukt worden door middel van tekst. Analoog kunnen we de eigenschap “Leeftijd” definiëren, deze zal uitgedrukt worden in getallen.

Een definitie van veld in een klasse volgt de syntax van een variabele declaratie:

public class Persoon
 {
 //Een veld "naam" van het type string 
 public string Naam;
 //Een veld leeftijd van het type Integer 
 public int Leeftijd;
}

Wanneer je nu een instantie maken van de klasse persoon, zal je zien dat je van elk object deze eigenschap kan instellen.

Methods

Bij het declareren van velden hebben we gezien hoe we staat kunnen toevoegen aan een object. Het toevoegen van gedrag doen we door het declareren van methods. Methods zijn codeblokken die we kunnen uitvoeren via een object van een klasse. Er bestaan twee soorten methods: functions en voids.

Een Void is een methode die geen resultaat teruggeeft, een function is een method die een resultaat teruggeeft aan de aanroeper van de functie. Je kan dit vergelijken met de echte wereld. Wanneer we aan een persoon zijn geboortejaar vragen zal deze een antwoord geven. Wanneer een persoon echter verjaart, komt er geen antwoord maar wordt zijn leeftijd enkel met 1 opgehoogd.

 public class Persoon
 {
     //Een veld "naam" van het type string 
     public string Naam;
     //Een veld leeftijd van het type Integer 
     public int Leeftijd;
     //Een void die iets uitvoert zonder resultaat te 
geven 
     public void Verjaar()
     {
         Leeftijd = Leeftijd + 1;
     }
 }

Een function daarentegen zal ons een antwoord geven, dit antwoord kunnen we bepalen door in de function gebruik te maken van het keyword “return”. Hetgeen achter dit keyword staat zal het antwoord van de function vormen. Wanneer een function een antwoord geeft zal dit steeds een antwoord zijn van een bepaald type. Dit type moeten we dan ook benoemen bij de declaratie van een functie.
Een functie declaratie ziet er als volgt uit:

//Bereken het geboortejaar en geef dit als antwoord terug 
public int GeefGeboortejaar() 
{ 
    return Huidigjaar - Leeftijd; 
} 

Samengevat ziet onze klassedefinitie er momenteel als volgt uit:

public class Persoon
 {
     //Een veld "naam" van het type string 
     public string Naam;
     //Een veld leeftijd van het type Integer 
     public int Leeftijd;
     //Een void die iets uitvoert zonder resultaat 
te geven 
     public void Verjaar()
     {
         Leeftijd = Leeftijd + 1;
     }
     //Bereken het geboortejaar en geef dit als antwoord 
terug 
     public int GeefGeboortejaar()
     {
         return Huidigjaar - Leeftijd;
     }
 }

Wanneer je nu een instantie van deze klasse maakt zal je zien dat je niet enkel de staat kan uitlezen maar ook het gedrag kan aansturen.

Verder kunnen we bij methods ook parameters meesturen die het gedrag zullen beïnvloeden. Wanneer we de benzinetank van onze auto willen laten bijvullen, moeten we uiteraard ook specificeren hoeveel liter we graag zouden willen bijtanken.
Om een voorbeeld te hebben dat logisch is voegen we een veld en een methode toe aan onze klasse Persoon. Het veld stelt de positie voor waar de persoon zich nu bevindt (staat). We voegen een methode (gedrag) Wandel toe die de persoon laat bewegen (zijn positie verandert). Wanneer we de persoon laten wandelen, moeten we hem natuurlijk nog zeggen hoeveel meter hij moet wandelen. Dit doen we door een parameter mee te sturen bij de methode aanroep. We moeten in de klassedeclaratie dan wel vermelden dat deze methode een parameter verwacht. Dit doen door tussen de haakjes van de methode (zowel in een functie als in een void) de naam van de parameter en zijn type te plaatsen:

//het nieuwe veld positie 
public int Positie; 
//Een method met een parameter die de positie zal wijzigen 
public void Wandel(int AantalMeters) 
{ 
    Positie = Positie + AantalMeters; 
}

Access Identifiers / Modifiers

Wanneer we velden en methods definiëren hebben we tot nu toe steeds het keyword Public gebruikt. Public is slechts één van de vier mogelijkheden die we kunnen gebruiken om een declaratie van een methode of veld te doen. Dit keyword noemen we een access identifier. Een access identifier bepaalt welke andere objecten toegang hebben tot deze velden/methods. Volgende verschillende access identifiers zijn mogelijk:

publicpubliek bereikbaar
protected internalbereikbaar vanuit een afgeleide klasse en niet afgeleide klasse
protectedbereikbaar vanuit een afgeleide klasse
internalbereikbaar van een niet-afgeleide klasse
private protectedbereikbaar vanuit een afgeleide klasse binnen dezelfde assembly
privateenkel binnen de klasse bereikbaar

Properties

Met properties kunnen we een betere encapsulatie bekomen. We gaan dit doen door onze velden, die de interne staat bijhouden, af te schermen van de buitenwereld. Dit kunnen we doen door de access identifiers te gebruiken. Het idee achter properties is dat we onze velden private maken en de waardes via properties naar buiten stellen. Op deze manier kunnen we het naar buiten brengen en het veranderen van staat controleren. Wanneer we bij het voorbeeld met de velden op de klasse persoon de leeftijd gaan instellen van een object, hebben we geen controle over de instelling van deze leeftijd. Dit betekent dat een ander stuk code een ongeldige waarde voor de leeftijd kan ingeven (bijvoorbeeld een negatieve). Met properties kunnen we het instellen van de leeftijd controleren.

public int Leeftijd { 
 get { } 
 
 set { } 
 
} 

We zien in bovenstaande code een Get-gedeelte en een Set-gedeelte. Het Get-gedeeldte wordt uitgevoerd wanneer we de waarde van Leeftijd willen uitlezen. Dit gedeelte moet dan logischerwijze ook een antwoord geven aan de code die de waarde wil uitlezen. Net zoals bij functies kunnen we hiervoor het return-keyword gebruiken.
Het Set-gedeelte wordt uitgevoerd wanneer externe code de waarde wil veranderen. De waarde die geset moet worden, wordt meegegeven als parameter (value).

Natuurlijk moeten we deze twee codeblokken nog opgevuld worden. We doen dit door een private field (veld) te declareren en zijn waarde via deze twee codeblokken naar buiten te stellen:

private int _leeftijd; 
public int Leeftijd { 
 get { return _leeftijd; } 
 set { _leeftijd = value; } 
}

In bovenstaand codevoorbeeld declareren we eerst een veld (_leeftijd) van het type Integer. We maken dit veld private zodat het niet toegankelijk is voor externe code. We maken daarna een property (Leeftijd), eveneens van het type Integer. Wanneer we de leeftijd gaan ophalen (Get-blok), lezen we de private variabele _leeftijd uit en geven deze waarde terug. Wanneer de leeftijd veranderd wordt (Set-blok), slaan we de waarde (value) op in de private variabele _leeftijd. In dit geval levert dit natuurlijk niet direct een voordeel op, aangezien we nu enkel een indirectie voorzien naar het veld _leeftijd. Echter, wanneer we zouden willen verzekeren dat er nooit een negatieve leeftijd wordt ingesteld, dan zouden we deze controle op alle locaties moeten uitvoeren waar deze klasse gebruikt wordt. Met properties kunnen we deze controle in de interne werking van de klasse integreren. We doen dit als volgt:

private int _leeftijd; 
public int Leeftijd { 
    get { return _leeftijd; } 
    set { 
    if (value > 0) { 
       _leeftijd = value; 
    } 
    } 
}

In bovenstaande code is het Set-blok zo aangepast dat _leeftijd slechts aangepast wordt wanneer de meegegeven waarde groter is dan 0. Dit heeft tot gevolg dat alle externe code die deze klasse gebruikt of in de toekomst zal gebruiken, beschermd wordt van het instellen van een negatieve leeftijd. We zeggen in dit geval dat Properties de encapsulatie van logica in een klasse bevordert. Het is namelijk zo dat we de interne werking van een klasse verbergen van de buitenwereld, zonder dat dit tot verlies van functionaliteit leidt. Net zoals met methods en velden kunnen we access identifiers instellen voor properties. We kunnen bijvoorbeeld een property enkel beschikbaar maken voor andere klasses binnen dezelfde assembly (Friend), of via een van de gezien access identifiers. We vervangen hiertoe het keyword Public door de gepaste access identifier. We kunnen deze Access Identifiers ook instellen op het Get en/of Set-gedeelte van onze Property:

public int Leeftijd { 
 get { } 
 
 private set { } 
 
}

In bovenstaand voorbeeld zorgen we ervoor dat de property leeftijd overal mag uitgelezen worden, maar enkel binnen dezelfde klasse gewijzigd mag worden. Verder kunnen we een property ook ReadOnly of WriteOnly maken. Dit doen we door het keyword toe te voegen aan onze property declaratie. We krijgen dan enkel een Get of enkel een Set-gedeelte:

//Een property die enkel kan gelezen worden 
public int Leeftijd { 
    get { } 
} 
//Een property die enkel kan geschreven worden 
public int Leeftijd { 
    set { } 

}

We zien dus dat er 2 manieren zijn om een property te beschermen voor het schrijven:

  1. De property markeren met het keyword ReadOnly. Er is nu geen Set-gedeelte, dus de property kan niet gewijzigd worden
  2. Het Set-gedeelte markeren als private. Er is nu nog wel een Set-gedeelte, maar dit is enkel toegankelijk binnen de klasse

Over het algemeen wordt aangeraden om de tweede mehode te gebruiken. Dit geeft namelijk nog steeds de mogelijkheid om intern in de klasse bepaalde validaties op 1 plaats uit te voeren (in het Set-gedeelte) alvorens de waarde van de property effectief gewijzigd wordt. Naar de buitenwereld toe zijn beide methodes equivalent, binnen in de klasse biedt de tweede methode meer mogelijkheden.

Constructors

Zoals we gezien hebben bij de declaratie van een klasse, moeten we eerst objecten maken van een klasse alvorens we een functionaliteit kunnen gebruiken. We moeten immers ook eerst een auto laten bouwen alvorens we hiermee kunnen rijden.
Bij de constructie van object hebben we echter ook de mogelijkheid om in te grijpen. We doen dit via een speciale methode, genaamd de constructor. De constructor is een methode die aangeroepen wordt bij de creatie van een nieuw object. De constructor ziet er als volgt uit:

public class Persoon
{
 public Persoon()
 {
 }
}

De code die in deze constructor staat wordt meteen bij de object-creatie uitgevoerd. We kunnen deze methode gebruiken om een aantal standaardwaardes in te stellen voor het nieuwe object. We kunnen bijvoorbeeld elke nieuwe persoon een standaardnaam laten hebben en een standaardleeftijd. Dit doen we als volgt:

public New() 
{ 
 Naam = "Naamloos"; 
 Leeftijd = 18; 
}

Bij de instantiatie van een nieuw object van het type persoon zullen de velden nu ingesteld worden op “Naamloos” en 18.

Wanneer we zelf geen constructor definiëren, zal .NET automatisch een lege constructor aanmaken in de achtergrond.

Vermits constructors methods zijn, betekent dit dat we ook parameters kunnen meegeven aan constructors. We doen dit op dezelfde manier als bij normale methods. In het volgende voorbeeld worden de parameters StartNaam en StartLeeftijd gevraagd. We kunnen deze parameters dan gebruiken om de waardes van naam en leeftijd reeds van bij de constructie op te vullen.

public New(string StartNaam, int StartLeeftijd) 
{ 
 Naam = StartNaam; 
 Leeftijd = StartLeeftijd; 
}

Wanneer we nu een nieuwe object van het type persoon maken, is het natuurlijk noodzakelijk dat we deze parameters meesturen, om de constructie van het object de nodige informatie mee te geven. We doen dit door deze waardes aan de New-operator mee te geven:

Persoon Erdem = new Persoon("", 40);

Static members

Wanneer we methods, properties en velden declareren op een klasse zijn dit steeds instance-members, oftewel instantieleden. Dit betekent dat we enkel van deze functionaliteiten kunnen gebruik maken na de instantiatie van een object. Bij Static members gaan we dit gedrag veranderen.

Wanneer we het keyword Static plaatsen in een declaratie van een lid (veld, methode of property) zorgen we ervoor dat we deze functionaliteit rechtstreeks via de klasse kunnen aanroepen. Volgend voorbeeld maakt dit principe duidelijk:

public static int GeefTotaalAantalPersonen() 
{ 
 
}

We markeren hier de functie “GeefTotaalAantalPersonen” als een static-member. We kunnen nu deze methode aanroepen door de klassenaam te gebruiken, in plaats van via een object van de klasse:

Persoon.GeefTotaalAantalPersonen();

Partial Classes

Partial classes laten toe om een klasse-definitie in meerdere fields te splitsen. Visual Studio maakt van deze feature handig gebruik om automatisch gegenereerde code in aparte bestanden te plaatsen die verborgen worden voor de ontwikkelaar. Op deze manier is het niet mogelijk voor de ontwikkelaar om ongewilde wijzigingen in de gegenereerde code aan te brengen (nota: indien nodig is het wel mogelijk om hieraan aanpassingen aan te doen).
Merk op dat partial classes een feature zijn van Visual Studio en van de verschillende compilers in Visual Studio en niet van het .NET-framework. Als we naar de gegenereerde IL-code zouden gaan kijken zullen we nergens een referentie vinden naar het feit dat een klasse in meerdere bestanden gedefinieerd staat.
We kunnen bijvoorbeeld klasse Persoon in twee verschillende bestanden definiëren. Visual Studio zal deze definities samenvoegen tot één klasse.

public partial class Persoon
 {
 //Een veld "naam" van het type string 
 public string Naam;
 //Een veld leeftijd van het type Integer 
 private int _leeftijd;
}