Mapmaker “all-in-one-project”
Dit project toont de belangrijkste OO-concepten samen in een applicatie waarmee we een plattegrond van een huis visualiseren. Het doel is zoveel mogelijk elementen van de voorbije hoofdstukken (overerving, abstracte klassen, polymorfisme, compositie, interfaces en is/as) te integreren tot een groter werkend geheel. Elke klasse en interface komt in een eigen bestand.
Console.SetCursorPosition(x, y) zet de cursor op kolom x en lijn y van de console, met (0,0) linksboven. Wat je daarna schrijft, verschijnt op die plek.
Abstracte klasse
Eerst maken we een kleine hulpklasse Point die een punt in de ruimte voorstelt. We kunnen ze ook gebruiken om een verschuiving (een vector) voor te stellen:
class Point
{
public Point(int x, int y)
{
X = x;
Y = y;
}
public int X { get; set; }
public int Y { get; set; }
}Dan maken we een abstracte klasse MapObject. Daarvan erven straks alle voorwerpen die op de kaart kunnen staan.
abstract class MapObject
{
private Point location;
public MapObject(Point location, char drawChar, double price)
{
Location = location;
DrawChar = drawChar;
Price = price;
}
public Point Location
{
get { return location; }
set { location = value; }
}
public char DrawChar { get; set; }
public double Price { get; set; }
// Teken het object in de console
public abstract void Paint();
}Price bevat de prijs van het object, zodat we vlot kunnen berekenen wat onze kaart in totaal kost. Location bevat de coördinaten (x,y) waar het object in de console getekend wordt. DrawChar zegt met welk teken het object getekend wordt. Location is een full property: verderop hebben we die set nog nodig.
Overerving
We maken MapObject bewust abstract: zo kan niemand rechtstreeks een MapObject aanmaken. Je maakt altijd een object van een klasse die ervan erft.
Een eerste klasse die van MapObject erft:
class WallElement : MapObject
{
public WallElement() : base(new Point(1, 1), '*', 10)
{
}
public WallElement(Point location, char drawChar, double price) : base(location, drawChar, price)
{
}
public override void Paint()
{
Console.SetCursorPosition(Location.X, Location.Y);
Console.Write(DrawChar);
}
}De methode Paint moeten we verplicht overriden, want ze is abstract in de basisklasse. De default constructor zet een muurtje met een sterretje op positie (1,1).
Elementen op het scherm
In Main kunnen we nu al elementen op het scherm brengen:
WallElement steen1 = new WallElement();
steen1.Paint();Dat geeft een sterretje op positie (1,1) op het scherm.
We kunnen nu meerdere stenen plaatsen (met een andere prijs, naargelang de soort) en de totaalprijs opvragen.
Grotere objecten
We hebben nu een basis om andere zaken te maken. Stel dat we grotere objecten willen. We spreken af dat Location het punt linksboven van het object is. Volgende klasse erft van MapObject en tekent een groter vierkant (maar je kan je fantasie de vrije loop laten):
class FurnitureElement : MapObject
{
private int unitSize;
public FurnitureElement(Point location, char drawChar, double price, int unitSize) : base(location, drawChar, price)
{
UnitSize = unitSize;
}
public int UnitSize
{
get { return unitSize; }
set
{
if (value > 0)
{
unitSize = value;
}
}
}
public override void Paint()
{
for (int i = Location.X; i < Location.X + UnitSize; i++)
{
for (int j = Location.Y; j < Location.Y + UnitSize; j++)
{
if (i < Console.WindowWidth && j < Console.WindowHeight)
{
Console.SetCursorPosition(i, j);
Console.Write(DrawChar);
}
}
}
}
}Nu kunnen we eenvoudig allerlei meubels maken, zoals een zetel:
class ZetelElement : FurnitureElement
{
public ZetelElement(Point location, int unitSize) : base(location, '+', 100, unitSize)
{
}
}Wil je de zetel anders tekenen (geen vierkant, maar iets dat meer op een zetel trekt), dan voeg je in ZetelElement nog een eigen Paint toe:
public override void Paint()
{
//Code om een mooiere zetel op het scherm te tonen
}Polymorfisme
Met een List kunnen we nu ongelooflijk veel objecten op het scherm tonen:
List<MapObject> allObjects = new List<MapObject>(); //lang leve polymorfisme
//Muurtje
for (int i = 0; i < 10; i++)
{
Point tempLoc = new Point(2 + i, 3);
WallElement tempWall = new WallElement(tempLoc, '=', 10.0);
allObjects.Add(tempWall);
}
//Zetel
allObjects.Add(new ZetelElement(new Point(6, 8), 3));
//Teken alle objecten
foreach (MapObject item in allObjects)
{
item.Paint();
}Dankzij polymorfisme kunnen we alle objecten die van MapObject erven in dezelfde lijst zetten. Roepen we Paint op, dan draait de versie van het object zelf.

