Zum Inhalt
wannacode

Entwurfsmuster

Kurz gesagt: Ein Entwurfsmuster ist eine bewährte Lösung für ein Problem, das in vielen Programmen wiederkehrt, zum Beispiel Strategy, Observer, Factory Method, Singleton und Adapter.

Problem

Die Methode, die mit jeder Aktion wächst

Der Preis im Shop hängt von der Aktion ab:

static decimal FinalPrice(decimal price, string promo)
{
if (promo == "student")
{
return price * 0.9m;
}
else if (promo == "black friday")
{
return Math.Max(price - 20, 0);
}
else if (promo == "stammkunde")
{
return price * 0.95m;
}
return price;
}
Console.WriteLine($"{FinalPrice(120, "student"):F2} €");
Console.WriteLine($"{FinalPrice(120, "black friday"):F2} €");
Console.WriteLine($"{FinalPrice(120, "keine"):F2} €");
Ausgabe
108.00 €
100.00 €
120.00 €
Es funktioniert. Aber das Marketing denkt sich jeden Monat eine neue Aktion aus. Jede bedeutet ein neues else if in dieser Methode, und oft in zwei weiteren: in der für die Rechnung und in der für die Bestätigungs-E-Mail. Die Methode kennt alle Aktionen auf einmal, und jede Änderung fasst sie an. Dieses Problem ist so häufig, dass seine Lösung einen Namen hat.

Die Idee

Entwurfsmuster

Ein Entwurfsmuster (englisch design pattern) ist eine bewährte Lösung für ein Problem, das in vielen Programmen wiederkehrt. Es ist keine Bibliothek und kein Code zum Kopieren. Es ist eine Art, die Klassen und Interfaces anzuordnen, die schon durch Tausende Projekte gegangen ist.

Die Muster teilen sich in drei Gruppen:

  • Erzeugungsmuster: wie Objekte entstehen. Zum Beispiel Singleton (nur ein Objekt der Klasse) und Factory Method (welche Klasse erzeugt wird, entscheidet jemand anderes).
  • Strukturmuster: wie Klassen und Objekte zu größeren zusammengesetzt werden. Zum Beispiel Adapter (verbindet unverträgliche Interfaces) und Composite (ein Baum aus Teilen, mit dem man wie mit einem Ganzen arbeitet).
  • Verhaltensmuster: wie Objekte die Arbeit aufteilen und miteinander reden. Zum Beispiel Strategy (eine austauschbare Regel) und Observer (Abonnenten für ein Ereignis).

Warum es sich lohnt, sie zu kennen:

  • du erfindest keine Lösung neu, die andere schon erprobt haben;
  • der Code ändert sich mit weniger Risiko, weil der Teil, der sich ändert, abgetrennt ist;
  • die Namen sind eine gemeinsame Sprache. „Setz hier einen Adapter ein“ sagt einer Kollegin eine ganze Struktur in einem Wort.
Genauer, für Neugierige
Der klassische Katalog ist das Buch „Design Patterns“ von 1994, geschrieben von vier Autoren, die man die „Viererbande“ nennt (Gang of Four, GoF). Es enthält 23 Muster. Hier sind die fünf, denen du in C#-Code am häufigsten begegnest. Einige hat C# in die Sprache eingebaut: Delegates (Func, Action) erledigen die Arbeit von Strategy, und Events (event) die von Observer. Ein Muster ist kein Ziel: Wenn ein einziges if die Arbeit erledigt, brauchst du kein Muster.

Sieh es dir an

Strategy: eine austauschbare Regel

