Objectgeoriënteerd programmeren

An Braeken - an.braeken@vub.be

Laurent Segers - laurent.segers@vub.be

Een speciale dank gaat naar Benjamin Lapauw, Jonas Verbeke, Mathias Knop en Dugagjin Lashi voor het nalezen van de oefeningen en de korte samenvattingen op deze website. Tevens worden ze ook bedankt voor het meehelpen tijdens het lesgeven.

Tutorial Visual Studio

Hieronder vinden jullie de oefeningen voor de WPO's objectgeoriënteerd programmeren. De oefeningen zijn onderverdeeld in 3 categorieën: A, E en X. A zijn de algemene basisoefeningen en zijn het minimumniveau. De oefeningen van de E reeks zijn het niveau dat verwacht wordt. De X-oefeningen (Xtreme) zijn voor de durvers.

Tijdens de oenfeningensessies wordt er van je verwacht dat je actief meewerkt. Is dit niet het geval en verstoor je de groep of kom je te laat binnen, dan kan de assistent je de toegang tot die sessies weigeren.

Oefening baart kunst! Iedereen kan leren programmeren, maar je leert het alleen door veel te oefenen!

Weerstation

Om je de kans te geven je programmeervaardigheden te demonstreren in een concrete toepassing, programmeer je als zelfstandige opdracht de software voor een weerstation. Dit doe je op basis van bestaande open source hardware die op de VUB werd ontwikkeld, WeatherStation v3.0. Je kan zo'n weerstation komen ontlenen in het Elektronicalab bij de aanvang van het tweede semester.

Het volledige hardwaredesign vind je op CircuitMaker.

De software (firmware en bootloader) staan op Github, en gedocumenteerd op de OpenObservatory website.

In de opdrachtbeschijving vind je een Nederlandstalige samenvatting van deze resources, en de doelstellingen van het project.

Het weerstation staat op 30% van de eindscore van dit vak. De andere punten staan op het WPO-examen (40%) en theorie-examen(30%).

Alternative opdracht aan het weerstation

Een alternatieve opdracht aan het weerstation bestaat er in om het concept van gauges e.d. toe te passen bij het VUB-racing team. Voor de volledige opdrachtbeschrijving wordt er naar onderstaande presentatie verwezen: presentatie.

Les 1

Theorie: herhaling 1ste bachelor

Opgaven WPO1

In dit eerste WPO herhalen we de leerstof Informatica van het eerste jaar. Voor meer oefeningen wordt verwezen naar de pagina van het eerste jaar. Door deze oefeningen op te lossen bereid je jezelf goed voor op de oefeningen van object georiëteerd programmeren. Hierdoor krijg je de syntax terug goed onder de knie.

Les 2

Theorie: schrijven van een eigen klasse

Opgaven WPO2

Klasse schrijven

In dit WPO wordt de basis gelegd van object georiënteerd programmeren. De eerste stappen richting het schrijven van klassen en het gebruik van hun instanties zal hier ook aan bod komen. In de OOP wereld worden de termen "klasse" en "object" vaak door elkaar gebruikt. Vaak worden deze foutief als synoniemen beschouwd. Het grote verschil tussen beide termen is dat een klasse de code (blauwdruk) zelf voorstelt terwijl het object de instantie van de klasse is. In wat volgt zullen we werken met de klasse "dataContainer" waarin we een getal in zullen bijhouden. In de meest eenvoudige vorm wordt de klasse geschreven als de klassedefinitie tesamen met interne variabele (veld). Dit wordt in onderstaand codefragment weergegeven.

// start the class by writing this first line-height
// each class starts with public (visiable in complete programm) 
// or private (only visible in sections of the programm)
public class dataContainer
{
	// declare fields of the class
	int data;
}

De klasse dataContainer bevat nu 1 veld genaamd "data" van het type integer. In de OOP wereld geldt echter als regel dat men interne velden het liefst van de "buitenwereld" afschermt. T.t.z. dat men niet wilt dat een veld door een andere code dan het object zelf aangepast kan worden. Om dit te doen zal men de velden dan ook als "private" declareren. Bovenstaand codefragment wordt dus:

// start the class by writing this first line-height
// each class starts with public (visiable in complete programm) 
// or private (only visible in sections of the programm)
public class dataContainer
{
	// declare fields of the class (private)
	private int data;
}