Compositie
We kunnen nu nog meer klassen maken (tafels, stoelen, deuren …) en een klasse die er een paar samen bevat, bijvoorbeeld een volledig salon:
class SalonElement : MapObject
{
private List<MapObject> elementen = new List<MapObject>();
public SalonElement(Point salonLoc) : base(salonLoc, ' ', 0)
{
elementen.Add(new ZetelElement(new Point(2, 2), 3));
elementen.Add(new ZetelElement(new Point(5, 9), 3));
}
public override void Paint()
{
foreach (MapObject element in elementen)
{
element.Paint();
}
}
}De locatie van het salon is het punt linksboven, en de locaties van de zetels zijn bedoeld ten opzichte van dat punt. Althans, dat willen we. Schrijven we in Main:
SalonElement salon1 = new SalonElement(new Point(6, 5));
salon1.Paint();
dan verschijnen de zetels wel, maar niet waar we wilden: ze staan op (2,2) en (5,9), terwijl we ze op (2+6, 2+5) en (5+6, 9+5) willen, dus rekening houdend met de locatie van het salon zelf.
Interface
We willen ook dat alle elementen van het salon mee verhuizen als we het salon verplaatsen:
List<MapObject> allObjects = new List<MapObject>();
allObjects.Add(new SalonElement(new Point(5, 5)));
allObjects[0].Paint();
//Verplaats het salon
allObjects[0].Location = new Point(10, 10);
allObjects[0].Paint();Dat gebeurt nu niet. De oplossing is wat gevorderder, maar ze laat hopelijk het voordeel van een interface zien.
We leggen een interface IComposite vast, die elke samengestelde klasse moet implementeren:
interface IComposite
{
void UpdateElements(Point offset);
}SalonElement wordt dan:
class SalonElement : MapObject, IComposite
{
private List<MapObject> elementen = new List<MapObject>();
public SalonElement(Point salonLoc) : base(new Point(0, 0), ' ', 0)
{
elementen.Add(new ZetelElement(new Point(2, 2), 3));
elementen.Add(new ZetelElement(new Point(5, 9), 3));
Location = salonLoc;
}
public override void Paint()
{
foreach (MapObject element in elementen)
{
element.Paint();
}
}
public void UpdateElements(Point offset)
{
foreach (MapObject element in elementen)
{
element.Location = new Point(element.Location.X + offset.X, element.Location.Y + offset.Y);
}
}
}Het salon start nu op (0,0), met de zetels op hun eigen plek ten opzichte van dat punt. Pas daarna zet de constructor het salon op zijn echte locatie. Op dat moment moeten de zetels mee verschuiven.
Is en as
Elke keer dat we UpdateElements oproepen, verschuiven alle elementen van het object mee.
Rest ons nog één aanpassing: ervoor zorgen dat UpdateElements ook echt opgeroepen wordt, telkens de Location van een samengesteld object verandert. Location hoort bij MapObject, dus de aanpassing gebeurt in die klasse. De set berekent de verschuiving (de offset): het verschil tussen de nieuwe en de huidige locatie. Bij de allereerste locatie is er nog geen huidige locatie (null), en is er dus niets te verschuiven.
public Point Location
{
get { return location; }
set
{
Point vorige = location;
location = value;
if (vorige != null && this is IComposite)
{
Point offset = new Point(value.X - vorige.X, value.Y - vorige.Y);
IComposite samengesteld = this as IComposite;
samengesteld.UpdateElements(offset);
}
}
}Deze code verdient wat toelichting:
- Telkens we de set van
Locationoproepen (bijvoorbeeld inallObjects[0].Location = new Point(10, 10);), krijgt het object sowieso zijn nieuwe locatie. - We bewaren eerst de huidige locatie in
vorige, zodat we de offset kunnen berekenen. - Is er een vorige locatie, en heeft het object (
this, want we zitten in het object zelf) de interfaceIComposite, dan berekenen we de offset in de x- en de y-richting. - We zetten het object even om naar een
IComposite, zodat weUpdateElementskunnen oproepen.
Nu kunnen we zelfs een volledig huis als klasse beschrijven, en zo verschillende soorten huizen maken. Verplaatsen we een huis, dan verhuist de hele inboedel mee.
Belangrijk: de interface staat hier vooral ter illustratie. Je kan dit probleem ook oplossen met een klasse CompositeElement die van MapObject erft, met een lijst van elementen en een methode UpdateElements. In MapObject controleer je dan of een object een CompositeElement is, in plaats van een IComposite.
Een nog betere oplossing: MapObject zelf kan een lijst van elementen bevatten. Een MapObject met precies 1 element werkt zoals voorheen, maar zonder veel aanpassingen kan je nu ook samengestelde objecten maken.
Think about it.