Der Rabatt wird eine Property vom Typ Func<decimal, decimal>: eine Funktion, die einen Preis annimmt und einen Preis zurückgibt. So einen Typ für eine Funktion nennt man in C# ein Delegate. Die Kasse weiß nicht, welche Aktion es ist, sie ruft nur Discount(price) auf:
class Checkout
{
public Func<decimal, decimal> Discount { get; set; }
public Checkout(Func<decimal, decimal> discount)
{
Discount = discount;
}
public decimal Total(decimal price)
{
return Math.Max(Discount(price), 0);
}
}
Jeder Rabatt ist ein Lambda:
var checkout = new Checkout(price => price);
Console.WriteLine($"Ohne Rabatt: {checkout.Total(120):F2} €");
checkout.Discount = price => price * 0.9m;
Console.WriteLine($"Student: {checkout.Total(120):F2} €");
checkout.Discount = price => price - 20;
Console.WriteLine($"Black Friday: {checkout.Total(120):F2} €");
Ausgabe
Ohne Rabatt: 120.00 €
Student: 108.00 €
Black Friday: 100.00 €
Eine neue Aktion ist ein neues Lambda. Checkout ändert sich nicht. Die Regel lässt sich auch wechseln, während das Programm läuft: Du speicherst ein neues Lambda in checkout.Discount.Das ist Strategy: eine Familie von Regeln, jede vom selben Typ (hier Func<decimal, decimal>), sodass sie austauschbar sind. Das Lambda zum Vergleichen, das du an Sort einer Liste übergibst, ist auch eine Strategie: Sort weiß, wie man sortiert, und du sagst, wonach.
Genauer, für Neugierige

In den Büchern heißt die Klasse, die die Strategie benutzt, „Kontext“ (hier Checkout). In Java ist die Strategie ein Interface. In C# geht ein Interface auch, aber dann kannst du kein Lambda übergeben: Ein Lambda wird in ein Delegate umgewandelt, und ein Interface ist kein Delegate.

interface IDiscountStrategy
{
decimal Apply(decimal price);
}
IDiscountStrategy student = price => price * 0.9m;
Kompilierfehler
error CS1660: Cannot convert lambda expression to type 'IDiscountStrategy' because it is not a delegate type

Darum ist eine austauschbare Regel aus einer Funktion in C# ein Delegate. Hat die Strategie eigene Felder, kann sie eine Klasse sein, und die Kasse bekommt deren Methode. new PercentDiscount(10).Apply, ohne Klammern, ist ein Delegate auf die Methode Apply dieses Objekts:

class PercentDiscount
{
private decimal percent;
public PercentDiscount(decimal percent)
{
this.percent = percent;
}
public decimal Apply(decimal price)
{
return price * (1 - percent / 100);
}
}
var checkout = new Checkout(new PercentDiscount(10).Apply);
Console.WriteLine($"Student: {checkout.Total(120):F2} €");
Ausgabe
Student: 108.00 €

Eine Klasse ist die bessere Wahl, wenn die Strategie eigene Felder hat oder lang ist. Ein Lambda, wenn sie eine Zeile ist.

Sieh es dir an

Observer: Abonnenten für ein Ereignis

Wenn sich der Bestand eines Produkts ändert, muss die Website die neue Anzahl zeigen, und das Lager muss nachbestellen, wenn nur noch wenige Stück da sind. Morgen kommt vielleicht noch eine E-Mail an die Kunden dazu. Das Produkt soll von keinem von ihnen wissen.

class Product
{
private string name;
private int quantity;
private List<Action<string, int>> listeners = new List<Action<string, int>>();
public Product(string name, int quantity)
{
this.name = name;
this.quantity = quantity;
}
public void Subscribe(Action<string, int> listener)
{
listeners.Add(listener);
}
public int Quantity
{
get { return quantity; }
set
{
if (value == quantity)
{
return;
}
quantity = value;
foreach (var listener in listeners)
{
listener(name, quantity);
}
}
}
}
var phone = new Product("Telefon", 0);
phone.Subscribe((name, qty) => Console.WriteLine($"Website: {name}, Bestand {qty} Stk."));
phone.Subscribe((name, qty) =>
{
if (qty < 3)
{
Console.WriteLine($"Lager: Nachbestellung {name}");
}
});
phone.Quantity = 10;
phone.Quantity = 2;
Ausgabe
Website: Telefon, Bestand 10 Stk.
Website: Telefon, Bestand 2 Stk.
Lager: Nachbestellung Telefon
  • Wer Bescheid wissen will, abonniert mit Subscribe und übergibt ein Lambda. Sein Typ ist Action<string, int>: ein Delegate, das einen Namen und eine Anzahl annimmt und nichts zurückgibt.
  • Der Bestand ist eine Property Quantity. Bei phone.Quantity = 10 läuft ihr set: Das Produkt geht die Liste durch und ruft jeden Abonnenten auf. Wer sie sind und was sie tun, geht es nichts an.
  • Hat sich die Anzahl nicht geändert, wird niemand gestört.

