OOD Handboek

12Uitgewerkt voorbeeld: DeelMobiel

De implementatie bij het diagram in 1.8. Prijzen: fiets € 0,10/min; scooter € 1,00 start + € 0,25/min; step € 0,50 start + € 0,20/min. Een elektrisch voertuig is alleen te huren bij minimaal 20% accu. Voertuig, Fiets, de exception en de logger staan in hoofdstuk 4, 8 en 9.

ElektrischVoertuig.cs
public abstract class ElektrischVoertuig : Voertuig, IOplaadbaar
{   // gedeelde accu-logica op één plek (geen dubbele code)
    private int _accuPercentage;

    protected ElektrischVoertuig(string id, string merk, int accu)
        : base(id, merk)
    {
        AccuPercentage = accu;          // via property = validatie
    }

    public int AccuPercentage
    {
        get => _accuPercentage;
        set
        {
            if (value is < 0 or > 100)
                throw new ArgumentOutOfRangeException(nameof(value),
                    "Accupercentage moet tussen 0 en 100 liggen.");
            _accuPercentage = value;
        }
    }

    public void LaadOp() => AccuPercentage = 100;

    public override string ToString()
        => $"{base.ToString()}, accu {AccuPercentage}%";
}
Uitleg

Scooter en step delen dezelfde accu-logica, dus die staat één keer in een abstracte tussenklasse. Die erft van Voertuig en implementeert IOplaadbaar. De constructor zet het percentage via de property, zodat de controle op 0 tot en met 100 ook bij aanmaken werkt.

Functies en keywords
abstract
Klasse kan niet met new gemaakt worden; een abstracte methode heeft geen body en moet in de subklasse worden ingevuld.
base(...)
Roept de constructor van de ouderklasse aan, vóór de eigen constructor draait.
base.Methode()
Roept de versie van de ouderklasse aan, bijvoorbeeld om die tekst aan te vullen.
protected
Zichtbaar in deze klasse en in subklassen, niet daarbuiten.
nameof
Geeft de naam van een variabele of parameter als tekst, en blijft kloppen als je hem hernoemt.
throw
Gooit een exception; de methode stopt direct en de fout gaat omhoog naar de aanroeper.
ArgumentOutOfRangeException
Voor een waarde buiten het toegestane bereik.
override
Vervangt een virtual of abstracte methode van de ouder; tijdens runtime wordt de versie van het echte object gekozen.
private
Alleen zichtbaar binnen deze klasse.
value
In een setter: de waarde die iemand probeert toe te kennen.
=>
Korte schrijfwijze voor een methode of property die één expressie teruggeeft; in LINQ ook de pijl van een lambda.
$"...{x}"
String-interpolatie: zet de waarde van x direct in de tekst.
Scooter.cs (ElektrischeStep idem: 0.50m + 0.20m/min)
public class Scooter : ElektrischVoertuig
{
    private const decimal Starttarief = 1.00m;
    private const decimal PrijsPerMinuut = 0.25m;
    public string Kenteken { get; }

    public Scooter(string id, string merk, string kenteken, int accu)
        : base(id, merk, accu)
    {
        Kenteken = kenteken;
    }

    public override decimal BerekenPrijs(int minuten)
        => Starttarief + minuten * PrijsPerMinuut;

    public override string ToString()
        => $"{base.ToString()}, kenteken {Kenteken}";
}
Uitleg

De scooter hoeft alleen nog zijn eigen gegevens en prijsberekening te leveren; de accu komt van de tussenklasse. Tarieven staan in benoemde constanten met het m-achtervoegsel, zodat het decimals zijn.

Functies en keywords
base(...)
Roept de constructor van de ouderklasse aan, vóór de eigen constructor draait.
base.Methode()
Roept de versie van de ouderklasse aan, bijvoorbeeld om die tekst aan te vullen.
override
Vervangt een virtual of abstracte methode van de ouder; tijdens runtime wordt de versie van het echte object gekozen.
private
Alleen zichtbaar binnen deze klasse.
const
Vaste waarde die bij het compileren al vastligt en nooit verandert.
{ get; }
Alleen-lezen property; de waarde wordt in de constructor gezet.
=>
Korte schrijfwijze voor een methode of property die één expressie teruggeeft; in LINQ ook de pijl van een lambda.
$"...{x}"
String-interpolatie: zet de waarde van x direct in de tekst.
ToString()
Tekstweergave van een object; wordt automatisch gebruikt bij Console.WriteLine(obj).
Verhuurbedrijf.cs
public sealed class Verhuurbedrijf
{
    private static Verhuurbedrijf? _instance;
    public static Verhuurbedrijf Instance => _instance ??= new();
    private Verhuurbedrijf() { }

    private readonly List<Voertuig> _voertuigen = new();
    public IReadOnlyList<Voertuig> Voertuigen => _voertuigen; // upcasting
    public ILogger Logger { get; set; } = new ConsoleLogger();

    public void VoegToe(Voertuig v)
    {
        ArgumentNullException.ThrowIfNull(v);
        _voertuigen.Add(v);
        Logger.Info($"Toegevoegd: {v.Id}");
    }