Getters en setters, properties

Het programma kan nu echter niet meer aan dit veld aan (zowel lezen en schrijven). Om dit te verhelpen moet de klassen een manier toelaten om dit toch te kunnen doen. Dit kan gebeuren a.d.h.v. getters/setters en/of properties. Door gebruik te maken van deze techniek kan men er zich van verzekeren dat de interne velden altijd een juiste inhoud bevatten. Veronderstel dat men wilt dat het veld beperkt blijft tussen -100 en 100, dan kan men dit realiseren door een eenvoudige if-conditie. Dit wordt in onderstaande codefragment geïllustreerd.

public class dataContainer
{
	// declare fields of the class (private)
	private int data;
	// getter 
	public int getData()
	{
		return data;
	}
	// setters
	public void setData(int value)
	{
		if ((value>=-100)&&(value<=100)) data = value;
	}
	// equivalent for getters and setters: properties
	public int Data
	{
		get{return data;} // if returning is allowed, programm "get"
		set // if setting is allowed, programm the "set"
		{
			if ((value>=-100)&&(value<=100)) data = value;
		}
	}
}

Het gebruik van getters en setters naast properties wordt zelden gedaan aangezien deze beide hetzelfde werk verrichten. Meestal wordt om het wijzigen van velden enkel naar de properties gegrepen. Bij de properties geldt dat ze pas getten en setten wanneer de get en set directieven geprogrammeerd zijn. Indien de get of set ontbreekt, zal maar 1 van de 2 delen functioneel zijn. Merk op dat de waarde van de setter zich altijd in "value" bevindt. In de regel geldt dat als men een property en/of getters en setters publiekelijk toegankelijk wil maken naar de rest van het programma dat men ze als "public" moet declareren. In wat volgt zullen we dezelfde klasse gebruiken, maar enkel met de bovenvermelde property.

public class dataContainer
{
	// declare fields of the class (private)
	private int data;
	// property for data (get and set)
	public int Data
	{
		get{return data;} // if returning is allowed, programm "get"
		set // if setting is allowed, programm the "set"
		{
			if ((value>=-100)&&(value<=100)) data = value;
		}
	}
}

Object aanmaken en gebruiken

De klasse zoals hierboven kan al geïnstantieerd worden. Dit instantiëren gebeurt doorgaans buiten de klasse zelf in de rest van het programma.

// create object datacontainer1 of class dataContainer
dataContainer datacontainer1 = new dataContainer();

Het object datacontainer1 is hierbij de instantie van de klasse dataContainer. Vereenvoudigd kan men zeggen dat deze regel overeenkomt met: definieer een variabele (datacontainer1 ) van het type dataContainer (klasse) en maak hem aan (new). Eenmaal het object aangemaakt is, kan men de properties en methoden (enkel de publieke) ervan aanspreken. Dit kan men als volgt doen:

dataContainer datacontainer1 = new dataContainer();
// put value 5 in the property
datacontainer1.Data = 5;  // is allowed since we have a setter
// read the value out of the property
int val = datacontainer1.Data; // is allowed since we have a getter

Indien men de waarde 512 (i.p.v. 5) in de property zou willen introduceren, levert dit niet het gewenste resultaat op. Waarom?

Methoden

Klassen bevatten naast getters, setters, velden en properties ook methoden die toelaten om de toestand van het object te veranderen. Methoden werken net zoals functies en procedures. Het grote verschil is echter dat deze vaak onmiddelijk effect hebben op het huidige object. In wat volgt wordt een methode geschreven die toelaat om het veld te vermenigvuldigen met een bepaalde waarde die als argument opgegeven wordt. Uiteraard wordt het resultaat opnieuw nagekeken op de opgelegde grenzen. Deze methode wordt uiteraard ook als public gedeclareerd zodat men er van buitenaf aan kan.

public class dataContainer
{
	// declare fields of the class (private)
	private int data;
	// property for data (get and set)
	public int Data
	{
		get{return data;} // if returning is allowed, programm "get"
		set // if setting is allowed, programm the "set"
		{
			if ((value>=-100)&&(value<=100)) data = value;
		}
	}
	// multiply value with another value in a method + do check if >=-100 and <=100
	public void multiply(int mul)
	{
		data = data * mul;
		if (data<-100) data = -100;
		if (data>100) data = 100; 
	}
}