var phone = new Product("Telefon", 0);
phone.Subscribe((name, qty) => Console.WriteLine($"Website: {name}, Bestand {qty} Stk."));
phone.Subscribe((name, qty) =>
{
if (qty < 3)
{
Console.WriteLine($"Lager: Nachbestellung {name}");
}
});
phone.Quantity = 10;
phone.Quantity = 2;
Schritt für Schritt, in Worten
  1. Telefon mit Bestand 0 und einer leeren Liste listeners.
  2. Die Website abonniert: das Lambda kommt in die Liste.
  3. Das Lager auch. Das Produkt weiß nicht, wer sie sind, nur dass es sie mit einem Namen und einer Anzahl aufrufen kann.
  4. Der Bestand ändert sich auf 10. Das Produkt geht die Liste durch und ruft jeden Abonnenten in der Reihenfolge des Abonnierens auf. Die Website zeigt die neue Anzahl.
  5. Das Lager prüft 10 < 3: nein, also tut es nichts.
  6. Der Bestand ändert sich auf 2. Das Produkt geht die Liste durch und ruft jeden Abonnenten in der Reihenfolge des Abonnierens auf. Die Website zeigt die neue Anzahl.
  7. Das Lager prüft 2 < 3: ja, also bestellt es nach.

Das ist Observer (Beobachter). Das Produkt ist das beobachtete Objekt (das Subject), und die Listener sind die Observer. Buttons in grafischen Programmen funktionieren genau so: „Beim Klick ruf das hier auf“.

Genauer, für Neugierige
  • In C# ist die Liste der Abonnenten in die Sprache eingebaut: ein Event (event). += abonniert, -= meldet ab, und StockChanged?.Invoke(...) ruft alle Abonnenten auf. Ohne Abonnenten ist das Event null, darum steht dort ?.: Dann wird nichts aufgerufen.
class Product
{
private string name;
private int quantity;
public event Action<string, int> StockChanged;
public Product(string name, int quantity)
{
this.name = name;
this.quantity = quantity;
}
public int Quantity
{
get { return quantity; }
set
{
if (value == quantity)
{
return;
}
quantity = value;
StockChanged?.Invoke(name, quantity);
}
}
}
  • Du meldest dasselbe Lambda ab, das du abonniert hast, also hältst du es in einer Variable. Ein Abonnent, den niemand abmeldet, bleibt im Speicher, solange das Produkt lebt.
var phone = new Product("Telefon", 5);
Action<string, int> site = (name, qty) => Console.WriteLine($"Website: {qty} Stk.");
phone.StockChanged += site;
phone.Quantity = 4;
phone.StockChanged -= site;
phone.Quantity = 3;
Console.WriteLine("Die Website hört nicht mehr zu");
Ausgabe
Website: 4 Stk.
Die Website hört nicht mehr zu
  • Von außen kann man das Event nur abonnieren und abmelden. Auslösen kann es nur die Klasse selbst:
