11Design patterns
"Ik herken dit probleem" → "ik ken de standaardoplossing". Bron: Gang of Four (1994) en refactoring.guru.
| Categorie | Gaat over | Voorbeelden |
|---|---|---|
| Creational | hoe objecten worden gemaakt | Singleton, Factory Method, Builder |
| Structural | hoe klassen samen grotere structuren vormen | Adapter (bv. twee medewerkersdatabases achter één interface), Composite, Decorator |
| Behavioral | verantwoordelijkheden en algoritmes tussen objecten | Strategy, Observer (prijs verandert → anderen reageren) |
#11.1 Singleton (creational): precies één instantie
- private constructor: niemand anders kan
newdoen. - static veld met de enige instantie.
- static property
Instancedie hem teruggeeft (en zo nodig maakt). - Klasse
sealedmaken (geen subklassen die alsnog extra objecten maken). - Bewijs in main:
ReferenceEquals(a, b)geeftTrue.
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.
sealed- Van deze klasse mag niemand erven.
??=- Vult de variabele alleen als hij nog null is.
ReferenceEqualstrueals 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
xdirect 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
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.
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
xdirect in de tekst.
#11.3 Strategy (behavioral): verwisselbaar gedrag tijdens runtime
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.
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
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.
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
xdirect 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 metCount.
#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.
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 metCount.
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).