Eine Frage ist offen geblieben: Die Kasse entscheidet nicht mehr â aber irgendwer muss entscheiden, welche Strategie es sein soll. Das Pattern schafft die Verzweigung nicht ab, es verschiebt sie an den Rand.
Im .NET-Alltag gibt es dafĂŒr drei gĂ€ngige Antworten.
1. Alle registrieren, per SchlĂŒssel auswĂ€hlen
Jede Strategie sagt selbst, wofĂŒr sie zustĂ€ndig ist. Ein kleiner AuswĂ€hler sammelt sie ein.
public interface IVersandkosten
{
string Art { get; }
decimal Berechne(Bestellung bestellung);
}
public sealed class VersandAuswahl
{
private readonly Dictionary<string, IVersandkosten> _nachArt;
public VersandAuswahl(IEnumerable<IVersandkosten> strategien) =>
_nachArt = strategien.ToDictionary(s => s.Art, StringComparer.OrdinalIgnoreCase);
public IVersandkosten Fuer(string art) =>
_nachArt.TryGetValue(art, out var s)
? s
: throw new ArgumentException($"Unbekannte Versandart: {art}");
}builder.Services.AddSingleton<IVersandkosten, StandardVersand>();
builder.Services.AddSingleton<IVersandkosten, ExpressVersand>();
builder.Services.AddSingleton<IVersandkosten, Abholung>();
builder.Services.AddSingleton<VersandAuswahl>();Mehrere Registrierungen derselben Schnittstelle
Der DI-Container von .NET erlaubt das ausdrĂŒcklich. Wer IVersandkosten anfordert, bekommt die zuletzt registrierte; wer IEnumerable<IVersandkosten> anfordert, bekommt alle in Registrierungsreihenfolge. Genau darauf baut das Muster oben.
2. Keyed Services
Seit .NET 8 kann der Container den SchlĂŒssel selbst verwalten, und der eigene AuswĂ€hler entfĂ€llt:
builder.Services.AddKeyedSingleton<IVersandkosten, StandardVersand>("standard");
builder.Services.AddKeyedSingleton<IVersandkosten, ExpressVersand>("express");
// Im Konstruktor:
public Kasse([FromKeyedServices("express")] IVersandkosten versand) => ...
// Oder zur Laufzeit, wenn die Art erst dann bekannt ist:
var strategie = serviceProvider.GetRequiredKeyedService<IVersandkosten>(art);GetRequiredKeyedService nicht ĂŒberall
Sobald du den IServiceProvider in eine Klasse hineinreichst, um dort Dinge nachzuschlagen, siehst du ihrer Signatur nicht mehr an, was sie braucht â das ist das Service-Locator-Antipattern. Halte das auf die eine Stelle beschrĂ€nkt, an der die Auswahl wirklich erst zur Laufzeit möglich ist.
3. Die leichte Variante: ein Delegate
Nicht jede Strategie braucht eine Schnittstelle und eine Klasse. Wenn das Verfahren eine Methode ist und keinen eigenen Zustand hat, ist ein Func<> dieselbe Idee in einer Zeile:
Dieselbe Idee, zwei Gewichtsklassen
Interface
public interface ISortierung
{
IEnumerable<Artikel> Sortiere(
IEnumerable<Artikel> artikel);
}
public sealed class NachPreis : ISortierung
{
public IEnumerable<Artikel> Sortiere(
IEnumerable<Artikel> a) =>
a.OrderBy(x => x.Preis);
}Delegate
Func<IEnumerable<Artikel>,
IEnumerable<Artikel>> nachPreis =
a => a.OrderBy(x => x.Preis);
// Ăbergeben wie jede andere Strategie:
var liste = Anzeigen(artikel, nachPreis);LINQ selbst ist voll davon: Der Vergleicher in OrderBy, das PrĂ€dikat in Where â das sind Strategien, die als Delegate ĂŒbergeben werden.
Nimm das Interface, wenn die Strategie einen Namen verdient, mehrere Methoden hat, eigene AbhĂ€ngigkeiten braucht oder aus dem DI-Container kommen soll. Nimm das Delegate fĂŒr alles andere.
Woran du merkst, dass es zu weit ging
- Es gibt genau eine Implementierung, und seit zwei Jahren kam keine dazu.
- Die Schnittstelle hat einen Parameter, den nur eine einzige Implementierung benutzt â dann sind die Strategien nicht wirklich austauschbar.
- Um eine Verzweigung zu verstehen, musst du vier Dateien öffnen, obwohl die FÀlle sich nie Àndern.
Strategy zahlt sich dort aus, wo FĂ€lle kommen und gehen. Wo sie stehen, ist ein switch ehrlicher.