phone.StockChanged("Telefon", 0);
Kompilierfehler
error CS0070: The event 'Product.StockChanged' can only appear on the left hand side of += or -= (except when used from within the type 'Product')
  • In der alten Vorlesung heißt die beobachtete Klasse Subject, mit den Methoden attach und notifyAllObservers, und der Observer ist eine abstrakte Klasse mit einer Methode update(). Die Idee ist dieselbe, und mit Delegates brauchst du keine Klasse für jeden Observer.
  • Die Listener werden nacheinander aufgerufen, in dem Thread, der die Anzahl geändert hat. Ist einer von ihnen langsam, warten alle.

Probier es aus

Wer hört es?

Product ist das aus der Karte davor.

Was gibt das Programm aus?

var coffee = new Product("Kaffee", 5);
coffee.Subscribe((name, qty) => Console.WriteLine($"A: {qty}"));
coffee.Quantity = 4;
coffee.Quantity = 4;
coffee.Subscribe((name, qty) => Console.WriteLine($"B: {qty}"));
coffee.Quantity = 0;
Antwort

Sieh es dir an

Factory Method: welche Klasse erzeugt wird

Der Kunde hat gewählt, wie wir ihn benachrichtigen: per E-Mail oder SMS. Der Code, der sendet, soll nicht an zehn Stellen new EmailNotification() schreiben, jede mit ihrem eigenen if.
interface INotification
{
void Send(string to, string text);
}
class EmailNotification : INotification
{
public void Send(string to, string text)
{
Console.WriteLine($"E-Mail an {to}: {text}");
}
}
class SmsNotification : INotification
{
public void Send(string to, string text)
{
Console.WriteLine($"SMS an {to}: {text}");
}
}
Die Wahl steht an einer Stelle, in einer Fabrik:
static class NotificationFactory
{
public static INotification Create(string channel)
{
return channel switch
{
"email" => new EmailNotification(),
"sms" => new SmsNotification(),
_ => throw new ArgumentException($"Unbekannter Kanal: {channel}"),
};
}
}
string[,] customers =
{
{ "email", "maria@example.com" },
{ "sms", "+49 151 2345678" },
{ "taube", "Hauptstraße 5" }
};
for (int i = 0; i < customers.GetLength(0); i++)
{
try
{
var n = NotificationFactory.Create(customers[i, 0]);
n.Send(customers[i, 1], "Deine Bestellung wurde versandt.");
}
catch (ArgumentException e)
{
Console.WriteLine(e.Message);
}
}
Ausgabe
E-Mail an maria@example.com: Deine Bestellung wurde versandt.
SMS an +49 151 2345678: Deine Bestellung wurde versandt.
Unbekannter Kanal: taube
Der Aufrufer sagt, was er will („email“), und bekommt eine INotification. Welche Klasse genau dahintersteht, weiß nur die Fabrik. Ein neuer Kanal, zum Beispiel Push, ist eine neue Klasse und eine neue Zeile im switch. Das Senden wird nicht angefasst.
Genauer, für Neugierige
  • Das ist die häufigste Form in der Praxis. Ihr genauerer Name ist „einfache Fabrik“ (simple factory). Die Factory Method aus dem GoF-Buch ist etwas anderes: Die Basisklasse hat eine abstrakte Methode, die das Objekt erzeugt, und die Unterklassen entscheiden, welches:
abstract class Shop
{
protected abstract INotification CreateNotification();
public void ConfirmOrder(string to)
{
CreateNotification().Send(to, "Die Bestellung ist eingegangen.");
}
}
class OnlineShop : Shop
{
protected override INotification CreateNotification()
{
return new EmailNotification();
}
}
class PhoneShop : Shop
{
protected override INotification CreateNotification()
{
return new SmsNotification();
}
}
new OnlineShop().ConfirmOrder("maria@example.com");
new PhoneShop().ConfirmOrder("+49 151 2345678");
Ausgabe
E-Mail an maria@example.com: Die Bestellung ist eingegangen.
SMS an +49 151 2345678: Die Bestellung ist eingegangen.

