web-dev-qa-db-de.com

Verwenden einer Strategie und eines Fabrikmusters mit Abhängigkeitseinspritzung

Ich arbeite an einem Nebenprojekt, um Inversion of Control und Dependency Injection und verschiedene Entwurfsmuster besser zu verstehen.

Ich frage mich, ob es Best Practices für die Verwendung von DI mit den Factory- und Strategiemustern gibt?

Meine Herausforderung entsteht, wenn für eine Strategie (aus einer Factory erstellt) unterschiedliche Parameter für jeden möglichen Konstruktor und für die Implementierung erforderlich sind. Infolgedessen deklariere ich alle möglichen Schnittstellen im Service Entry Point und leite sie durch die Anwendung weiter. Aus diesem Grund muss der Einstiegspunkt für neue und verschiedene Implementierungen von Strategieklassen geändert werden.

Ich habe unten ein paar Beispiele zur Veranschaulichung zusammengestellt. Mein Stack für dieses Projekt ist .NET 4.5/C # und Unity für IoC/DI.

In dieser Beispielanwendung habe ich eine Standardprogrammklasse hinzugefügt, die für die Annahme einer fiktiven Bestellung verantwortlich ist. Abhängig von den Bestellungseigenschaften und dem ausgewählten Versanddienstleister werden die Versandkosten berechnet. Es gibt unterschiedliche Berechnungen für UPS, DHL und Fedex. Jede Implementierung kann auf zusätzliche Dienste angewiesen sein oder auch nicht (z. B. Datenbank, API usw.).

public class Order
{
    public string ShippingMethod { get; set; }
    public int OrderTotal { get; set; }
    public int OrderWeight { get; set; }
    public int OrderZipCode { get; set; }
}

Fiktives Programm oder Dienst zur Berechnung der Versandkosten

public class Program
{
    // register the interfaces with DI container in a separate config class (Unity in this case)
    private readonly IShippingStrategyFactory _shippingStrategyFactory;

    public Program(IShippingStrategyFactory shippingStrategyFactory)
    {
        _shippingStrategyFactory = shippingStrategyFactory;
    }

    public int DoTheWork(Order order)
    {
        // assign properties just as an example
        order.ShippingMethod = "Fedex";
        order.OrderTotal = 90;
        order.OrderWeight = 12;
        order.OrderZipCode = 98109;

        IShippingStrategy shippingStrategy = _shippingStrategyFactory.GetShippingStrategy(order);
        int shippingCost = shippingStrategy.CalculateShippingCost(order);

        return shippingCost;
    }
}

// Unity DI Setup
public class UnityConfig
{
    var container = new UnityContainer();
    container.RegisterType<IShippingStrategyFactory, ShippingStrategyFactory>();
    // also register  IWeightMappingService and IZipCodePriceCalculator with implementations
}

public interface IShippingStrategyFactory
{
    IShippingStrategy GetShippingStrategy(Order order);
}

public class ShippingStrategyFactory : IShippingStrategyFactory
{
    public IShippingStrategy GetShippingStrategy(Order order)
    {
        switch (order.ShippingMethod)
        {
            case "UPS":
                return new UPSShippingStrategy();

            // The issue is that some strategies require additional parameters for the constructor
            // SHould the be resolved at the entry point (the Program class) and passed down?
            case "DHL":
                return new DHLShippingStrategy();

            case "Fedex":
                return new FedexShippingStrategy();

            default:
                throw new NotImplementedException(); 
        }
    }
}

Jetzt für die Strategie-Schnittstelle und -Implementierungen. UPS ist eine einfache Berechnung, während DHL und Fedex unterschiedliche Dienste (und andere Konstruktorparameter) benötigen.

public interface IShippingStrategy
{
    int CalculateShippingCost(Order order);
}

public class UPSShippingStrategy : IShippingStrategy()
{
    public int CalculateShippingCost(Order order)
    {
        if (order.OrderWeight < 5)
            return 10; // flat rate of $10 for packages under 5 lbs
        else
            return 20; // flat rate of $20
    }
}

public class DHLShippingStrategy : IShippingStrategy()
{
    private readonly IWeightMappingService _weightMappingService;

    public DHLShippingStrategy(IWeightMappingService weightMappingService)
    {
        _weightMappingService = weightMappingService;
    }

    public int CalculateShippingCost(Order order)
    {
        // some sort of database call needed to lookup pricing table and weight mappings
        return _weightMappingService.DeterminePrice(order);
    }
}

