OOD Handboek

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.

Functies en keywords
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.
KenmerkAbstracte klasseInterface
Velden (state)JaNee
ConstructorJaNee
Aantal per klasseEénMeerdere
Code meegevenJaBeperkt (default methods)
UMLcursief / «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.

Functies en keywords
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 met Count.

Declareer met het smalste type dat je nodig hebt, maak aan met het concrete type.

#DIP: Dependency Inversion Principle

  1. Hoog niveau hangt niet af van laag niveau. Beide hangen af van abstracties.
  2. 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.

Functies en keywords
ThrowIfNull
Gooit meteen een ArgumentNullException als 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 met Count.

Property injection: public ILogger Logger { get; set; } = new ConsoleLogger();. Handig bij een Singleton, omdat je die constructor niet zelf aanroept.