ConfirmOrder ist einmal geschrieben, in Shop, und weiß nicht, welche Benachrichtigung es sendet.

  • In alten Beispielen gibt die Fabrik für eine unbekannte Art null zurück. Dann stürzt der Aufrufer mit einer NullReferenceException ab, weit weg von der eigentlichen Ursache:
static class NotificationFactory
{
public static INotification Create(string channel)
{
if (channel == "email")
{
return new EmailNotification();
}
if (channel == "sms")
{
return new SmsNotification();
}
return null;
}
}
var n = NotificationFactory.Create("taube");
try
{
n.Send("Hauptstraße 5", "Deine Bestellung wurde versandt.");
}
catch (NullReferenceException e)
{
Console.WriteLine($"NullReferenceException: {e.Message}");
}
Ausgabe
NullReferenceException: Object reference not set to an instance of an object.

Eine Exception mit einer klaren Meldung in der Fabrik ist besser.

Sieh es dir an

Singleton: nur ein Objekt

Die Einstellungen der Anwendung, etwa die Währung, werden einmal gelesen. Macht sich jeder Teil des Programms sein eigenes Einstellungsobjekt, werden sie viele Male gelesen und können auseinanderlaufen.

class AppConfig
{
private static AppConfig instance;
public string Currency { get; } = "EUR";
private AppConfig()
{
Console.WriteLine("Einstellungen werden gelesen...");
}
public static AppConfig Instance
{
get
{
if (instance == null)
{
instance = new AppConfig();
}
return instance;
}
}
}
var a = AppConfig.Instance;
var b = AppConfig.Instance;
Console.WriteLine(a == b);
Console.WriteLine(b.Currency);
Ausgabe
Einstellungen werden gelesen...
True
EUR
  • Der Konstruktor ist private. Niemand außerhalb der Klasse kann new AppConfig() schreiben.
  • Das einzige Objekt steht im statischen Feld instance. Die Property Instance erzeugt es beim ersten Lesen und gibt danach immer dieses zurück.
  • „Einstellungen werden gelesen...“ erscheint einmal, und a == b ist True: Es gibt ein Objekt.

Das ist Singleton.

Genauer, für Neugierige
  • Lesen zwei Threads (Teile des Programms, die gleichzeitig laufen) Instance im selben Moment, können beide null sehen und zwei Objekte erzeugen.
  • Der sicherste Singleton in C# ist ein static readonly Feld. Die Laufzeitumgebung initialisiert die statischen Felder einer Klasse genau einmal, bevor die Klasse benutzt wird, auch wenn viele Threads gleichzeitig danach fragen:
class Settings
{
public static readonly Settings Instance = new Settings();
public string Currency { get; } = "EUR";
private Settings()
{
Console.WriteLine("Einstellungen werden erstellt...");
}
}
Console.WriteLine(Settings.Instance.Currency);
Console.WriteLine(Settings.Instance == Settings.Instance);
Ausgabe
Einstellungen werden erstellt...
EUR
True
  • Ist das Erzeugen teuer und soll erst passieren, wenn das Objekt gebraucht wird, gibt es Lazy<T>. Das Objekt entsteht beim ersten .Value, ebenfalls einmal und ebenfalls bei vielen Threads:
class AppConfig
{
private static readonly Lazy<AppConfig> instance = new Lazy<AppConfig>(() => new AppConfig());
public string Currency { get; } = "EUR";
private AppConfig()
{
Console.WriteLine("Einstellungen werden gelesen...");
}
public static AppConfig Instance
{
get { return instance.Value; }
}
}
Console.WriteLine("Das Programm startet");
var a = AppConfig.Instance;
var b = AppConfig.Instance;
Console.WriteLine(a == b);
Ausgabe
Das Programm startet
Einstellungen werden gelesen...
True
  • Ein Singleton ist eine globale Variable für das ganze Programm mit einem schönen Namen. Jeder kann ihn von überall benutzen, darum ist er schwer zu testen und schwer zu ersetzen. Oft ist es besser, das Objekt einmal in Main zu erzeugen und es über den Konstruktor denen zu übergeben, die es benutzen.

