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);

Les 3

Theorie: overerving en abstracte klassen

Opgaven WPO3 (overerving)

Overerving

In het voorgaande WPO hebben we gezien hoe we een klasse konden schrijven en op welke manier men deze tot een object kon instantiëren. In de oefeningen die daarop volgden werd er telkens gebruik gemaakt van 1 enkel type object. In grotere programma's kan het echter voorkomen dat men tijdens het schrijven van verschillende klassen opmerkt dat een aantal klassen gemeenschappelijke of gelijkwaardige code bevatten. Als deze klassen een gelijkwaardige functionaliteit implementeren, kan het nuttig zijn om deze gemeenschappelijke functionaliteiten onder te brengen in een gemeenschappelijke klasse en deze klasse over te erven door andere klassen die elk een specifiek deel implementeren. Als voorbeeld zullen we de klassen Bubble en Ball bespreken.

Deze 2 klassen hebben volgende eigenschappen:

  • x- en y-positie en,
  • een diameter d.

Deze eigenschappen zijn gemeenschappelijk voor beide klassen en kunnen ondergebracht worden in een gemeenschappelijke klasse: Sphere. De klasse Sphere kan er als volgt uitzien:

public class Sphere
{
	private double x;
	private double y;
	private double diameter;
	//------------------------------------------------------------
	public Sphere(double x, double y, double diameter)
	{
		this.x = x;
		this.y = y;
		this.diameter = diameter;		
	}
	//------------------------------------------------------------
	public double X
	{
		get {return x;}
		set {x = value;}
	}
	//------------------------------------------------------------
	public double Y
	{
		get {return y;}
		set {y = value;}
	}
	//------------------------------------------------------------
	public double Diameter
	{
		get {return diameter;}
		set {diameter = value;}
	}
}

Als bijzondere eigenschappen hebben de klassen Ball en Bubble respectievelijk: een gevulde schijf en en lege cirkel. Deze specialisaties worden via overerving geïmplementeerd.

public class Bubble: Sphere
{
	private Color contourcolor;
	//------------------------------------------------------------
	public Bubble(double x, double y, double diameter,Color clr):base(x,y,diameter)
	{
		this.contourcolor = clr;
	}
	//------------------------------------------------------------
	public Color ContourColor
	{
		get {return contourcolor;}
		set {contourcolor = value;}
	}
}
public class Ball: Sphere
{
	private Color fillcolor;
	//------------------------------------------------------------
	public Ball(double x, double y, double diameter,Color clr):base(x,y,diameter)
	{
		this.fillcolor = clr;
	}
	//------------------------------------------------------------
	public Color FillColor
	{
		get {return fillcolor;}
		set {fillcolor = value;}
	}
}

De overerving (overnemen van de functionaliteiten van Sphere) vindt plaats door de nieuwe klasse te schrijven, en de laten volgen door ":Sphere". Vanaf dat moment kan de kindklasse (Ball, Bubble) de eigenschappen van de superklasse (Sphere) gebruiken. Op dit moment vullen de kindklassen de superklasse met een kleur. In de verdere uitleg zullen beide kindklassen weldegelijk een specialiteit invullen.

Constructor

Wat opvalt is dat de constructor een speciale syntax heeft. Het eerste deel (voor de ":") is de constructor van de kindklasse zelf. Het tweede deel is chter de aanroep van de constructor van de superklasse. Merk op dat de argumenten onmiddelijk ingevuld kunnen worden. Als argumenten kan men enkel de argumenten meergeven van de constructor van de kindklasse, of constanten. Om de constructor van de superklasse aan te roepen volstaat het om de "base" te gebruiken met de gepaste argumenten. Indien de superklasse over meerdere constructoren beschikt, kan de meest geschikte constructor gebruikt worden.

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);

Les 4

Opgaven WPO4 (abstracte klassen)

Bijlagen weerstation

Voorbeeldapplicatie met werkende gauge

Bijlagen weerstation (Gauge control V2)

Voorbeeld applicatie met de nieuwe control

Abstracte klassen

Overerving zoals gezien in het eerste deel van dit WPO is handig omdat delen van een bestaande klasse overgenomen worden door de kindklassen. Een aantal extra functionaliteiten hoeven nog geprogrammeerd te worden. Het kan soms gebeuren dat men in de superklasse al weet waarvoor de klasse gaat dienen, maar men deze niet volledig kan afwerken. Men kan wel al een aantal functies definiëren maar men kan deze nog niet invullen. Andere functies zijn gemeenschappelijk voor alle kindklassen en kunnen wel al ingevuld worden. Om te voldoen aan zowel ingevulde als niet ingevulde methoden kan men gebruik maken van abstracte klassen.