Deze methode kan gebruikt worden zoals weergegeven in onderstaand codefragment.

dataContainer datacontainer1 = new dataContainer();
// put value 5 in the property
datacontainer1.Data = 5; 
// read the value out of the property
datacontainer1.multiply(5);
int val = datacontainer1.Data; // returns 25

Net zoals functies kunnen methoden een willekeurig aantal argumenten opnemen en een return statement hebben.

Bovenstaande klasse maakt gebruik van 2 gelijkaardige stukken code: het vergelijken van de waarde op de opgelegde grenzen. Dit laatst kan gecentraliseerd worden in een aparte (private) methode.

public class dataContainer
{
	// declare fields of the class (private)
	private int data;
	// property for data (get and set)
	public int Data
	{
		get{return data;} // if returning is allowed, programm "get"
		set // if setting is allowed, programm the "set"
		{
			data = value;
			checkBorders();
		}
	}
	// multiply value with another value in a method + do check if >=-100 and <=100
	public void multiply(int mul)
	{
		data = data * mul;
		checkBorders();
	}
	// private method to check validity of the value
	private void checkBorders()
	{
		if (data<-100) data = -100;
		if (data>100) data = 100; 
	}
}

Dit heeft als voordeel dat men een stukje code slechts eenmaal hoeft te schrijven en men er zeker van is dat deze werkt. Bovendien hoeven we deze nieuwe methode gewoon op te roepen elders in de klasse zelf. Deze methode is echter niet van buitenaf het object beschikbaar.

Constructor

Elke klasse dat geschreven en tot object geïnstantieerd wordt, maakt gebruik van een constructor. Dit geldt ook voor bovenstaande voorbeelden. Een constructor hoeft niet gespecifieerd te worden in de klasse indien er bij het aanmaken van het object geen speciale acties ondernomen hoeven te worden. Indien men wel zelf een constructor schrijft, dan moet deze aan een aantal regels voldoen:

  • de constructor heeft dezelfde naam als de naam van de klasse,
  • een constructor heeft minstens 0 argumenten, maar kan ook een willekeurig aantal argumenten hebben,
  • een constructor retourneert niets (geen return), bijgevolgd wordt een constructor nooit voorafgegaan door void, int, of iets dergelijks.

De constructor van de bovenvermelde klasse zou echter een getal kunnen opnemen zodat het bijpassende veld onmiddelijk ingevuld wordt.

public class dataContainer
{
	// declare fields of the class (private)
	private int data;
	// constructor (also public)
	public dataContainer(int val)
	{
		data = val;
		checkBorders();
	}
	// property for data (get and set)
	public int Data
	{
		get{return data;} // if returning is allowed, programm "get"
		set // if setting is allowed, programm the "set"
		{
			data = value;
			checkBorders();
		}
	}
	// multiply value with another value in a method + do check if >=-100 and <=100
	public void multiply(int mul)
	{
		data = data * mul;
		checkBorders();
	}
	// private method to check validity of the value
	private void checkBorders()
	{
		if (data<-100) data = -100;
		if (data>100) data = 100; 
	}
}

De constructor die nu gebruikt wordt vult het getal onmiddelijk in voert ook de nodige check uit. Merk op dat zolang er geen constructor geschreven wordt in de klasse, men altijd terugvalt op de standaard constructor. Eenmaal men er zelf schrijft moet men een geschreven constructor gebruiken. De standaard constructor komt in dat geval te vervallen. Het aanmaken van het object verschilt hierdoor t.o.v. vroeger.

dataContainer datacontainer1 = new dataContainer(5);
// read the value out of the property
datacontainer1.multiply(5);
int val = datacontainer1.Data; // returns 25

Het is ook mogelijk om 2 of meer constructoren in een klasse te schrijven. In dat geval kan men tijdens het aanmaken van een object de meest geschikte constructor gebruiken.

