Using a Strategy and Factory Pattern with Dependen

2020-02-26 07:37发布

I am working on a side project to better understand Inversion of Control and Dependency Injection and different design patterns.

I am wondering if there are best practices to using DI with the factory and strategy patterns?

My challenge comes about when a strategy (built from a factory) requires different parameters for each possible constructor and implementation. As a result I find myself declaring all possible interfaces in the service entry point, and passing them down through the application. As a result, the entry point must be changed for new and various strategy class implementations.

I have put together a paired down example for illustration purposes below. My stack for this project is .NET 4.5/C# and Unity for IoC/DI.

In this example application, I have added a default Program class that is responsible for accepting a fictitious order, and depending on the order properties and the shipping provider selected, calculating the shipping cost. There are different calculations for UPS, DHL, and Fedex, and each implemnentation may or may not rely on additional services (to hit a database, api, etc).

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

Fictitious program or service to calculate shipping cost

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(); 
        }
    }
}

Now for the Strategy interface and implementations. UPS is an easy calculation, while DHL and Fedex may require different services (and different constructor parameters).

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);
    }
}

The issue with the above is that each strategy requires additional and different services to perform the 'CalculateShippingCost' method. Do these interfaces/implementations need to be registered with the entry point (the Program class) and passed down through the constructors?

Are there other patterns that would be a better fit to accomplish the above scenario? Maybe something that Unity could handle specifically (https://msdn.microsoft.com/en-us/library/dn178463(v=pandp.30).aspx)?

I greatly appreciate any help or a nudge in the right direction.

Thanks, Andy

5条回答
smile是对你的礼貌
2楼-- · 2020-02-26 08:14

Please see both John H and Silas Reinagel's answers. They are both very helpful.

I ended up doing a combination of both answers.

I updated the factory and interface as John H mentions.

And then in the Unity container, I added the implementations with the new named parameters like Silas Reinagel shows.

I then followed the answer here for using Unity to register the collection for injection into the strategy factory. Way to fill collection with Unity

Now each strategy can be implemented separately without needing to modify upstream.

Thank you all.

查看更多
贼婆χ
3楼-- · 2020-02-26 08:23

Register and resolve them using your Strategy Type strings.

Like this:

// 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");
查看更多
兄弟一词,经得起流年.
4楼-- · 2020-02-26 08:25

There are a few ways of doing this, but the way I prefer is to inject a list of available strategies into your factory, and then filtering them to return the one(s) you're interested in.

Working with your example, I'd modify IShippingStrategy to add a new property:

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

Then I'd implement the factory like so:

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;
    }
}

The main reason I like using it this way is that I never have to come back and modify the factory. If ever I have to implement a new strategy, the factory doesn't have to be changed. If you're using auto-registration with your container, you don't even have to register the new strategy either, so it's simply a case of allowing you to spend more time writing new code.

查看更多
放荡不羁爱自由
5楼-- · 2020-02-26 08:26

When applying Dependency Injection, we define all the class’s dependencies as required arguments in the constructor. This practice is called Constructor Injection. This pushes the burden of creating the dependency from the class to its consumer. The same rule however applies to the class’s consumers as well. They as well need to define their dependencies in their constructor. This goes all the way up the call stack, and this means that so called 'object graphs' can become quite deep at some points.

Dependency Injection causes the responsibility of creating classes all the way up to the entry point of the application; the Composition Root. This does mean however that the entry point needs to know about all the dependencies. In case we don't use a DI Container -a practice called Pure DI- it means that at this point all dependencies must be created in plain old C# code. In case we use a DI Container we still have to tell the DI container about all dependencies.

Sometimes however we can make use of a technique called batch or Auto-Registration, where the DI Container will use reflection over our projects and register types using Convention over Configuration. This saves us the burden of registering all types one by one and often prevents us from making changes to the Composition Root every time a new class is added to the system.

Do these interfaces/implementations need to be registered with the entry point (the Program class) and passed down through the constructors?

Absolutely.

As a result I find myself declaring all possible interfaces in the service entry point, and passing them down through the application. As a result, the entry point must be changed for new and various strategy class implementations.

The application's entry point is the most volatile part of the system. It always is, even without DI. But with DI we make the rest of the system much less volatile. Again, we can reduce the amount of code changes we need to make at the entry point by applying Auto-Registration.

I am wondering if there are best practices to using DI with the factory and strategy patterns?

I would say the best practice concerning factories is to have as little of them as possible, as explained in this article. As a matter of fact, your factory interface is redundant and only complicates the consumers that require it (as explained in the article). Your application can easily do without and you can instead inject a IShippingStrategy directly, since this is the only thing the consumer is interested in: to get the shipping cost for an order. It doesn't care whether there is one or dozens of implementations behind it. It just wants to get the shipping cost and go on with its job:

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);
}

This means however that the injected shipping strategy must now be something that can decide how to calculate the cost based on the Order.Method property. But there's a pattern for this called the Proxy pattern. Here's an example:

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);
        }
    }
}

This proxy internally acts a bit like a factory, but there are two important differences here:

  1. It does not define a different interface. This allows the consumer to only take a dependency on 1 concept: the IShippingStrategy.
  2. It does not create the strategies itself; they are still injected into it.

This proxy simply forwards the incoming call to an underlying strategy implementation that does the actual work.

There is a variety of ways to implement such proxy. For instance, you could still create the dependencies here manually -or you could forward the call to the container, who will create the dependencies for you. Also the way you inject the dependencies can differ based on what's best for your application.

And even though such proxy might internally work like a factory, the important thing is that there is no factory Abstraction here; that would only complicate the consumers.

Everything discussed above is discussed in more detail in the book Dependency Injection Principles, Practices, and Patterns, by Mark Seemann and myself. For instance, Composition Root is discussed in § 4.1, Constructor Injection in § 4.2, abuse of Abstract Factories in § 6.2, and Auto-Registration in chapter 12.

查看更多
成全新的幸福
6楼-- · 2020-02-26 08:26

So I did it like this. I would have preferred to have injected an IDictionary, but because of the limitation with injecting "IEnumerable" into the constructor (this limitation is Unity specific), I came up with a little workaround.

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;
    }
}

...

And calling code: (that would be in something like "Program.cs" for example)

            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);

Logging Output:

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

So each concrete has a static string providing its name in a strong-typed fashion. ("FriendlyName")

And then I have an instance string-get property which uses the exact same value to keep things in sync. ("FriendlyNameInstance")

By forcing the issue by using a property on the Interface (below partial code)

public interface IShipper
{
   string FriendlyNameInstance { get;}
}

I can use this to "find" my shipper out of the collection of shippers.

The internal method "FindIShipper" is the kinda-factory, but removes the need to have a separate IShipperFactory and ShipperFactory interface and class. Thus simplifying the overall setup. And still honors Constructor-Injection and Composition root.

If anyone knows how to use IDictionary<string, IShipper> (and inject via the constructor), please let me know.

But my solution works...with a little razzle dazzle.

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

My third-party-dll dependency list. (I'm using dotnet core, but dotnet framework with a semi new version of Unity should work too). (See PackageReference's below)

<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>
查看更多
登录 后发表回答