Abstracte klassen worden grotendeels op dezelfde manier als een gewone klasse geschreven. Twee grote verschillen zijn echter op te merken:

  • de klasse wordt voorafgegaan door het woordje abstract
  • een aantal methoden zijn gedeclareerd maar worden niet ingevuld. Deze zijn bodyless

In onderstaand codefragment wordt het gebruik van abstracte klassen verduidelijkt.

abstract class shapeClass
{
	//-- we define the method without implementation
    abstract public int Area();
}
//---------------------------------------------
class rechthoek:shapeClass
{
	//-- must be filled with a function body
	public override int Area()
	{
		return 4*3;
	}
}
//---------------------------------------------
class cirkel:shapeClass
{
	//-- must be filled with a function body
	public override int Area()
	{
		return 2*Math.PI*3*3;
	}
}

Een abstracte klasse is per definitie een klasse waarin potentieel methoden gedeclareerd zijn waarin geen uitvoerbare code geschreven is. Hierdoor is deze klasse dus onvolledig en moet deze hoe dan ook overgeërfd worden ook al zijn alle functies wel degelijk uitgeschreven. De kindklassen kunnen ook abstract zijn. De enige klassen die geïnstantieerd kunnen worden zijn de gewone klassen die alle methoden uit de abstracte klasse(n) implementeren.

Les 5

Theorie: interfaces en polymorfie

Opgaven WPO5 (interfaces)

Opgaven WPO5 (polymorfie)

Bijlagen opgaven WPO 5

De concepten van overerving en abstracte klassen zullen tijdens dit WPO verder uitgebouwd worden. Bij de gewone overerving erft men alle eigenschappen en methoden over van de superklasse. Hierbij worden enkel nieuwe elementen bij elke kindklasse toegevoegd. Abstracte klassen laten echter toe om bepaalde methodes op voorhand te definiëren zonder deze in in te vullen. Deze klasse is dan ook niet volledig en kan in tegenstelling tot gewone superklassen niet tot een object geïnstantieerd worden. Men is dus verplicht om deze klasse over te erven en alle abstracte methoden te implementeren in een niet-abstracte klasse. Het verhaal van overerving en abstractie kan verder doorgetrokken worden. In dit WPO zullen we interfaces en polymorfie behandelen.

Interfaces

Interfaces zijn een nog strengere vorm van abstractie t.o.v. de abstracte klassen. Een abstracte klasse kan nog over een aantal ingevulde methoden beschikken. Een interface daarentegen is op dit vlak volledig abstract. Dit wil dus zeggen dat een interface enkel bestaat uit methode-prototypes en prototypes van properties. Een interface bevat dus geen enkel lijntje uitvoerbare code. Hierdoor zijn alle gedeclareerde methoden/properties altijd public. Er kan immers geen code van de interface (die niet bestaat) aan de inwendige delen van een interface aan. Bij het schrijven van een interface is het sleutelwoordje public dus volledig overbodig. Als conventie laten we een interface steeds met "I" (hoofdletter i) voorafgaan. Hieronder wordt een voorbeeld gegeven.

interface IPoint
{
	void setPoint(double x, double y);
	double X {get; set;}
	double Y {get; set;}	
}

Het implementeren van de interface wordt hieronder weergegeven.

public class point:IPoint
{
	private double x_;
	private double y_;
	//---
	public point(double x, double y)
	{
		setPoint(x,y);
	}
	// implementation mandatory
	void setPoint(double x, double y)
	{
		this.x_=x;
		this.y_=y;
	}
	// implementation mandatory
	double X 
	{
		get {return x_;}
		set {this.x_ = value;}
	}
	double Y
	{
		get {return y_;}
		set {this.y_ = value;}
	}	
}

Een voorbeeld op interfaces is de IList. Van deze interface wordt de klasse List geïmplementeerd. Interfaces kunnen gezien worden als een contract tussen verschillende programmadelen waarbij verschillende programmeurs weten welke methoden en/of properties door een bepaalde klasse geïmplenteerd zullen worden.

Meervoudige overerving