public class dataContainer
{
	// declare fields of the class (private)
	private int data;
	// constructor (also public)
	public dataContainer()
	{
		// do something here
	}
	public dataContainer(int val)
	{
		data = val;
		checkBorders();
	}
	// property for data (get and set)
	public int Data
	{
		get{return data;} // if returning is allowed, programm "get"
		set // if setting is allowed, programm the "set"
		{
			data = value;
			checkBorders();
		}
	}
	// multiply value with another value in a method + do check if >=-100 and <=100
	public void multiply(int mul)
	{
		data = data * mul;
		checkBorders();
	}
	// private method to check validity of the value
	private void checkBorders()
	{
		if (data<-100) data = -100;
		if (data>100) data = 100; 
	}
}
// both constructors are now valid
dataContainer datacontainer1 = new dataContainer(5);
dataContainer datacontainer2 = new dataContainer();
// read the value out of the property
datacontainer1.multiply(5);
int val = datacontainer1.Data; // returns 25

Reference to object

Een neveneffect van objecten te gebruiken is dat deze via een references gebruikt worden. Een reference kan bekeken worden als een verwijzing naar het object. Dit betekent dus dat we nooit een object rechtstreeks benaderen, maar via de verwijzing. Dit effect is het meest zichtbaar wanneer men een object doorgeeft aan een andere variabele.

// both constructors are now valid
dataContainer datacontainer1 = new dataContainer(5);
dataContainer datacontainer2;
// copy the reference into datacontainer2
datacontainer2 = datacontainer1;
// read the value out of the property
datacontainer2.multiply(5);
// returns 25, even if datacontainer1 was no used for the multiplication
int val = datacontainer1.Data; 

Zelfs al gebruiken we het object niet via zijn originele variabele, kan het gebeuren dat de toestand van het object gewijzigd kan worden. Hiermee dient ten alle tijde rekening gehouden mee te worden. Wenst men een kopie te maken van het origineel object, dan is het best om een nieuw object aan te maken waarin alle velden worden gekopieerd. Hiervoor kan men best een methode "clone" aanmaken in de klasse zelf.

Static methoden

Naast de gewone methoden bestaan er ook de "static" methoden. Dit type van methode verschilt in programmatie en gebruik t.o.v. de gewone methoden. Static methoden behoren wel tot de klasse, maar maken geen deel uit van de objecten die uit deze klasse voortvloeien. Het is dus niet mogelijk om bij een object dergelijke methodes aan te roepen. Juist doordat deze methoden geen deel uitmaken van objecten, kunnen deze methoden ook geen invloed hebben hebben op de toestand van het huidige object. Dit type van methoden wordt voornamelijk gebruikt wanneer men functies schrijft die geen directe link kunnen vertonen met een bepaald object. Denk hierbij aan de klasse "Math" waarin een aantal methoden geïmplementeerd zijn zoals sinus, cosinus, tangens, enz. Static methoden worden altijd voorafgegaan door het sleutelwoord "static". Hieronder wordt een voorbeeld gegeven van een static methode en het gebruik ervan.

public class dataContainer
{
	// other code 
	public static dataContainer clone(dataContainer obj)
	{
		dataContainer ret = new dataContainer();
		ret.data = obj.data;
		return ret;
	}
}

een mooi voorbeeld van een static methode is de methode clone. Deze methode moet nu een argument openemen van het type "dataContainer" omdat deze methode niet op het object zelf werkt. De methode retourneert een kopie van het object. Het aanroepen van deze methode wordt hieronder geïllustreerd.

dataContainer datacontainer1 = new dataContainer(5);
dataContainer datacontainer2;
// call the static method from the class itself!!
datacontainer2 = dataContainer.clone(datacontainer1);

Private, protected en public

Tot nu toe zijn de methoden en velden enkel als private of als public gedeclareerd. Naast public en private bestaat er ook "protected". De verschillende vormen van zichtbaaarheid worden hieronder opgesomd:

  • Private: enkel zichtbaar binnen de eigen klasse. Niet zichtbaar buiten de klasse en ook niet zichtbaar binnen kindklassen.
  • Protected: enkel zichtbaar binnen de eigen klasse en binnen de kindklassen.
  • Public: zichtbaar voor iedereen.

Public betekent dus zichtbaar voor iedereen. Private bevindt zich aan het andere eind van het spectrum: enkel zichtbaar voor de klasse zelf. Protected is een mildere vorm dan private en bevindt zich tussenbeide. Dit laatste kan handig zijn in beide kindklassen. Door gebruik te maken van enkel de public en private zijn beide kindklassen verplicht van de properties van de superklasse aan te spreken wat betreft positie en diameter. Dit kan vermeden worden door de velden in de superklasse als protected te declareren.

