Designmuster in C# (Factory, Singleton, Repository, Observer)
Lernen Sie Designmuster in C#, wie Factory, Singleton und Repository, für flexible und wartbare Anwendungen.
Entwurfsmuster (Design Patterns) bieten standardisierte und bewährte Lösungen für wiederkehrende Probleme in der Softwareentwicklung. Häufig verwendete Muster in C# sind Factory, Singleton, Repository, Observer, Strategy, Adapter und Decorator. Dieser Artikel untersucht die Kernkonzepte dieser Muster, ihre Anwendungsfälle und C#-Beispiele.
Factory Pattern
Das Factory-Muster zentralisiert die Objekterstellung.
Anstatt überall new zu verwenden, wird die Verantwortung für die Objekterzeugung einer Factory-Klasse übertragen.
public interface IProduct { void DoWork(); }
public class ProductA : IProduct
{
public void DoWork() => Console.WriteLine("Produkt A");
}
public class ProductB : IProduct
{
public void DoWork() => Console.WriteLine("Produkt B");
}
public static class ProductFactory
{
public static IProduct Create(string type) => type switch
{
"A" => new ProductA(),
"B" => new ProductB(),
_ => throw new ArgumentException("Ungültiger Typ")
};
}
Singleton Pattern
Das Singleton-Muster stellt sicher, dass nur eine einzige Instanz einer Klasse erstellt wird. Es wird häufig für Logging, Caching oder Konfigurationsobjekte verwendet.
public class Singleton
{
private static Singleton? _instance;
private static readonly object _lock = new();
private Singleton() { }
public static Singleton Instance
{
get
{
lock (_lock)
return _instance ??= new Singleton();
}
}
}
Repository Pattern
Das Repository-Muster abstrahiert die Datenschicht. Auf diese Weise wird die Geschäftslogikschicht unabhängig von den Details der Datenbank.
public class Product { public int Id; public string Name = ""; }
public interface IProductRepository
{
void Add(Product p);
Product? GetById(int id);
}
public class InMemoryProductRepository : IProductRepository
{
private readonly List<Product> _list = new();
public void Add(Product p) => _list.Add(p);
public Product? GetById(int id) => _list.FirstOrDefault(x => x.Id == id);
}
Observer Pattern
Das Observer-Muster etabliert eine Publisher–Subscriber-Beziehung. Wenn sich ein Objekt ändert, werden alle Abonnenten benachrichtigt. In C# wird dies durch den Event/Delegate-Mechanismus implementiert.
public class NewsAgency
{
public event EventHandler<string>? NewsPublished;
public void Publish(string news)
{
Console.WriteLine($"Nachricht: {news}");
NewsPublished?.Invoke(this, news);
}
}
Strategy Pattern
Das Strategy-Muster kapselt Algorithmen so, dass sie austauschbar sind. Die Client-Klasse muss nicht wissen, welcher Algorithmus verwendet wird; die Strategie wird extern bereitgestellt.
public interface IPaymentStrategy { void Pay(decimal amount); }
public class CreditCardPayment : IPaymentStrategy
{
public void Pay(decimal amount) => Console.WriteLine($"{amount} EUR mit Kreditkarte bezahlt.");
}
public class PayPalPayment : IPaymentStrategy
{
public void Pay(decimal amount) => Console.WriteLine($"{amount} EUR mit PayPal bezahlt.");
}
public class Checkout
{
private readonly IPaymentStrategy _strategy;
public Checkout(IPaymentStrategy strategy) => _strategy = strategy;
public void ProcessOrder(decimal total) => _strategy.Pay(total);
}
Adapter Pattern
Das Adapter-Muster ermöglicht es, dass zwei inkompatible Klassen zusammenarbeiten. Es wird oft verwendet, um alte Bibliotheken oder APIs in neue Systeme zu integrieren.
// Altes System
public class LegacyPrinter { public void PrintText(string t) => Console.WriteLine(t); }
// Neue Schnittstelle
public interface IPrinter { void Print(string text); }
// Adapter
public class PrinterAdapter : IPrinter
{
private readonly LegacyPrinter _legacy;
public PrinterAdapter(LegacyPrinter legacy) => _legacy = legacy;
public void Print(string text) => _legacy.PrintText(text);
}
Decorator Pattern
Das Decorator-Muster ermöglicht es, bestehenden Objekten dynamisch neues Verhalten hinzuzufügen. Dies ist ein kompositionsbasierter Ansatz anstelle von Vererbung.
public interface IMessage { string GetText(); }
public class SimpleMessage : IMessage
{
private readonly string _text;
public SimpleMessage(string text) => _text = text;
public string GetText() => _text;
}
// Decorator
public class EncryptedMessage : IMessage
{
private readonly IMessage _inner;
public EncryptedMessage(IMessage inner) => _inner = inner;
public string GetText() => Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(_inner.GetText()));
}
Vorteile
- Factory: Abstrahiert die Objekterstellung, erhöht die Flexibilität.
- Singleton: Verwaltet gemeinsame Ressourcen mit einer einzigen Instanz.
- Repository: Abstrahiert den Datenzugriff, verbessert die Testbarkeit.
- Observer: Etabliert Publisher–Subscriber-Beziehungen und sorgt für lose Kopplung.
- Strategy: Macht Algorithmen austauschbar.
- Adapter: Integriert inkompatible Systeme.
- Decorator: Ermöglicht das dynamische Hinzufügen von neuem Verhalten.
Kurz & Knapp (TL;DR)
- Factory: Abstrahiert die Objekterstellung.
- Singleton: Garantiert eine einzige Instanz.
- Repository: Abstrahiert den Datenzugriff.
- Observer: Benachrichtigt Abonnenten über Änderungen.
- Strategy: Algorithmen können leicht gewechselt werden.
- Adapter: Passt inkompatible Klassen an.
- Decorator: Fügt dynamisch neue Funktionen hinzu.
Beispiel: Produkterstellung, Auflistung, Bestellablauf und Protokollierung
Das folgende Beispiel zeigt Produkterstellung (Factory), Auflistung (Repository),
Bestellerstellung und Statusaktualisierungen (Observer), Zahlung (Strategy),
Belegdruck (Adapter) und Nachrichtenverarbeitung (Decorator) zusammen.
Es verwendet eine sichere Singleton-Konfiguration mit Lazy, eine enum-basierte Factory, benutzerdefinierte EventArgs
und kulturabhängige Währungsformatierung.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
// =======================
// DOMAIN (Kleines Modell)
// =======================
// Produkttypen als Enum statt Strings ausdrücken, um Fehler zu reduzieren.
public enum ProductType { Book, Phone }
// Einfacher, unveränderlicher Datenträger (record type)
public record Product(int Id, string Name, decimal Price);
// =======================
// REPOSITORY (Speicherung)
// =======================
public interface IProductRepository
{
void Add(Product p);
Product? GetById(int id);
IEnumerable<Product> GetAll(); // IEnumerable zurückgeben; interne Liste nicht direkt exponieren.
}
public class InMemoryProductRepository : IProductRepository
{
private readonly List<Product> _list = new();
public void Add(Product p) => _list.Add(p);
public Product? GetById(int id) => _list.FirstOrDefault(x => x.Id == id);
// Falls man eine schützende Kopie zurückgeben möchte: return _list.ToList();
public IEnumerable<Product> GetAll() => _list;
}
// =======================
// FACTORY (Produkterstellung)
// =======================
public static class ProductFactory
{
// Zentrale Erstellung: Bei neuen Typen einfach im switch erweitern.
public static Product Create(ProductType type, int id)
=> type switch
{
ProductType.Book => new Product(id, $"Buch-{id}", 29.90m),
ProductType.Phone => new Product(id, $"Telefon-{id}", 599.00m),
_ => throw new ArgumentOutOfRangeException(nameof(type))
};
}
// =======================
// SINGLETON EINSTELLUNGEN
// =======================
// Thread-sicheres, Lazy-geladenes, einfaches Singleton mit Lazy<T>.
public sealed class AppSettings
{
private static readonly Lazy<AppSettings> _lazy = new(() => new AppSettings());
public static AppSettings Instance => _lazy.Value;
private AppSettings() { }
public string StoreName { get; init; } = "Mein Geschäft";
}
// =======================
// OBSERVER (Ereignismodell)
// =======================
// Benutzerdefinierte EventArgs: erweiterbare Struktur anstelle eines einfachen Strings.
public sealed class OrderStatusChangedEventArgs : EventArgs
{
public string Status { get; }
public DateTime When { get; }
public OrderStatusChangedEventArgs(string status, DateTime when)
{
Status = status;
When = when;
}
}
public class Order
{
public event EventHandler<OrderStatusChangedEventArgs>? StatusChanged;
public int Id { get; }
public Product Product { get; }
public Order(int id, Product product)
{
Id = id;
Product = product;
}
public void UpdateStatus(string status)
{
Console.WriteLine($"Bestellung {Id} Status: {status}");
StatusChanged?.Invoke(this, new OrderStatusChangedEventArgs(status, DateTime.Now));
}
}
// Ereignis-Abonnenten (Listener)
public class EmailNotifier
{
public void OnOrderStatusChanged(object? sender, OrderStatusChangedEventArgs e)
=> Console.WriteLine($"[E-Mail] Benachrichtigung an Kunden gesendet: {e.Status} ({e.When:T})");
}
public class SmsNotifier
{
public void OnOrderStatusChanged(object? sender, OrderStatusChangedEventArgs e)
=> Console.WriteLine($"[SMS] Benachrichtigung an Kunden gesendet: {e.Status} ({e.When:T})");
}
// =======================
// STRATEGY (Zahlung)
// =======================
public interface IPaymentStrategy
{
void Pay(decimal amount);
}
public class CreditCardPayment : IPaymentStrategy
{
public void Pay(decimal amount) => Console.WriteLine($"{amount:0.00} mit Kreditkarte bezahlt.");
}
public class PayPalPayment : IPaymentStrategy
{
public void Pay(decimal amount) => Console.WriteLine($"{amount:0.00} mit PayPal bezahlt.");
}
// =======================
// ADAPTER (Belegdruck)
// =======================
// Alte API
public class LegacyPrinter
{
public void PrintText(string t) => Console.WriteLine("[LegacyPrinter] " + t);
}
// Neues Interface
public interface IPrinter
{
void Print(string text);
}
// Adapter: Verbindet das neue Interface mit der alten API.
public class PrinterAdapter : IPrinter
{
private readonly LegacyPrinter _legacy;
public PrinterAdapter(LegacyPrinter legacy) => _legacy = legacy;
public void Print(string text) => _legacy.PrintText(text);
}
// =======================
// DECORATOR (Nachricht)
// =======================
public interface IMessage
{
string GetText();
}
public class SimpleMessage : IMessage
{
private readonly string _text;
public SimpleMessage(string text) => _text = text;
public string GetText() => _text;
}
// Dekorator, der die Nachricht protokolliert
public class LoggedMessageDecorator : IMessage
{
private readonly IMessage _inner;
public LoggedMessageDecorator(IMessage inner) => _inner = inner;
public string GetText()
{
var text = _inner.GetText();
var preview = text.Length > 60 ? text.Substring(0, 60) + "..." : text;
Console.WriteLine("[Log] Nachricht erstellt (Vorschau): " + preview);
return text;
}
}
// Einfacher Dekorator, der die Nachricht mit Base64 „verschlüsselt“
public class EncryptedMessageDecorator : IMessage
{
private readonly IMessage _inner;
public EncryptedMessageDecorator(IMessage inner) => _inner = inner;
public string GetText()
{
var raw = _inner.GetText();
return Convert.ToBase64String(Encoding.UTF8.GetBytes(raw));
}
}
// =======================
// ANWENDUNG (Demo-Ablauf)
// =======================
class Program
{
static void Main()
{
// Währungsformatierung mit de-DE Kultur
var deDE = new CultureInfo("de-DE");
Console.WriteLine($"Willkommen bei: {AppSettings.Instance.StoreName}");
Console.WriteLine();
// Produkte über Repository + Factory hinzufügen
IProductRepository repo = new InMemoryProductRepository();
var p1 = ProductFactory.Create(ProductType.Book, 1);
var p2 = ProductFactory.Create(ProductType.Phone, 2);
repo.Add(p1);
repo.Add(p2);
// Produkte auflisten (kultursensitiver Preis)
Console.WriteLine("Produkte auf Lager:");
foreach (var p in repo.GetAll())
Console.WriteLine($"- {p.Id}: {p.Name} {p.Price.ToString("C", deDE)}");
Console.WriteLine();
// Bestellung erstellen und Ereignisse abonnieren
var order = new Order(1001, p1);
var email = new EmailNotifier();
var sms = new SmsNotifier();
// Listener zu Ereignissen hinzufügen
order.StatusChanged += email.OnOrderStatusChanged;
order.StatusChanged += sms.OnOrderStatusChanged;
// Zahlung (Strategy)
IPaymentStrategy payment = new CreditCardPayment();
Console.WriteLine("Zahlung wird verarbeitet:");
payment.Pay(order.Product.Price);
Console.WriteLine();
// Bestelllebenszyklus (Observer wird ausgelöst)
order.UpdateStatus("Genehmigt");
order.UpdateStatus("Versandt");
// (Optional) SMS-Listener abmelden
order.StatusChanged -= sms.OnOrderStatusChanged;
order.UpdateStatus("Geliefert");
Console.WriteLine();
// Beleg drucken (alte Drucker-API über Adapter nutzen)
IPrinter printer = new PrinterAdapter(new LegacyPrinter());
printer.Print($"Beleg für Bestellung #{order.Id}: {order.Product.Name} - {order.Product.Price.ToString("C", deDE)}");
Console.WriteLine();
// Nachrichtenverarbeitung (Decorator)
// Ziel: Klartext protokollieren, dann verschlüsseln, bevor gespeichert/gesendet wird.
// Deshalb zuerst Logged, dann Encrypted Dekorator verketten.
IMessage msg = new SimpleMessage($"Vielen Dank für Ihren Kauf: {order.Product.Name}!");
msg = new LoggedMessageDecorator(msg); // 1) Log: KLARTEXT protokollieren
msg = new EncryptedMessageDecorator(msg); // 2) Verschlüsseln: Verschlüsselten Text ausgeben
var finalText = msg.GetText();
Console.WriteLine("Endgültige Nachricht zum Speichern/Senden (Base64):");
Console.WriteLine(finalText);
Console.WriteLine();
// Strategie zur Laufzeit ändern
payment = new PayPalPayment();
Console.WriteLine("Zweite Zahlung mit einer anderen Strategie:");
payment.Pay(199.99m);
Console.WriteLine("\nDemo abgeschlossen.");
}
}
Ähnliche Artikel
Dependency Injection Grundlagen in C#
Lernen Sie die Grundlagen von Dependency Injection in C#, um Abhängigkeiten zu verwalten und lose Kopplung zu erreichen.
Interfaces und Abstrakte Klassen in C#
Lernen Sie Interfaces und abstrakte Klassen in C#, ihre Unterschiede und den Einsatz für sauberes, erweiterbares Design.
Kapselung, Vererbung und Polymorphismus in C#
Lernen Sie Kapselung, Vererbung und Polymorphismus in C# mit Beispielen, um zentrale OOP-Konzepte sicher anzuwenden.
SOLID-Prinzipien mit C#
SOLID-Prinzipien mit C#-Beispielen: flexiblen, wartbaren und testbaren Code erstellen.