Wird geladen...

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


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