public class Sphere
{
	protected double x;
	protected double y;
	protected double diameter;
	//------------------------------------------------------------
	//.... other stuff here
}

In de kindklasse kunnen de velden als volgt geraadpleegd worden:

public class Ball: Sphere
{
	private Color fillcolor;
	// generic method
	public void doSomething()
	{
		// x can be accessed now.
		double a = x;
	}
}

This en base

Soms is het onduidelijk welk veld/methode tot de kindklasse of van de superklasse behoort. Om die reden wordt er vaak naar de sleutelwoorden "this" en "base" gegrepen. Bij gebruik van this bedoelt men dat men de velden/methoden van de eigen klasse wilt aanspreken. Bij gebruik van base wilt men de velden/methoden van de superklasse aanspreken. Het gebruik hiervan wordt hieronder weergegeven.

public class Ball: Sphere
{
	private Color fillcolor;
	// generic method
	public void doSomething()
	{
		// x can be accessed now.
		double a = base.x;
		Color clr = this.fillcolor;
	}
}

Virtual en override

Het kan voorkomen dat men bij het schrijven van een kindklasse een bestaande methode wil overschrijven. Dit is bv het geval met de methode "ToString()". Deze methode wordt van de superklasse "object" overgeërfd en bevat een generische invulling. Om hier een beter invulling aan te geven volstaat het deze methode over te schrijven (override) door dezelfde methode in de eigen klasse. Dit wordt hieronder getoond.

public class Ball: Sphere
{
	public override void ToString()
	{
		return "This is a ball";
	}
}

Dit concept kan verder doorgetrokken worden door zelf methoden te schrijven die door de kindklassen overschreven kunnen worden. Deze methode moet echter wel als "virtual" gedeclareerd worden. Dit principe worddt hieronder gedemonstreerd.

public class Sphere
{
	// declare as virtual 
	public virtual void Draw(Canvas cvs)
	{		
		Ellipse el = new Ellipse();		
		el.Stroke = new SolidColorBrush(Colors.Black);
		el.StrokeThickness = 5;		
		el.Width = this.diameter;
		el.Height = this.diameter;		
		Canvas.SetTop(el, this.y);
		Canvas.SetLeft(el, this.x);
	}
}
public class Ball: Sphere
{
	// override old method from the superclass 
	public override void Draw(Canvas cvs)
	{
		Ellipse el = new Ellipse();		
		el.Fill = new SolidColorBrush(this.fillcolor);		
		el.Width = base.diameter;
		el.Height = base.diameter;		
		Canvas.SetTop(el, base.y);
		Canvas.SetLeft(el, base.x);
	}
}

Merk hierbij op dat de methode van de superklasse en de kindklasse identiek hetzelfde moet zijn (return en argumenten ook) om te kunnen overriden. Zijn ze niet gelijk, dan is een override niet nodig. Merk op dat overriden niet altijd gewenst is. Denk bij het schrijven van de superklasse hierbij altijd goed na of een override noodzakelijk kan zijn. Deze methoden wordedn doorgaans als public aangegeven.

Instantiëren tot object

Hierboven is aangehaald hoe men een klasee kan overerven, methoden kan overriden en de constructors kan doorlinken. In wat volgt als het gebruik ervan volgen om objecten aan te maken. Het aanmaken van dergelijk objecten vindt op dezelfde wijze plaats als voor gewone objecten. Hieronder wordt dit verduidelijkt.

Sphere s = new Sphere(0,0,25);
Sphere s2 = new Bubble(0,0,25,Colors.Red);
Bubble b = new Bubble(0,0,25,Colors.Red);
Ball b2 = new Ball(10,10,30,Colors.Blue);

Hierbij valt op dat je een object kan declareren van de superklasse, maar ook kan instantiëren a.d.h.v. een kindklasse. De regel hierbij is dat de instantie steeds minstens even gespecialiseerd is als de klasse zelf. Meer gespecialiseerd mag, minder mag niet. Dus onderstaande kan niet:

// invalid code below!!!
Bubble b = new Sphere(0,0,25);