public class FedexShippingStrategy : IShippingStrategy()
{
    private readonly IZipCodePriceCalculator _zipCodePriceCalculator;

    public FedexShippingStrategy(IZipCodePriceCalculator zipCodePriceCalculator)
    {
        _zipCodePriceCalculator = zipCodePriceCalculator;
    }

    public int CalculateShippingCost(Order order)
    {
        // some sort of dynamic pricing based on zipcode
        // api call to a Fedex service to return dynamic price
        return _zipCodePriceService.CacluateShippingCost(order.OrderZipCode);
    }
}

Das Problem mit dem oben genannten ist, dass für jede Strategie zusätzliche und andere Dienste erforderlich sind, um die Methode 'CalculateShippingCost' auszuführen. Müssen diese Schnittstellen/Implementierungen beim Einstiegspunkt (der Programmklasse) registriert und durch die Konstruktoren weitergegeben werden?

Gibt es andere Muster, die das obige Szenario besser erfüllen könnten? Vielleicht etwas, das Unity speziell behandeln könnte ( https://msdn.Microsoft.com/en-us/library/dn178463(v=pandp.30).aspx )?

Ich freue mich über jede Hilfe oder einen Nudge in die richtige Richtung.

Danke, Andy

7
apleroy

Es gibt verschiedene Möglichkeiten, dies zu tun, aber ich bevorzuge es, eine Liste der verfügbaren Strategien in Ihre Fabrik einzufügen und diese dann zu filtern, um die für Sie interessanten Ziele anzuzeigen.

An Ihrem Beispiel würde ich IShippingStrategy ändern, um eine neue Eigenschaft hinzuzufügen:

public interface IShippingStrategy
{
    int CalculateShippingCost(Order order);
    string SupportedShippingMethod { get; }
}

Dann würde ich die Fabrik so implementieren:

public class ShippingStrategyFactory : IShippingStrategyFactory
{
    private readonly IEnumerable<IShippingStrategy> availableStrategies;

    public ShippingStrategyFactory(IEnumerable<IShippingStrategy> availableStrategies)
    {
        this.availableStrategies = availableStrategies;
    }

    public IShippingStrategy GetShippingStrategy(Order order)
    {
        var supportedStrategy = availableStrategies
                .FirstOrDefault(x => x.SupportedShippingMethod == order.ShippingMethod);
        if (supportedStrategy == null)
        {
            throw new InvalidOperationException($"No supported strategy found for shipping method '{order.ShippingMethod}'.");
        }

        return supportedStrategy;
    }
}

Der Hauptgrund, warum ich es so benutze, ist, dass ich niemals zurückkommen und die Fabrik modifizieren muss. Wenn ich jemals eine neue Strategie implementieren muss, muss die Fabrik nicht geändert werden. Wenn Sie die automatische Registrierung für Ihren Container verwenden, müssen Sie die neue Strategie auch nicht registrieren. Sie müssen also nur mehr Zeit für das Schreiben von neuem Code aufwenden.

7
John H

Beim Anwenden der Abhängigkeitseinspritzung definieren wir alle Abhängigkeiten der Klasse als erforderliche Argumente im Konstruktor. Diese Praxis wird als Konstruktorinjektion bezeichnet. Dies bedeutet, dass die Abhängigkeit von der Klasse zum Konsumenten entsteht. Die gleiche Regel gilt jedoch auch für die Verbraucher der Klasse. Sie müssen auch ihre Abhängigkeiten in ihrem Konstruktor definieren. Dies geht den Call Stack ganz nach oben und das bedeutet, dass sogenannte "Objektgraphen" an einigen Stellen ziemlich tief werden können.

Abhängigkeitsinjektion bewirkt, dass Klassen bis zum Einstiegspunkt der Anwendung erstellt werden. die Kompositionswurzel . Dies bedeutet jedoch, dass der Einstiegspunkt alle Abhängigkeiten kennen muss. Wenn wir keine DI Container -a-Übung namens Pure DI - verwenden, bedeutet dies, dass an dieser Stelle alle Abhängigkeiten in einfachem alten C # -Code erstellt werden müssen. Wenn wir einen DI-Container verwenden, müssen wir dem DI-Container noch alle Abhängigkeiten mitteilen.

Manchmal können wir jedoch eine Technik namens batch oder Auto-Registration verwenden, bei der der DI-Container mithilfe von Convention over Configuration Reflection für unsere Projekte und Registertypen verwendet. Dies erspart uns den Aufwand, alle Typen nacheinander zu registrieren, und verhindert häufig, dass wir bei jedem Hinzufügen einer neuen Klasse zum Composition Root Änderungen am Composition Root vornehmen.

Müssen diese Schnittstellen/Implementierungen beim Einstiegspunkt (der Programmklasse) registriert und durch die Konstruktoren weitergegeben werden?

Absolut.

Infolgedessen deklariere ich alle möglichen Schnittstellen im Service Entry Point und leite sie durch die Anwendung weiter. Aus diesem Grund muss der Einstiegspunkt für neue und verschiedene Implementierungen von Strategieklassen geändert werden.

Der Einstiegspunkt der Anwendung ist der volatilste Teil des Systems. Es ist immer, auch ohne DI. Mit DI machen wir den Rest des Systems jedoch weniger flüchtig. Wir können die Menge an Codeänderungen, die wir am Einstiegspunkt vornehmen müssen, reduzieren, indem Sie Auto-Registration anwenden.

Ich frage mich, ob es Best Practices für die Verwendung von DI mit den Fabrik- und Strategiemustern gibt?

Ich würde sagen, die beste Praxis in Bezug auf Fabriken besteht darin, so wenig wie möglich von ihnen zu haben, wie in diesem Artikel erläutert. Tatsächlich ist Ihre werkseitige Schnittstelle redundant und erschwert nur die Verbraucher, die dies erfordern (wie in diesem Artikel beschrieben). Auf Ihre Anwendung kann problemlos verzichtet werden, und Sie können stattdessen direkt eine IShippingStrategy einfügen, da dies das einzige ist, woran der Verbraucher interessiert ist: die Versandkosten für eine Bestellung zu erhalten. Es ist egal, ob eine oder Dutzende Implementierungen dahinter stehen. Es will nur die Versandkosten bekommen und seine Arbeit fortsetzen:

public int DoTheWork(Order order)
{
    // assign properties just as an example
    order.ShippingMethod = "Fedex";
    order.OrderTotal = 90;
    order.OrderWeight = 12;
    order.OrderZipCode = 98109;

    return shippingStrategy.CalculateShippingCost(order);
}

Dies bedeutet jedoch, dass die eingespritzte Versandstrategie nun etwas sein muss, das entscheiden kann, wie die Kosten basierend auf der Order.Method-Eigenschaft berechnet werden. Es gibt jedoch ein Muster dafür, das Proxy-Muster. Hier ist ein Beispiel:

public class ShippingStrategyProxy : IShippingStrategy
{
    private readonly DHLShippingStrategy _dhl;
    private readonly UPSShippingStrategy _ups;
    //...

    public ShippingStrategyProxy(DHLShippingStrategy dhl, UPSShippingStrategy ups, ...)
    {
        _dhl = dhl;
        _ups = ups;
        //...
    }

    public int CalculateShippingCost(Order order) => 
        GetStrategy(order.Method).CalculateShippingCost(order);

    private IShippingStrategy GetStrategy(string method)
    {
        switch (method)
        {
            case "DHL": return dhl;
            case "UPS": return ups:
            //...
            default: throw InvalidOperationException(method);
        }
    }
}

Dieser Proxy wirkt intern wie eine Factory, aber hier gibt es zwei wichtige Unterschiede:

  1. Es definiert keine andere Schnittstelle. Dies erlaubt dem Konsumenten, nur eine Abhängigkeit von einem Konzept zu übernehmen: der IShippingStrategy.
  2. Die Strategien selbst werden nicht erstellt. Sie werden immer noch eingespritzt.

Dieser Proxy leitet den eingehenden Anruf einfach an eine zugrunde liegende Strategieimplementierung weiter, die die eigentliche Arbeit erledigt.

Es gibt verschiedene Möglichkeiten, einen solchen Proxy zu implementieren. Beispielsweise können Sie die Abhängigkeiten hier noch manuell erstellen, oder Sie können den Aufruf an den Container weiterleiten, der die Abhängigkeiten für Sie erstellt. Die Art, wie Sie die Abhängigkeiten einfügen, kann auch davon abhängen, was für Ihre Anwendung am besten ist.

Und obwohl ein solcher Stellvertreter intern wie eine Fabrik funktionieren könnte, ist es wichtig, dass es hier keine Fabrik Abstraction gibt; das würde die Verbraucher nur komplizieren.

Alles oben Erörterte wird in dem Buch Prinzips, Praktiken und Muster der Abhängigkeitsinjektion von Mark Seemann und mir ausführlicher erörtert. Zum Beispiel wird Composition Root in § 4.1, Constructor Injection in § 4.2, Missbrauch von Abstract Factories in § 6.2 und Auto-Registration in Kapitel 12 behandelt .

12
Steven

Also habe ich es so gemacht. Ich hätte es vorgezogen, ein IDictionary zu injizieren, aber wegen der Einschränkung beim Einfügen von "IEnumerable" in den Konstruktor (diese Einschränkung ist Unity-spezifisch), kam ich zu einer kleinen Problemumgehung. 

public interface IShipper
{
    void ShipOrder(Order ord);

    string FriendlyNameInstance { get;} /* here for my "trick" */
}

..

public interface IOrderProcessor
{
    void ProcessOrder(String preferredShipperAbbreviation, Order ord);
}

..

public class Order
{
}

..

public class FedExShipper : IShipper
{
    private readonly Common.Logging.ILog logger;

    public static readonly string FriendlyName = typeof(FedExShipper).FullName; /* here for my "trick" */

    public FedExShipper(Common.Logging.ILog lgr)
    {
        if (null == lgr)
        {
            throw new ArgumentOutOfRangeException("Log is null");
        }

        this.logger = lgr;
    }

    public string FriendlyNameInstance => FriendlyName; /* here for my "trick" */

    public void ShipOrder(Order ord)
    {
        this.logger.Info("I'm shipping the Order with FedEx");
    }

..

public class UpsShipper : IShipper
{
    private readonly Common.Logging.ILog logger;

    public static readonly string FriendlyName = typeof(UpsShipper).FullName; /* here for my "trick" */

    public UpsShipper(Common.Logging.ILog lgr)
    {
        if (null == lgr)
        {
            throw new ArgumentOutOfRangeException("Log is null");
        }

        this.logger = lgr;
    }

    public string FriendlyNameInstance => FriendlyName; /* here for my "trick" */

    public void ShipOrder(Order ord)
    {
        this.logger.Info("I'm shipping the Order with Ups");
    }
}

..

public class UspsShipper : IShipper
{
    private readonly Common.Logging.ILog logger;

    public static readonly string FriendlyName = typeof(UspsShipper).FullName; /* here for my "trick" */

    public UspsShipper(Common.Logging.ILog lgr)
    {
        if (null == lgr)
        {
            throw new ArgumentOutOfRangeException("Log is null");
        }

        this.logger = lgr;
    }

    public string FriendlyNameInstance => FriendlyName; /* here for my "trick" */

    public void ShipOrder(Order ord)
    {
        this.logger.Info("I'm shipping the Order with Usps");
    }
}

..

public class OrderProcessor : IOrderProcessor
{
    private Common.Logging.ILog logger;
    //IDictionary<string, IShipper> shippers; /*   :(    I couldn't get IDictionary<string, IShipper>  to work */
    IEnumerable<IShipper> shippers;

    public OrderProcessor(Common.Logging.ILog lgr, IEnumerable<IShipper> shprs)
    {
        if (null == lgr)
        {
            throw new ArgumentOutOfRangeException("Log is null");
        }

        if (null == shprs)
        {
            throw new ArgumentOutOfRangeException("ShipperInterface(s) is null");
        }

        this.logger = lgr;
        this.shippers = shprs;
    }

    public void ProcessOrder(String preferredShipperAbbreviation, Order ord)
    {
        this.logger.Info(String.Format("About to ship. ({0})", preferredShipperAbbreviation));

        /* below foreach is not needed, just "proves" everything was injected */
        foreach (IShipper sh in shippers)
        {
            this.logger.Info(String.Format("ShipperInterface . ({0})", sh.GetType().Name));
        }

        IShipper foundShipper = this.FindIShipper(preferredShipperAbbreviation);
        foundShipper.ShipOrder(ord);
    }


    private IShipper FindIShipper(String preferredShipperAbbreviation)
    {

        IShipper foundShipper = this.shippers.FirstOrDefault(s => s.FriendlyNameInstance.Equals(preferredShipperAbbreviation, StringComparison.OrdinalIgnoreCase));

        if (null == foundShipper)
        {
            throw new ArgumentNullException(
                String.Format("ShipperInterface not found in shipperProviderMap. ('{0}')", preferredShipperAbbreviation));
        }

        return foundShipper;
    }
}

...

Und Aufrufcode: (das wäre zum Beispiel in "Program.cs")

            Common.Logging.ILog log = Common.Logging.LogManager.GetLogger(typeof(Program));

            IUnityContainer cont = new UnityContainer();

            cont.RegisterInstance<ILog>(log);

            cont.RegisterType<IShipper, FedExShipper>(FedExShipper.FriendlyName);
            cont.RegisterType<IShipper, UspsShipper>(UspsShipper.FriendlyName);
            cont.RegisterType<IShipper, UpsShipper>(UpsShipper.FriendlyName);

            cont.RegisterType<IOrderProcessor, OrderProcessor>();

            Order ord = new Order();
            IOrderProcessor iop = cont.Resolve<IOrderProcessor>();
            iop.ProcessOrder(FedExShipper.FriendlyName, ord);

Protokollierungsausgabe:

2018/09/21 08:13:40:556 [INFO]  MyNamespace.Program - About to ship. (MyNamespace.Bal.Shippers.FedExShipper)
2018/09/21 08:13:40:571 [INFO]  MyNamespace.Program - ShipperInterface . (FedExShipper)
2018/09/21 08:13:40:572 [INFO]  MyNamespace.Program - ShipperInterface . (UspsShipper)
2018/09/21 08:13:40:572 [INFO]  MyNamespace.Program - ShipperInterface . (UpsShipper)
2018/09/21 08:13:40:573 [INFO]  MyNamespace.Program - I'm shipping the Order with FedEx

Daher hat jeder Beton eine statische Zeichenfolge, die seinen Namen auf eine stark typisierte Weise liefert. ("FriendlyName")

Und dann habe ich eine Instanz string-get -Eigenschaft, die genau denselben Wert verwendet, um die Dinge synchron zu halten. ("FriendlyNameInstance")

Durch Erzwingen des Problems durch Verwenden einer Eigenschaft in der Schnittstelle (unter Teilcode)

public interface IShipper
{
   string FriendlyNameInstance { get;}
}

Ich kann damit meinen Versender aus der Sammlung der Versender "finden".

Die interne Methode "FindIShipper" ist eine Art-Factory, aber die Notwendigkeit einer separaten IShipperFactory- und ShipperFactory-Schnittstelle und -Klasse ist nicht erforderlich. Dadurch wird das Gesamtsetup vereinfacht. Und noch ehrt Constructor-Injection und Composition root .

Wenn jemand weiß, wie IDictionary<string, IShipper> verwendet wird (und über den Konstruktor injiziert wird), lass es mich wissen.

Aber meine Lösung funktioniert ... mit einem kleinen Glanz.

...........................

Meine Drittanbieter-DLL-Abhängigkeitsliste. (Ich verwende Dotnet-Core, aber das Dotnet-Framework mit einer halb neuen Version von Unity sollte ebenfalls funktionieren.) (Siehe PackageReference's weiter unten)

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp2.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Common.Logging" Version="3.4.1" />
    <PackageReference Include="Microsoft.Extensions.Configuration" Version="2.1.1" />
    <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="2.1.1" />
    <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.1.1" />
    <PackageReference Include="Unity" Version="5.8.11" />
  </ItemGroup>

  <ItemGroup>
    <None Update="appsettings.json">
      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </None>
  </ItemGroup>

</Project>
2
granadaCoder

Registrieren Sie sie und lösen Sie sie mithilfe Ihrer Strings für Strategietypen.

So was:

// Create container and register types
IUnityContainer myContainer = new UnityContainer();
myContainer.RegisterType<IShippingStrategy, FedexShippingStrategy>("Fedex");
myContainer.RegisterType<IShippingStrategy, DHLShippingStrategy>("DHL");

// Retrieve an instance of each type
IShippingStrategy shipping = myContainer.Resolve<IShippingStrategy>("DHL");
IShippingStrategy shipping = myContainer.Resolve<IShippingStrategy>("Fedex");
1
Silas Reinagel

Siehe die Antworten von John H und Silas Reinagel. Sie sind beide sehr hilfreich.

Am Ende habe ich eine Kombination aus beiden Antworten gemacht.

Ich habe die Factory und das Interface aktualisiert, wie John H. erwähnt.

Im Unity-Container fügte ich die Implementierungen mit den neuen benannten Parametern hinzu, die Silas Reinagel zeigt.

Ich folgte der Antwort hier, wie Sie Unity verwenden, um die Sammlung für die Injektion in die Strategiefabrik zu registrieren. Möglichkeit, die Sammlung mit Unity zu füllen

Jetzt kann jede Strategie separat implementiert werden, ohne dass Upstream geändert werden muss.

Danke euch allen.

1
apleroy