Probier es aus

Finde den Fehler

Bestellnummern müssen eindeutig sein, darum ist der Zähler ein Singleton. Das Programm hat aber die Nummer 1001 zweimal vergeben:

Console.WriteLine(OrderNumbers.Instance.NextNumber());
Console.WriteLine(OrderNumbers.Instance.NextNumber());
Console.WriteLine(new OrderNumbers().NextNumber());
Ausgabe
1001
1002
1001

In welcher Zeile der Klasse steckt der Fehler?

Wähle die Zeile mit dem Fehler.

Sieh es dir an

Adapter: verbindet unverträgliche Interfaces

Der Shop bezahlt über ein Interface mit einem Betrag in Euro und einer Antwort „ja oder nein“. Die Bank gibt dir eine alte Bibliothek, die Cent will und einen Fehlercode zurückgibt. Die Bibliothek gehört nicht dir, und du kannst sie nicht ändern.

// Die alte Bibliothek der Bank. Du kannst sie nicht ändern.
class LegacyBankClient
{
// Der Betrag ist in Cent. Gibt bei Erfolg 0 zurück, sonst einen Fehlercode.
public int Charge(long cents, string reference)
{
Console.WriteLine($"Bank: {cents} Cent für {reference}");
return cents <= 50000 ? 0 : 51;
}
}

Du schreibst eine Klasse, die so aussieht, wie der Shop es erwartet, und darin die Bank aufruft:

// Das erwartet der Shop.
interface IPaymentGateway
{
bool Pay(string orderId, decimal amountEur);
}
class BankAdapter : IPaymentGateway
{
private LegacyBankClient bank;
public BankAdapter(LegacyBankClient bank)
{
this.bank = bank;
}
public bool Pay(string orderId, decimal amountEur)
{
long cents = (long)Math.Round(amountEur * 100);
return bank.Charge(cents, "ORDER-" + orderId) == 0;
}
}
IPaymentGateway gateway = new BankAdapter(new LegacyBankClient());
Console.WriteLine(gateway.Pay("1001", 49.90m));
Console.WriteLine(gateway.Pay("1002", 1299.00m));
Ausgabe
Bank: 4990 Cent für ORDER-1001
True
Bank: 129900 Cent für ORDER-1002
False
Der Adapter übersetzt in beide Richtungen: Euro in Cent und eine Nummer in Text auf dem Hinweg, Code 0 in true auf dem Rückweg. Der Shop weiß nicht, dass eine alte Bibliothek dahintersteht. Wechselst du morgen die Bank, schreibst du einen neuen Adapter, und der Shop wird nicht angefasst.

Das ist Adapter: wie ein Reisestecker-Adapter, der weder den Stecker noch die Steckdose verändert.

Genauer, für Neugierige

Das klassische Beispiel in den Büchern, und in der alten Vorlesung, ist ein Mediaplayer. AudioPlayer kann nur mp3 abspielen, und eine vorhandene Klasse AdvancedMediaPlayer hat die Methoden playVlc und playMp4. MediaAdapter implementiert das Interface des Players und leitet an die richtige Methode weiter. Die Struktur ist dieselbe wie hier.

Übung

Muster im Shop

Löse die Aufgaben in deinem Editor.

1. Rabatte als Strategien. Eine statische Klasse Discounts mit Methoden, die ein Lambda vom Typ Func<decimal, decimal> zurückgeben: None(), Percent(decimal p) und Fixed(decimal amount) (nie unter 0 €). Eine Klasse Cart mit einer Liste von Preisen und einem austauschbaren Rabatt in der Property Discount. Füge Kaffeebohnen (18,50 €), Honig (12 €) und Tee (4,20 €) hinzu und gib die Summe aus: ohne Rabatt, mit −10 %, mit −5 € und mit −50 €.

Ausgabe
Ohne Rabatt: 34.70 €
-10%: 31.23 €
-5 €: 29.70 €
-50 €: 0.00 €