    public void Huur(string id)
    {
        Voertuig v = Zoek(id) ?? throw Weiger(id, $"Voertuig {id} bestaat niet.");
        if (!v.IsBeschikbaar)
            throw Weiger(id, $"Voertuig {id} is al verhuurd.");
        if (v is IOplaadbaar o && o.AccuPercentage < IOplaadbaar.MinimaleAccu)
            throw Weiger(id, $"Accu van {id} is te leeg ({o.AccuPercentage}%).");
        v.IsBeschikbaar = false;
        Logger.Info($"Verhuurd: {id}");
    }

    public decimal Retourneer(string id, int minuten)
    {
        Voertuig v = Zoek(id)
            ?? throw new ArgumentException($"Voertuig {id} bestaat niet.", nameof(id));
        v.IsBeschikbaar = true;
        decimal prijs = v.BerekenPrijs(minuten);   // dynamic dispatch
        Logger.Info($"Geretourneerd: {id} na {minuten} min");
        return prijs;
    }

    public void LaadAllesOp()
    {
        List<IOplaadbaar> lijst = _voertuigen.OfType<IOplaadbaar>().ToList();
        foreach (IOplaadbaar o in lijst) o.LaadOp();  // upcasting via interface
        Logger.Info($"{lijst.Count} voertuigen opgeladen");
    }

    private Voertuig? Zoek(string id) => _voertuigen.FirstOrDefault(v => v.Id == id);

    private VoertuigNietBeschikbaarException Weiger(string id, string melding)
    {
        Logger.Fout(melding);                         // eerst loggen
        return new VoertuigNietBeschikbaarException(id, melding);
    }
}
Uitleg

Het hart van het systeem. Singleton via Instance, voertuigen in een private lijst die alleen-lezen naar buiten gaat, en een logger via de interface. Huur controleert drie situaties en logt elke weigering voordat de exception wordt gegooid. LaadAllesOp haalt met OfType alleen de oplaadbare voertuigen op.

Functies en keywords
sealed
Van deze klasse mag niemand erven.
nameof
Geeft de naam van een variabele of parameter als tekst, en blijft kloppen als je hem hernoemt.
??
Geeft de rechterkant als de linkerkant null is.
??=
Vult de variabele alleen als hij nog null is.
IReadOnlyList<T>
Alleen-lezen weergave van een lijst: wel lezen en doorlopen, niet toevoegen of verwijderen.
FirstOrDefault
LINQ: eerste element dat voldoet, of null als er geen is.
OfType<T>()
LINQ: houdt alleen elementen van type T over en cast ze meteen; veilig downcasten.
ToList()
Maakt van het LINQ-resultaat een nieuwe List<T>.
throw
Gooit een exception; de methode stopt direct en de fout gaat omhoog naar de aanroeper.
ArgumentException
Voor een ongeldig argument, zoals een lege tekst of verkeerde combinatie.
ThrowIfNull
Gooit meteen een ArgumentNullException als het argument null is.
private
Alleen zichtbaar binnen deze klasse.
Program.cs
using DeelMobiel;

// 1. Singleton aantonen
var bedrijf = Verhuurbedrijf.Instance;
var ook = Verhuurbedrijf.Instance;
Console.WriteLine($"Zelfde object: {ReferenceEquals(bedrijf, ook)}");

// 2. Objecten (upcasting naar Voertuig)
Voertuig fiets   = new Fiets("F1", "Gazelle", 7);
Voertuig scooter = new Scooter("S1", "Niu", "DX-123-B", 80);
Voertuig step    = new ElektrischeStep("E1", "Segway", 25, 10);
bedrijf.VoegToe(fiets); bedrijf.VoegToe(scooter); bedrijf.VoegToe(step);

// 3 + 4. Huren en retourneren
bedrijf.Huur("F1");
Console.WriteLine($"Prijs fiets: € {bedrijf.Retourneer("F1", 30):0.00}");   // 3,00
bedrijf.Huur("S1");
Console.WriteLine($"Prijs scooter: € {bedrijf.Retourneer("S1", 20):0.00}"); // 6,00

// 5. Weigering afvangen
try { bedrijf.Huur("E1"); }
catch (VoertuigNietBeschikbaarException ex)
{
    Console.WriteLine($"Melding aan klant: {ex.Message}");
}

// 6. Opladen en opnieuw
bedrijf.LaadAllesOp();
bedrijf.Huur("E1");
Console.WriteLine("Step is nu verhuurd.");

// 7. Overzicht: overridden ToString() per subklasse
foreach (Voertuig v in bedrijf.Voertuigen)
    Console.WriteLine(v);
Uitleg

De demonstratie: twee keer Instance ophalen bewijst de Singleton, de prijzen laten polymorfisme zien, de try/catch toont de eigen exception, en de laatste lus laat per object de juiste ToString() zien.

Functies en keywords
try / catch
try bevat code die kan falen; catch vangt een specifiek type fout af.
ReferenceEquals
true als twee variabelen naar precies hetzelfde object wijzen.
$"...{x}"
String-interpolatie: zet de waarde van x direct in de tekst.
var
De compiler leidt het type zelf af uit de rechterkant.
foreach
Loopt één voor één door alle elementen van een collectie.
Verwachte uitvoer (bedragen)
Fiets 30 min × € 0,10 = € 3,00. Scooter € 1,00 + 20 × € 0,25 = € 6,00. Step € 0,50 + 10 × € 0,20 = € 2,50.