7Interfaces, DIP en DI
Een interface is een contract: hij zegt wát een klasse kan, niet hoe. Wie hem implementeert, belooft alle leden te leveren. De compiler controleert de vorm (alle methodes aanwezig), niet de betekenis (een lege methode compileert ook: dat leg je vast in documentatie en tests).
public interface IOplaadbaar { const int MinimaleAccu = 20; // constante mag (C# 8+) int AccuPercentage { get; } // property void LaadOp(); // geen body, geen public nodig } // meerdere interfaces tegelijk public class Smartphone : ICamera, ITelefoon { public void MaakFoto() { ... } public void Bel(string nummer) { ... } } // standaard .NET-interface: sorteren public class Student : IComparable<Student> { public int Credits { get; init; } public int CompareTo(Student? other) { if (other is null) return 1; return Credits.CompareTo(other.Credits); // <0 eerder, 0 gelijk, >0 later } } studenten.Sort(); // werkt nu
Uitleg
Een interface beschrijft alleen wat een klasse kan; de klasse levert de code. Een klasse kan meerdere interfaces tegelijk implementeren. IComparable<T> is een interface van .NET zelf: implementeer CompareTo en Sort() weet hoe jouw objecten geordend moeten worden.
interface- Contract: een lijst met leden die een klasse belooft te leveren, zonder implementatie.
init- Property mag alleen bij het aanmaken van het object een waarde krijgen.
Sort()- Sorteert de lijst zelf, op basis van
CompareTo. CompareTo- Geeft negatief (eerder), 0 (gelijk) of positief (later) terug.
IComparable<T>- .NET-interface die zegt dat objecten onderling vergelijkbaar zijn.
const- Vaste waarde die bij het compileren al vastligt en nooit verandert.
{ get; }- Alleen-lezen property; de waarde wordt in de constructor gezet.
is- Controleert het type (of een patroon) en kan het object meteen in een nieuwe variabele zetten.
| Kenmerk | Abstracte klasse | Interface |
|---|---|---|
| Velden (state) | Ja | Nee |
| Constructor | Ja | Nee |
| Aantal per klasse | Eén | Meerdere |
| Code meegeven | Ja | Beperkt (default methods) |
| UML | cursief / «abstract» | «interface» |
| Relatie | "is een" | "kan" / speelt rol van |
Vuistregel: begin met een interface. Voeg een abstracte basisklasse toe zodra er echt gedeelde code (of state) ontstaat. Het mag ook allebei: class EmailSender : NotificationSenderBase, INotificationSender.
#Interface als type (smalste type)
INotificationSender s = new SmsSender(); // variabele void NotifyAll(IEnumerable<INotificationSender> all) // parameter IReadOnlyList<Order> GetOpenOrders() // returntype private readonly INotificationSender _sender; // veld
Uitleg
Gebruik een interface als type overal waar je een object ontvangt of bewaart. Zo hangt je code niet af van één concrete klasse en kun je later een andere implementatie inpluggen.
IReadOnlyList<T>- Alleen-lezen weergave van een lijst: wel lezen en doorlopen, niet toevoegen of verwijderen.
IEnumerable<T>- Het smalste collectietype: alleen doorlopen met
foreach. private- Alleen zichtbaar binnen deze klasse.
readonly- Veld mag alleen bij de declaratie of in de constructor een waarde krijgen.
List<T>- Lijst die automatisch groeit; toevoegen met
Add, aantal metCount.
Declareer met het smalste type dat je nodig hebt, maak aan met het concrete type.
#DIP: Dependency Inversion Principle
- Hoog niveau hangt niet af van laag niveau. Beide hangen af van abstracties.
- Abstracties hangen niet af van details. Details hangen af van abstracties.
Fout: private readonly EmailSender _s = new EmailSender(); in OrderService. Sms toevoegen = OrderService aanpassen, en testen zonder echt te mailen kan niet.
Goed: OrderService kent alleen INotificationSender. De interface hoort bij wie hem gebruikt (dus geen SMTP-details in de interface).
DIP = ontwerpprincipe (afhangen van abstracties). DI = techniek (de afhankelijkheid van buiten aanreiken). DI zonder DIP kan (je injecteert dan een concrete klasse), maar dan win je weinig.
#DI: constructor injection
public class OrderService { private readonly INotificationSender _sender; public OrderService(INotificationSender sender) { ArgumentNullException.ThrowIfNull(sender); _sender = sender; } public void PlaceOrder(Order o) => _sender.Send(o.Email, "Bedankt voor je bestelling!"); } // Program.cs: alleen hier staan de concrete klassen var service = new OrderService(new EmailSender()); // Fake voor tests public class FakeSender : INotificationSender { public List<(string To, string Msg)> Sent { get; } = new(); public void Send(string to, string msg) => Sent.Add((to, msg)); }
Uitleg
Constructor injection: OrderService krijgt zijn afzender van buiten mee en kent alleen de interface. Welke afzender het wordt, beslist Program.cs. In een test geef je een fake mee die berichten alleen onthoudt, zodat je kunt controleren wat er verstuurd zou worden.
ThrowIfNull- Gooit meteen een
ArgumentNullExceptionals het argument null is. 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.
var- De compiler leidt het type zelf af uit de rechterkant.
new()- Target-typed new: het type wordt afgeleid uit de declaratie links.
List<T>- Lijst die automatisch groeit; toevoegen met
Add, aantal metCount.
Property injection: public ILogger Logger { get; set; } = new ConsoleLogger();. Handig bij een Singleton, omdat je die constructor niet zelf aanroept.