2. „Benachrichtige mich, wenn es wieder da ist“. Das Produkt hält eine Liste von Kunden, die warten. Wenn der Bestand von 0 auf mehr als 0 geht, benachrichtigt es alle und leert die Liste, denn die Benachrichtigung kommt nur einmal. Die Konsole hat 0 Stück auf Lager, Maria und Jonas abonnieren, dann wird der Bestand 5, 0 und 3.

Ausgabe
E-Mail an Maria: Konsole ist verfügbar (5 Stk.)
SMS an Jonas: Konsole ist verfügbar
Ende

3. Eine Fabrik für Benachrichtigungen. Füge PushNotification hinzu und eine Fabrik für die drei Kanäle, der Groß- und Kleinschreibung egal ist. Lies eine Anzahl von Zeilen und dann Zeilen „Kanal;Adresse“. Schick an jede Adresse „Deine Reservierung ist bestätigt.“, und gib für einen unbekannten Kanal die Meldung der Exception aus.

Eingabe
4
Email;maria@example.com
sms;+49 151 2345678
push;Lauras Handy
fax;030 123456
Ausgabe
E-Mail an maria@example.com: Deine Reservierung ist bestätigt.
SMS an +49 151 2345678: Deine Reservierung ist bestätigt.
Push an Lauras Handy: Deine Reservierung ist bestätigt.
Unbekannter Kanal: fax
4. Ein altes Thermometer. Eine Klasse OldThermometer liefert die Temperatur in Fahrenheit mit ReadFahrenheit(). Das neue System arbeitet mit interface ITemperatureSensor { double Celsius(); }. Schreib ThermometerAdapter (°C = (°F − 32) × 5 / 9). Leg in eine Liste von Sensoren zwei alte Thermometer (71,6 °F und 39,2 °F) über den Adapter und einen neuen Sensor DigitalSensor, der 21,5 °C liefert. Ein Lambda geht hier nicht: Das Interface ist kein Delegate.
Ausgabe
Büro: 22.0 °C
Lager: 4.0 °C
Serverraum: 21.5 °C

Weitere Aufgaben

  • Lieferung als Strategie. Mach ein Func<decimal, decimal> für den Lieferpreis zur Packstation, an die Haustür und ab dem Laden, und setz das gewählte zusammen mit dem Rabatt in die Kasse.
  • Ein Log als Singleton. Eine Klasse Logger mit einer Property Instance und einer Methode Log(string text), die die Zeilen nummeriert. Prüf, dass die Nummern weiterlaufen, wenn du sie aus zwei verschiedenen Methoden aufrufst.
  • Ein Preis-Observer. Füge dem Produkt ein Event für Preisänderungen hinzu und schick eine Benachrichtigung nur, wenn der Preis um mindestens 10 % fällt.

Test

Teste dich

1. Was ist ein Entwurfsmuster?
2. Jede neue Aktion im Shop fügt der Preismethode ein neues else if hinzu. Welches Muster hilft?
3. Das Produkt sagt der Website, dem Lager und dem E-Mail-System Bescheid, wenn sich der Bestand ändert, ohne zu wissen, wer sie sind. Welches Muster ist das?
4. Was verhindert Singleton?
5. Du hast eine alte Klasse, die die Temperatur in Fahrenheit liefert, und das Programm erwartet ein Interface ITemperatureSensor mit einer Methode Celsius(). Was tust du?

Die Idee

Weiter: die Muster aus der Nähe

Hier hast du fünf Muster kurz gesehen, mit Lambdas. Im Kurs „Softwaretechnik“ gibt es ein ganzes Modul über Muster: Jedes beginnt mit Code, der wehtut, hat eine Animation, welches Objekt welches aufruft, und sagt, wann du es nicht brauchst. Dort sind auch Builder, Decorator, Composite, Command, State, Template Method, Iterator und Repository.

Das Modul ist hier: Entwurfsmuster.

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·