Versteckte Abhängigkeiten als Designgeruch

Ursprünglicher Autor: EngineerSpock
  • Übersetzung
Mark Siman schrieb einen tollen Beitrag: "Service Locator bricht die Kapselung ab." Der Titel des Beitrags spricht für sich, dass er dem Service Locator- Muster (Anti-Pattern) gewidmet ist . Wenn ein Programmierer zufällig einen IoC-Container im Code aufruft, um die Abhängigkeit eines Objekts aufzulösen, verwendet er das Service Locator- Anti-Pattern. Mark betrachtet das folgende Beispiel:
public class OrderProcessor : IOrderProcessor
{
    public void Process(Order order)
    {
        var validator = Locator.Resolve();
        if (validator.Validate(order))
        {
            var shipper = Locator.Resolve();
            shipper.Ship(order);
        }
    }
}


Wie wir sehen können, ist die Kapselung des OrderProcessor- Typs aufgrund von zwei versteckten Abhängigkeiten, die in der Process- Methode im Hintergrund aufgelöst werden, fehlerhaft . Diese Abhängigkeiten werden im aufrufenden Code nicht angezeigt. Dies kann zur Laufzeit zu einer Ausnahme führen, wenn der Client den IoC-Container nicht ordnungsgemäß konfiguriert hat und die erforderlichen Abhängigkeiten ermittelt hat. Als Lösung für das Problem schlägt Mark vor, die Auflösung von Abhängigkeiten an den Konstruktor des Objekts zu übertragen.
public class OrderProcessor : IOrderProcessor
{
    public OrderProcessor(IOrderValidator validator, IOrderShipper shipper)
    public void Process(Order order)
}

Auf diese Weise erkennt der aufrufende Code, was das OrderProcessor- Objekt tatsächlich benötigt .

Meiner Meinung nach gibt es jedoch immer noch Szenarien, in denen versteckte Abhängigkeiten angewendet werden können. Stellen Sie sich eine WPF-Anwendung vor, in der fast jedes ViewModel die folgenden Abhängigkeiten erfordert: IEventAggregator, IProgress, IPromptCreator . Um die Bedeutung der letzten beiden Schnittstellen zu verdeutlichen , möchte ich hinzufügen, dass die IProgress- Implementierung einen Teil des lang laufenden Codes akzeptieren und ein Fenster mit einer Fortschrittsanzeige anzeigen kann. Mit IPromptCreator können Sie Fenster öffnen, die eine Bestätigung, Zustimmung oder Ablehnung erfordern (modale Dialoge). Stellen Sie sich nun vor, es gibt ViewModelsdie zusätzlich zwei (oder vielleicht drei) Abhängigkeiten erfordern, um ein Modell zu erstellen. So könnte ein ViewModel mit so vielen Abhängigkeiten aussehen:
public class PaymentViewModel: ICanPay
{
    public PaymentViewModel(IPaymentSystem paymentSystem, 
                            IRulesValidator rulesValidator, 
                            IEventAggregator aggregator, 
                            IProgress progress, 
                            IPromptCreator promptCreator)
    public void PayFor(Order order)
}

Was für ein Durcheinander! Zu viel Rauschen in der Konstruktordeklaration. Nur zwei Abhängigkeiten enthalten aus Sicht der Geschäftslogik wirklich nützliche Informationen.

Wenn wir zum Beispiel MEF verwenden, um Abhängigkeiten einzufügen, können wir Folgendes tun:
[Export]
public class PaymentViewModel : ICanPay
{
    [Import]
    protected IEventAggregator aggregator;
    [Import]
    protected IProgress progress;
    [Import]
    protected IPromptCreator promptCreator;
    public PaymentViewModel(IPaymentSystem paymentSystem, 
                            IRulesValidator rulesValidator)
    {
    }
    public void PayFor(Order order)
    {
        //use aggreagtor, progress, promptCreator
    }
}

Wir haben die Abhängigkeiten vom Konstruktor in die Felddeklaration übernommen und mit dem Attribut Import gekennzeichnet . Obwohl wir den IoC-Container nicht direkt aufrufen (obwohl MEF kein reiner IoC-Container ist), verbergen wir die Abhängigkeiten auf dieselbe Weise wie im Beispiel von Mark. Nichts hat sich wesentlich geändert. Warum finde ich diesen Code nicht so schlecht? Aus mehreren Hauptgründen:
  • ViewModels sind keine Geschäftseinheiten, sondern nur Code-Teile zum Zusammenfügen von Modellen und Ansichten . Niemand kümmert sich wirklich um die obigen Abhängigkeiten;
  • ViewModels sind keine öffentliche API und werden (in den meisten Fällen) nicht wiederverwendet.
  • In Anbetracht der beiden vorherigen Punkte können die Teammitglieder einfach zustimmen, dass alle ViewModels diese nützlichen Abhängigkeiten aufweisen, und das ist alles.
  • Diese Dienstprogrammabhängigkeiten sind als geschützt definiert. Dadurch können wir eine ViewModel- Klasse erstellen , die PaymentViewModel in den Tests erbt , und die Abhängigkeiten durch Mocks ersetzen, da wir Zugriff auf diese Felder haben. Somit verlieren wir nicht die Möglichkeit, PaymentViewModel mit Tests zu belegen . Im Beispiel von Mark (wo Service Locator verwendet wird ) war es erforderlich, den IoC-Container im Komponententestprojekt zu konfigurieren, um diese Abhängigkeiten zu sperren oder zu beenden, und diese Vorgehensweise kann für den Komponententestprozess schmerzhaft werden.

Fazit


Wie ich bereits sagte, gibt es beim Programmieren keine richtigen Antworten oder Aussagen für alle Gelegenheiten. Generell sollten wir die Verwendung des Service Locator vermeiden , da dies gegen die Kapselung verstößt, wie Mark in seinem Artikel sagt. Bewerten Sie vor der Verwendung des Service Locator den potenziellen Schaden, den Sie für das System verursachen. Wenn Sie sicher sind, dass sich die versteckte Auflösung von Abhängigkeiten nicht auf den Client-Code auswirkt (den Ihre Teamkollegen schreiben können) und das System keinen potenziellen Schaden nimmt, fahren Sie mit dem Song fort.

Jetzt auch beliebt: