Using a Strategy and Factory Pattern with Dependency Injection

29,159

Solution 1

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.

Solution 2

When applying Dependency Injection, you 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 you use DI without 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 you use a DI Container you still have to tell the DI container about all dependencies.

Sometimes however you can make use of a technique called batch or Auto-Registration, where the DI Container will use reflection over your projects and register types using Convention over Configuration. This saves you the burden of registering all types one by one and often prevents you 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 (which is implied by the Stable-Dependencies Principle). It always is, even without DI. But with DI you make the rest of the system much less volatile. Again, you can reduce the amount of code changes you 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.

Solution 3

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>

APPEND:

Here is the autofac version:

(using all the same interfaces and concretes above )

Program.cs

namespace MyCompany.ProofOfConcepts.AutofacStrategyPatternExample.DemoCommandLineInterfaceOne
{
    using System;
    using System.Text;
    using Autofac;
    using Autofac.Extensions.DependencyInjection;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Logging;
    /* need usings for all the object above */
    using MyCompany.ProofOfConcepts.AutofacStrategyPatternExample.Domain;
    using NLog;
    using NLog.Extensions.Logging;

    public class Program
    {
        private static Logger programStaticLoggerThatNeedsToBeInitiatedInMainMethod = null;

        public static int Main(string[] args)
        {
            Logger loggerFromNLogLogManagerGetCurrentClassLogger = NLog.LogManager.GetCurrentClassLogger(); /* if this is made a private-static, it does not log the entries */
            programStaticLoggerThatNeedsToBeInitiatedInMainMethod = loggerFromNLogLogManagerGetCurrentClassLogger;

            programStaticLoggerThatNeedsToBeInitiatedInMainMethod.Info("programStaticLoggerThatNeedsToBeInitiatedInMainMethod: Main.Start");
            try
            {
                bool useCodeButNotAutofacJson = true; /* true will "code up" the DI in c# code, false will kick in the autofac.json */

                string autoFacFileName = useCodeButNotAutofacJson ? "autofac.Empty.json" : "autofac.json"; /* use "empty" to prove the DI is not coming from non-empty json */

                programStaticLoggerThatNeedsToBeInitiatedInMainMethod.Info(string.Format("programStaticLoggerThatNeedsToBeInitiatedInMainMethod: autoFacFileName={0}", autoFacFileName));

                IConfiguration config = new ConfigurationBuilder()
                    .SetBasePath(System.IO.Directory.GetCurrentDirectory())
                    .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                    .AddJsonFile(autoFacFileName)
                    .Build();

                IServiceProvider servicesProvider = BuildDi(config, useCodeButNotAutofacJson);
                using (servicesProvider as IDisposable)
                {
                    IOrderProcessor processor = servicesProvider.GetRequiredService<IOrderProcessor>();
                    processor.ProcessOrder(FedExShipper.FriendlyName, new Order());

                    Microsoft.Extensions.Logging.ILogger loggerFromIoc = servicesProvider.GetService<ILoggerFactory>()
                    .CreateLogger<Program>();
                    loggerFromIoc.LogInformation("loggerFromIoc:Starting application");

                    loggerFromIoc.LogInformation("loggerFromIoc:All done!");

                    Console.WriteLine("Press ANY key to exit");
                    Console.ReadLine();
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(GenerateFullFlatMessage(ex));
                //// NLog: catch any exception and log it.
                programStaticLoggerThatNeedsToBeInitiatedInMainMethod.Error(ex, "programStaticLoggerThatNeedsToBeInitiatedInMainMethod : Stopped program because of exception");
                throw;
            }
            finally
            {
                // Ensure to flush and stop internal timers/threads before application-exit (Avoid segmentation fault on Linux)
                LogManager.Shutdown();
            }

            Console.WriteLine("Returning 0 and exiting.");

            return 0;
        }

        private static IServiceProvider BuildDi(IConfiguration config, bool useCodeButNotAutofacJson)
        {
            NLog.Extensions.Logging.NLogProviderOptions nlpopts = new NLog.Extensions.Logging.NLogProviderOptions
            {
                IgnoreEmptyEventId = true,
                CaptureMessageTemplates = true,
                CaptureMessageProperties = true,
                ParseMessageTemplates = true,
                IncludeScopes = true,
                ShutdownOnDispose = true
            };

            IServiceCollection sc = new ServiceCollection()

            ////.AddLogging(loggingBuilder =>
            ////{
            ////    // configure Logging with NLog
            ////    loggingBuilder.ClearProviders();
            ////    loggingBuilder.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace);
            ////    loggingBuilder.AddNLog(config);
            ////})

            .AddLogging(loggingBuilder =>
            {
                ////use nlog
                loggingBuilder.AddNLog(nlpopts);
                NLog.LogManager.LoadConfiguration("nlog.config");
            })

            .AddSingleton<IConfiguration>(config);

            //// // /* before autofac */   return sc.BuildServiceProvider();

            //// Create a container-builder and register dependencies
            Autofac.ContainerBuilder builder = new Autofac.ContainerBuilder();

            // Populate the service-descriptors added to `IServiceCollection`
            // BEFORE you add things to Autofac so that the Autofac
            // registrations can override stuff in the `IServiceCollection`
            // as needed
            builder.Populate(sc);

            if (useCodeButNotAutofacJson)
            {
                programStaticLoggerThatNeedsToBeInitiatedInMainMethod.Info("Coding up Autofac DI");

                /* "Keyed" is not working, do not use below */
                ////builder.RegisterType<FedExShipper>().Keyed<IShipper>(FedExShipper.FriendlyName);
                ////builder.RegisterType<UpsShipper>().Keyed<IShipper>(UpsShipper.FriendlyName);
                ////builder.RegisterType<UspsShipper>().Keyed<IShipper>(UspsShipper.FriendlyName);

                builder.RegisterType<FedExShipper>().As<IShipper>();
                builder.RegisterType<UpsShipper>().As<IShipper>();
                builder.RegisterType<UspsShipper>().As<IShipper>();
                builder.RegisterType<OrderProcessor>().As<IOrderProcessor>();
            }
            else
            {
                programStaticLoggerThatNeedsToBeInitiatedInMainMethod.Info("Using .json file to define Autofac DI");

                // Register the ConfigurationModule with Autofac.
                var module = new Autofac.Configuration.ConfigurationModule(config);
                builder.RegisterModule(module);
            }

            Autofac.IContainer autofacContainer = builder.Build();

            // this will be used as the service-provider for the application!
            return new AutofacServiceProvider(autofacContainer);
        }

        private static string GenerateFullFlatMessage(Exception ex)
        {
            return GenerateFullFlatMessage(ex, false);
        }

        private static string GenerateFullFlatMessage(Exception ex, bool showStackTrace)
        {
            string returnValue;

            StringBuilder sb = new StringBuilder();
            Exception nestedEx = ex;

            while (nestedEx != null)
            {
                if (!string.IsNullOrEmpty(nestedEx.Message))
                {
                    sb.Append(nestedEx.Message + System.Environment.NewLine);
                }

                if (showStackTrace && !string.IsNullOrEmpty(nestedEx.StackTrace))
                {
                    sb.Append(nestedEx.StackTrace + System.Environment.NewLine);
                }

                if (ex is AggregateException)
                {
                    AggregateException ae = ex as AggregateException;

                    foreach (Exception flatEx in ae.Flatten().InnerExceptions)
                    {
                        if (!string.IsNullOrEmpty(flatEx.Message))
                        {
                            sb.Append(flatEx.Message + System.Environment.NewLine);
                        }

                        if (showStackTrace && !string.IsNullOrEmpty(flatEx.StackTrace))
                        {
                            sb.Append(flatEx.StackTrace + System.Environment.NewLine);
                        }
                    }
                }

                nestedEx = nestedEx.InnerException;
            }

            returnValue = sb.ToString();

            return returnValue;
        }
    }
}

........

autofac.Empty.json (set to copy always)

{}

.......

autofac.json (set to copy always)

{
  "defaultAssembly": "MyCompany.MyProject",
  "components": [
    {
      "type": "MyCompany.MyProject.Shippers.FedExShipper",
      "services": [
        {
          "type": "MyCompany.MyProject.Shippers.Interfaces.IShipper"
        }
      ]
    },
    {
      "type": "MyCompany.MyProject.Shippers.UpsShipper",
      "services": [
        {
          "type": "MyCompany.MyProject.Shippers.Interfaces.IShipper"
        }
      ]
    },
    {
      "type": "MyCompany.MyProject.Shippers.UspsShipper",
      "services": [
        {
          "type": "MyCompany.MyProject.Shippers.Interfaces.IShipper"
        }
      ]
    },
    {
      "type": "MyCompany.MyProject.Processors.OrderProcessor",
      "services": [
        {
          "type": "MyCompany.MyProject.Processors.Interfaces.IOrderProcessor"
        }
      ]
    }
  ]
}

and csproj

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Autofac" Version="5.1.2" />
    <PackageReference Include="Autofac.Configuration" Version="5.1.0" />
    <PackageReference Include="Autofac.Extensions.DependencyInjection" Version="6.0.0" />
    <PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="3.1.2" />
    <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.2" />
    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.2" />
    <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.2" />
    <PackageReference Include="Microsoft.Extensions.Logging" Version="3.1.2" />
    <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="3.1.2" />
    <PackageReference Include="NLog.Extensions.Logging" Version="1.6.1" />
  </ItemGroup>

From

https://autofaccn.readthedocs.io/en/latest/integration/netcore.html

PS

In the autofac version I had to change the Logger being injected to be a LoggerFactory.

Here is the OrderProcessor alternate version. You'll have do the same "Microsoft.Extensions.Logging.ILoggerFactory loggerFactory" alternative injection for all 3 concrete "Shipper"'s as well.

namespace MyCompany.MyProject.Processors
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using Microsoft.Extensions.Logging;
    public class OrderProcessor : IOrderProcessor
    {
        ////private readonly IDictionary<string, IShipper> shippers; /*   :(    I couldn't get IDictionary<string, IShipper>  to work */
        private readonly IEnumerable<IShipper> shippers;
        private Microsoft.Extensions.Logging.ILogger<OrderProcessor> logger;

