OOD Handboek

11Design patterns

"Ik herken dit probleem" → "ik ken de standaardoplossing". Bron: Gang of Four (1994) en refactoring.guru.

CategorieGaat overVoorbeelden
Creationalhoe objecten worden gemaaktSingleton, Factory Method, Builder
Structuralhoe klassen samen grotere structuren vormenAdapter (bv. twee medewerkersdatabases achter één interface), Composite, Decorator
Behavioralverantwoordelijkheden en algoritmes tussen objectenStrategy, Observer (prijs verandert → anderen reageren)

#11.1 Singleton (creational): precies één instantie

Configuratie-_instance: Configuratie?-Configuratie()+Instance: Configuratie {get}
  • private constructor: niemand anders kan new doen.
  • static veld met de enige instantie.
  • static property Instance die hem teruggeeft (en zo nodig maakt).
  • Klasse sealed maken (geen subklassen die alsnog extra objecten maken).
  • Bewijs in main: ReferenceEquals(a, b) geeft True.
public sealed class Verhuurbedrijf
{
    private static Verhuurbedrijf? _instance;

    private Verhuurbedrijf() { }               // private!

    public static Verhuurbedrijf Instance
        => _instance ??= new Verhuurbedrijf();  // lazy aanmaken
}

// Thread-safe variant met Lazy<T>:
private static readonly Lazy<Verhuurbedrijf> _lazy =
    new(() => new Verhuurbedrijf());
public static Verhuurbedrijf Instance => _lazy.Value;

// Gebruik
var a = Verhuurbedrijf.Instance;
var b = Verhuurbedrijf.Instance;
Console.WriteLine($"Zelfde object: {ReferenceEquals(a, b)}"); // True
Uitleg

De Singleton zorgt dat er precies één object bestaat. De constructor is private, dus alleen de klasse zelf kan hem aanmaken. Instance maakt het object bij het eerste gebruik en geeft daarna steeds hetzelfde terug. De Lazy<T>-variant is veilig als meerdere threads tegelijk vragen.

Functies en keywords
sealed
Van deze klasse mag niemand erven.
??=
Vult de variabele alleen als hij nog null is.
ReferenceEquals
true als twee variabelen naar precies hetzelfde object wijzen.
Lazy<T>
Maakt het object pas bij het eerste gebruik aan, thread-safe.
private
Alleen zichtbaar binnen deze klasse.
static
Hoort bij de klasse zelf, niet bij één object; gedeeld door alle objecten.
readonly
Veld mag alleen bij de declaratie of in de constructor een waarde krijgen.
=>
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.
var
De compiler leidt het type zelf af uit de rechterkant.
new()
Target-typed new: het type wordt afgeleid uit de declaratie links.

#11.2 Factory Method (creational): juiste type maken op basis van invoer

ApparaatFactory+Maak(type: string, naam: string): Apparaat«abstract»ApparaatDimbareLampThermostaat«create»
public static class ApparaatFactory
{
    public static Apparaat Maak(string type, string naam) => type.ToLower() switch
    {
        "lamp"        => new DimbareLamp(naam),
        "thermostaat" => new Thermostaat(naam),
        _ => throw new ArgumentException($"Onbekend type: {type}", nameof(type))
    };
}
Apparaat a = ApparaatFactory.Maak("lamp", "Hal");   // upcasting
Uitleg

Een factory beslist op basis van invoer welk concreet type er gemaakt wordt. De aanroeper krijgt een Apparaat terug en hoeft de subklassen niet te kennen. Een nieuw type toevoegen betekent alleen de factory uitbreiden.

Functies en keywords
nameof
Geeft de naam van een variabele of parameter als tekst, en blijft kloppen als je hem hernoemt.
switch-expressie
Kiest een waarde op basis van patronen; _ is het standaardgeval.
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.
static
Hoort bij de klasse zelf, niet bij één object; gedeeld door alle objecten.
=>
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.

#11.3 Strategy (behavioral): verwisselbaar gedrag tijdens runtime

GebouwBeheerder+Strategie: IEnergieStrategie {get; set}+Stuur(zone: Zone): void«interface»IEnergieStrategie+Pas(zone: Zone): voidPiekuurStrategie+Pas(zone: Zone): voidNachtStrategie+Pas(zone: Zone): void1
public interface IEnergieStrategie { void Pas(Zone zone); }

public class PiekuurStrategie : IEnergieStrategie
{
    public void Pas(Zone zone) { /* lampen dimmen */ }
}
public class NachtStrategie : IEnergieStrategie
{
    public void Pas(Zone zone) { /* alles uit */ }
}

public class GebouwBeheerder
{
    public IEnergieStrategie Strategie { get; set; } = new PiekuurStrategie();
    public void Stuur(Zone zone) => Strategie.Pas(zone);
}
beheerder.Strategie = new NachtStrategie();   // wisselen tijdens runtime
Uitleg

