4

我正在开发一个侧面项目,以便更好地理解控制和依赖注入的反转以及不同的设计模式。在依赖注入中使用策略和工厂模式

我想知道是否有使用DI的工厂和战略模式的最佳实践

我的挑战来自何时一个策略(由工厂构建)需要不同的参数为每个可能的构造函数和实现。因此,我发现自己在服务入口点声明所有可能的接口,并通过应用程序传递它们。因此,必须为新的和各种策略类实现更改入口点。

为了下面的说明,我已经组合了一个配对的例子。我的这个项目的堆栈是.NET 4.5/C#和Unity for IoC/DI。

在本示例应用程序中,我添加了一个默认的程序类,负责接受虚构的订单,并根据订单属性和所选的运输供应商计算运输成本。对于UPS,DHL和Fedex有不同的计算方法,并且每个实现可能会或可能不会依赖额外的服务(打击数据库,api等)。

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

虚拟程序或服务来计算运费成本

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

现在的策略接口和实现。 UPS是一个简单的计算,而DHL和Fedex可能需要不同的服务(和不同的构造参数)。

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

上述问题是每个策略都需要额外的不同服务来执行'CalculateShippingCost'方法。这些接口/实现是否需要在入口点(程序类)注册并通过构造函数传递?

是否有其他模式更适合完成上述场景?也许是Unity可以专门处理的事情(https://msdn.microsoft.com/en-us/library/dn178463(v=pandp.30).aspx)?

我非常感谢任何帮助或在正确的方向微调。

感谢, 安迪

+1

请参见[使用DI和Ioc的工厂方法](http://stackoverflow.com/a/31971691/181087)。 – NightOwl888

回答

5

有这样做的几种方法,但我喜欢的方式是注入可用策略的列表到您的工厂,然后过滤他们返回的一个(S )你感兴趣。

你的榜样工作,我会修改IShippingStrategy添加一个新的属性:

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

然后,我对工厂像这样:

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

主要的原因,我喜欢用这样我就不必回来修改工厂了。如果我不得不实施新的战略,工厂不必改变。如果你在你的容器中使用自动注册,你甚至不需要注册新的策略,所以这只是一个让你花更多时间来编写新代码的例子。

+0

感谢您的帮助@John H.我为此问题添加了一个附加答案,并将您的答案与Silas Reinagel的答案结合在一起。 – apleroy

1

注册和使用策略类型的字符串解决这些问题。

像这样:

// 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"); 
+0

感谢您的帮助@Silas Reinagel。我在John H的帖子的一些细节中加入了你的答案。我很感激。 – apleroy

1

请参阅John H和Silas Reinagel的答案。他们都非常有帮助。

我最终做了两个答案的组合。

我更新了John H所提到的工厂和界面。

然后在Unity容器中,我添加了像Silas Reinagel所示的新命名参数的实现。

然后,我在这里回答了使用Unity将注射集注册到战略工厂的答案。 Way to fill collection with Unity

现在每个策略都可以单独实施而不需要修改上游。

谢谢大家。

4

应用依赖注入时,我们将所有类的依赖性定义为构造函数中的必需参数。这将创建从类到客户的依赖关系的负担。同样的规则也适用于班级的消费者。他们也需要在构造函数中定义它们的依赖关系。这一直沿着调用堆栈,这意味着所谓的“对象图”在某些点上可能变得相当深刻。

依赖注入导致创建类直到应用程序入口点的责任; Composition Root。不过这意味着入口点需要知道所有的依赖关系。如果我们不使用DI容器 - 一种练习叫做Pure DI - 这意味着此时所有依赖关系必须在普通的旧C#代码中创建。如果我们使用DI容器,我们仍然必须告诉DI容器所有的依赖关系。

但是有时候我们可以利用的技术,称为批处理或自动注册,其中DI容器将使用反射在我们的项目和使用Convention over Configuration注册类型。这为我们节省了逐个注册所有类型的负担,并且经常阻止我们在每次将新类添加到系统时对Composition Root进行更改。

这些接口/实现是否需要注册入口点(Program类)并通过构造函数传递?

绝对。

因此,我发现自己在服务入口点声明所有可能的接口,并通过应用程序传递它们。因此,必须为新的和各种策略类实现更改入口点。

应用程序的入口点是系统中最易变的部分,即使没有DI也是如此。但是在DI中,我们让系统的其他部分变得更加不稳定。再次,我们可以通过应用自动注册来减少我们在入口点处所需的代码更改量。

我想知道是否有与工厂和战略模式使用DI的最佳做法?

我想说的是关于工厂的最佳做法是尽可能少地使用它们,正如this article中所解释的那样。事实上,你的工厂界面是多余的,只会使需要它的用户复杂化(如文章中所解释的)。您的应用程序很容易就没有,您可以直接注入IShippingStrategy,因为这是消费者感兴趣的唯一事情:获得订单的运输成本。它并不关心它背后是否有一个或几十个实现。它只是想要得到的运输成本,并与它的工作继续下去:

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

这意味着然而,该注射航运战略,现在必须的东西,可以决定如何计算基础上,Order.Method财产的成本。但是有一种称为代理模式的模式。这里有一个例子:

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

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

该代理行为的内部有点像一个工厂,但这里有两个重要的区别:

  1. 没有定义不同的接口。这允许消费者仅依赖于1概念:IShippingStrategy
  2. 它不会创建策略本身;他们仍然被注入到它。

该代理只是简单地将传入呼叫转发到执行实际工作的基础策略实施。

有多种方法可以实现这样的代理。例如,您仍然可以手动在此处创建依赖关系 - 或者您可以将调用转发给容器,容器将为您创建依赖项。此外,注入依赖关系的方式可能因您的应用程序的最佳选择而有所不同。

即使这样的代理可能像工厂一样在内部工作,但重要的是这里没有工厂抽象;这只会使消费者复杂化。