        public OrderProcessor(Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, IEnumerable<IShipper> shprs)
        {
            if (null == loggerFactory)
            {
                throw new ArgumentOutOfRangeException("loggerFactory is null");
            }

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

            this.logger = loggerFactory.CreateLogger<OrderProcessor>();
            this.shippers = shprs;
        }

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

            /* below foreach is not needed, just "proves" everything was injected */
            int counter = 0;
            foreach (IShipper sh in this.shippers)
            {
                this.logger.LogInformation(string.Format("IEnumerable:ShipperInterface. ({0} of {1}) -> ({2})", ++counter, this.shippers.Count(), 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;
        }
    }
}

not related to autofac

nlog.config (set to copy always)

<?xml version="1.0" encoding="utf-8" ?>
<!-- XSD manual extracted from package NLog.Schema: https://www.nuget.org/packages/NLog.Schema-->
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xsi:schemaLocation="NLog NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      autoReload="true"
      internalLogFile="MyCompany.MyProject.Nlog.internalLogFile.log"
      internalLogLevel="Info" >

  <!-- the targets to write to -->
  <targets>
    <!-- write logs to file -->
    <target xsi:type="File" name="target1" fileName="MyCompany.MyProject.Nlog.MyConsoleAppProgram.log"
            layout="${date}|${level:uppercase=true}|${message} ${exception}|${logger}|${all-event-properties}" />
    <target xsi:type="Console" name="target2"
            layout="${date}|${level:uppercase=true}|${message} ${exception}|${logger}|${all-event-properties}" />
  </targets>

  <!-- rules to map from logger name to target -->
  <rules>
    <logger name="*" minlevel="Trace" writeTo="target1,target2" />
  </rules>
</nlog>

Solution 4

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.

Solution 5

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");
Share:
29,159

Related videos on Youtube

apleroy
Author by

apleroy

http://www.linkedin.com/in/andy-leroy-1b2a7323 https://github.com/apleroy https://www.exponentiallayers.com

Updated on January 31, 2022

Comments

  • apleroy
    apleroy over 2 years

    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

  • apleroy
    apleroy about 7 years
    Thanks for your help @John H. I added an additional answer to this question and combined your answer with that of Silas Reinagel.
  • apleroy
    apleroy about 7 years
    Thanks for your help @Silas Reinagel. I added in some details from John H's post with your answer. I appreciate it.
  • granadaCoder
    granadaCoder over 5 years
    This is still "Service Locator" and not "Ioc/DI". It works...but consider it is not pure DI.
  • Silas Reinagel
    Silas Reinagel over 5 years
    @granadaCoder Totally agree. In fact anything involving resolution tooling has nothing itself to do with DI/IoC. DI/IoC are architectural approaches, not tools. Whether or not Service Locator pattern is being used is a question of whether objects are requested specific instances themselves, or whether the composition root is selection which dependency to inject into an object.
  • granadaCoder
    granadaCoder over 5 years
    So I have moved away from a dedicated ISomethingFactory, and more towards injecting everything via the Constructor. (Composite Root makes alot of sense). What I am doing now is injecting all "shippers" into the class that needs them. public MyClass(IDictionary<string, IShipper> availableShippers), and having a mini private method to "find" the one I need based on string-key. I actually have this same pseudo example in java at (see next comment)
  • granadaCoder
    granadaCoder over 5 years
  • granadaCoder
    granadaCoder over 5 years
    I need to do a write up on a Unity version of my IShipper pseudo example. Unity is still a great IoC/DI framework, especially after dealing with spring di for a bit.
  • granadaCoder
    granadaCoder over 5 years
  • Groo
    Groo over 5 years
    One remark though, string SupportedShippingMethod seems like an unnecessarily specific implementation detail IMHO, that doesn't need to be imposed on strategies. Perhaps a strategy could support multiple shipping methods, or only works for products of certain dimensions? It would be more general to either have each strategy implement bool SupportsOrder(Order) in any way it wants, or to pass such delegate along with each strategy separately, while constructing the factory.
  • John H
    John H over 5 years
    @Groo That's a pretty fair comment. I'd agree with that.
  • IngoB
    IngoB over 5 years
    I like this approach but doesn't I lead to a lot of never used instances?
  • Creepin
    Creepin over 5 years
    @IngoB You can use Lazy<> to defer the creation of instances, if they should ever be needed. Lazy<> is directly supported by some IoC containers. However, similar to an abstract factory, getting Lazy<> injected into a consumer is kind of a leaky abstraction. I would say the proxy mentioned here belongs to the composition root, so using Lazy<> in this proxy is OK. More details: blog.ploeh.dk/2010/01/20/EnablingDIforLazyComponents and blog.ploeh.dk/2014/08/24/decoraptor
  • Ronnie
    Ronnie about 5 years
    You say best practice is to use factories as little as possible and link to an article, however in the article it says that "factories are fine" and it is abstract factories that should generally be avoided in LOB applications.
  • void.pointer
    void.pointer almost 4 years
    I personally use UnityContainer, and I find abstract factories unavoidable when I'm injecting concrete types that require construction parameters for types that can't be registered, like string. For example if I have an IHttpClient interface implemented by class HttpClient, which requires a string hostname as a construction parameter. UnityContainer can't facilitate this; so I have a factory IHttpClientFactory.Create(string hostname), while the factory implementation stores other DI arguments and passes them to HttpClient along with the string when it's constructed.