Strategy maakt gedrag verwisselbaar: de beheerder roept altijd Pas() aan, maar wat er gebeurt hangt af van welke strategie er op dat moment in de property zit. Je kunt tijdens het draaien wisselen zonder if-ketens.

Functies en keywords
interface
Contract: een lijst met leden die een klasse belooft te leveren, zonder implementatie.
=>
Korte schrijfwijze voor een methode of property die één expressie teruggeeft; in LINQ ook de pijl van een lambda.

#11.4 Observer (behavioral): anderen automatisch op de hoogte stellen

TemperatuurSensor-_observers: List<ISensorObserver>+Abonneer(o: ISensorObserver): void+Meet(waarde: double): void-Meld(waarde: double): void«interface»ISensorObserver+Update(sensor: Sensor, waarde: double): voidSecurityServiceLogService0..*

Het subject (sensor) kent alleen de interface van zijn observers, niet de concrete klassen. Zo kun je observers toevoegen zonder de sensor te wijzigen.

public interface ISensorObserver
{
    void Update(Sensor sensor, double waarde);
}

public class TemperatuurSensor : Sensor
{
    private readonly List<ISensorObserver> _observers = new();
    private const double Grens = 5.0;

    public void Abonneer(ISensorObserver o) => _observers.Add(o);
    public void Opzeggen(ISensorObserver o) => _observers.Remove(o);

    public void Meet(double waarde)
    {
        if (waarde < Grens) Meld(waarde);
    }
    private void Meld(double waarde)
    {
        foreach (var o in _observers) o.Update(this, waarde);
    }
}

public class SecurityService : ISensorObserver
{
    public void Update(Sensor s, double w)
        => Console.WriteLine($"ALARM: {s.Naam} meet {w}");
}
Uitleg

Observer: de sensor houdt een lijst bij van geïnteresseerden en waarschuwt ze allemaal zodra een waarde onder de grens zakt. De sensor kent alleen de interface, dus je kunt observers toevoegen of weghalen zonder de sensor aan te passen.

Functies en keywords
interface
Contract: een lijst met leden die een klasse belooft te leveren, zonder implementatie.
this
Verwijst naar het huidige object, bijvoorbeeld om het zelf terug te geven.
private
Alleen zichtbaar binnen deze klasse.
const
Vaste waarde die bij het compileren al vastligt en nooit verandert.
readonly
Veld mag alleen bij de declaratie of in de constructor een waarde krijgen.
=>
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.
var
De compiler leidt het type zelf af uit de rechterkant.
new()
Target-typed new: het type wordt afgeleid uit de declaratie links.
foreach
Loopt één voor één door alle elementen van een collectie.
List<T>
Lijst die automatisch groeit; toevoegen met Add, aantal met Count.

#11.5 Builder (creational): stap voor stap samenstellen

public class Pizza
{
    public int Bodem { get; }
    public string Saus { get; }
    public IReadOnlyList<string> Toppings { get; }
    public Pizza(int bodem, string saus, List<string> toppings)
        => (Bodem, Saus, Toppings) = (bodem, saus, toppings);
}

public class PizzaBuilder
{
    private int _bodem = 29;
    private string _saus = "tomaat";
    private readonly List<string> _toppings = new();

    public PizzaBuilder MetBodem(int cm) { _bodem = cm; return this; }
    public PizzaBuilder MetSaus(string s) { _saus = s; return this; }
    public PizzaBuilder MetTopping(string t) { _toppings.Add(t); return this; }
    public Pizza Bouw() => new Pizza(_bodem, _saus, new List<string>(_toppings));
}
Uitleg

De builder verzamelt de onderdelen stap voor stap en maakt pas in Bouw() het eindproduct. Elke Met...-methode geeft this terug, daardoor kun je aanroepen achter elkaar plakken. De toppings worden gekopieerd zodat de pizza niet verandert als je de builder hergebruikt.

Functies en keywords
IReadOnlyList<T>
Alleen-lezen weergave van een lijst: wel lezen en doorlopen, niet toevoegen of verwijderen.
this
Verwijst naar het huidige object, bijvoorbeeld om het zelf terug te geven.
private
Alleen zichtbaar binnen deze klasse.
readonly
Veld mag alleen bij de declaratie of in de constructor een waarde krijgen.
{ 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.
new()
Target-typed new: het type wordt afgeleid uit de declaratie links.
List<T>
Lijst die automatisch groeit; toevoegen met Add, aantal met Count.

Elke Met...-methode geeft this terug, zodat je aanroepen kunt ketenen (fluent interface). Bouw() levert het eindproduct. Handig bij veel optionele onderdelen: geen constructor met tien parameters.

Pizza pizza = new PizzaBuilder()
    .MetBodem(35)
    .MetSaus("bbq")
    .MetTopping("kaas")
    .MetTopping("ui")
    .Bouw();
Uitleg

Zo gebruik je de builder: lees het als een recept. Alles wat je niet opgeeft houdt de standaardwaarde (29 cm, tomatensaus).