Tijdens de voorgaande WPO's hebben we gezien dat een klasse van maximaal 1 andere (abstracte) klasse kan overerven. Een klasse kan echter van 1 of meerdere interfaces overerven. Ook is het mogelijk dat een klasse van zowel een interface als een (abstracte) klasse kan overerven. In het geval van meervoudige overerving moeten alle niet ingevulde methoden en/of properties ingevuld worden door de kindklasse. Dit wordt hieronder getoond. Meervoudige overerving wordt gescheiden door een komma.

public class point:apoint,IPoint
{
	// write code for the class here 
	
	// implement abstract and interface methods here
}

Polymorfie

Een van de krachtigste elementen in objectgeoriënteerd programmeren is het gebruik van polymorfie. Er wordt ook gezegd dat objectgeoriënteerd programmeren pas echt tot zijn recht komt indien men aan polymorfie doet.

In het Grieks betekent polymorfie: veelvormigheid. Hiermee wordt in de informatica bedoeld dat men veel klassen kan hebben, die dezelfde methodes implementeren (wat betreft naam en argumenten) maar met een verschillende code.

Men kan aan polymorfie doen door een superklasse te definiëren en die te laten overerven door een aantal kindklassen. Men declareert alle kindklassen als het type van de superklasse, maar instantieert ze als een van de kindklassen. In dat geval worden alle objecten van het zelfde type beschouwd. Een voorbeeld wordt hieronder weergegeven. Hierbij hernemen we het voorbeeld van de shapes.

abstract class shapeClass
{
	//-- we define the method without implementation
    abstract public double Area();
}
//---------------------------------------------
class rechthoek:shapeClass
{
	//-- must be filled with a function body
	public override int Area()
	{
		return Height*Width;
	}
	//-----------------------------------------
	public double Height
	{
		get; set;
	}
	//-----------------------------------------
	public double Width
	{
		get; set;
	}
}
//---------------------------------------------
class cirkel:shapeClass
{
	//-- must be filled with a function body
	public override int Area()
	{
		return 2*Math.PI*Diameter*Diameter;
	}
	public double Diameter
	{
		get; set;
	}
}

Declareren als de gemeenschappelijke superklasse is altijd mogelijk, ook als de superklasse abstract of een interface is. Enkel het instantiëren moet volgens een volledige klasse verlopen.

// declare 2 object as shapeClass, but instantiate from one of the subclasses.
shapeClass rect = new rechthoek();
shapeClass circle = new cirkel();
// we can nog take the method Area();
double a_rect = rect.Area();
double a_circ = circle.Area();

In bovenstaande code is het dus mogelijk om telkens de methode Area op te roepen. Dit is mogelijk omdat de superklasse over deze methode beschikt. Het is dus een gekende methode voor shapeClass en alle kindklassen. Doordat alle klassen zich nu als shapeClass laten voordoen, verbergen deze als het ware de extra informatie die beschikbaar is in de kindklassen. In bovenstaande code is het aanroepen van de properties Height, Width en Diameter niet mogelijk. Dit is zo omdat de objecten volgens de superklasse gedeclareerd zijn waar deze properties niet bestaan. Bovenstaande constructie biedt als voordeel dat men vanaf nu alle objecten die bv. een tekenmethode overerven van een gemeesnchappelijke superklasse in eenzelfde lijst bijgehouden kunnen worden.

Het sleutelwoord "is"

Door het declareren van alle objecten als de superklasse verliest men de informatie van de specifieke implementaties (kindklassen). Hierdoor is het niet meer mogelijk om die informatie rechtreeks aan te spreken. Men kan dit echter wel door de objecten te casten naar een specifieke klasse. Echter loopt men hierbij het risico dat de objecten voor een verkeerde type-cast kunnen zorgen. Dit probleem kan verholpen worden door de objecten te testen op type vooraleer ze te casten. Dit testen kan door gebruik te maken van het sleutelwoordje "is". Dit principe wordt hieronder weergegeven.

// make a list of shapes
List<shapeClass> l = new List<shapeClass>();
l.Add(new rechthoek());
l.Add(new cirkel());

// set height, width or diameter depending on the class 
for (int i=0;inew cirkel();l.Count;i++)
{
	if (l[i] is rechthoek) // first check type with "is"
	{
		// typecast
		rechthoek r = (rechthoek)l[i];
		// now we can access the features of rechthoek
		r.Width = 10;	
		r.Height = 15;
	}
	else if (l[i] is cirkel)
	{
		// typecast
		cirkel c = (cirkel)l[i];
		// now we can access the features of cirkel
		c.Diameter = 5;
	}
}
// calculate the area for each class
for (int i=0;inew cirkel();l.Count;i++)
{
	double a = l[i].Area();
}