我正在开展一个侧面项目,以更好地理解控制和依赖注入的反转以及不同的设计模式。
我想知道在工厂和战略模式中使用DI是否有最佳做法?
我的挑战来自于一个策略(从工厂构建)需要为每个可能的构造函数和实现提供不同的参数。结果,我发现自己在服务入口点声明了所有可能的接口,并将它们传递给应用程序。因此,必须针对新的和各种策略类实现更改入口点。
我已经将一个配对的示例放在一起,以用于下面的插图目的。我的这个项目的堆栈是.NET 4.5 / C#和Unity for IoC / DI。
在这个示例应用程序中,我添加了一个默认的Program类,负责接受虚构订单,并根据订单属性和选择的送货提供商计算运费。 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'方法。这些接口/实现是否需要在入口点(Program类)中注册并通过构造函数向下传递?
是否有其他模式更适合完成上述方案?也许Unity可以专门处理的事情(https://msdn.microsoft.com/en-us/library/dn178463(v=pandp.30).aspx)?
我非常感谢任何帮助或推动正确的方向。
谢谢, 安迪
答案 0 :(得分:15)
在应用依赖注入时,我们将所有类的依赖项定义为构造函数中的必需参数。这种做法称为构造函数注入。这推动了从类到其使用者创建依赖关系的负担。然而,同样的规则也适用于班级的消费者。他们还需要在构造函数中定义它们的依赖项。这在调用堆栈中一直向上,这意味着所谓的“对象图”在某些点上会变得非常深。
依赖注入导致创建类的责任一直到应用程序的入口点; 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 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);
}
}
}
此代理在内部有点像工厂,但这里有两个重要的区别:
IShippingStrategy
。此代理只是将传入呼叫转发给执行实际工作的基础策略实施。
有多种方法可以实现此类代理。例如,您仍然可以手动创建依赖项 - 或者您可以将调用转发给容器,容器将为您创建依赖项。注入依赖项的方式也可能因应用程序的最佳位置而异。
即使这样的代理可能在内部像工厂一样工作,但重要的是这里没有工厂 Abstraction ;这只会使消费者感到复杂。
Mark Seemann和我自己在Dependency Injection Principles, Practices, and Patterns一书中更详细地讨论了上面讨论的所有内容。例如,组合根在§4.2中讨论,第4.2节中的构造函数注入,第6.2节中滥用抽象工厂,以及自动注册在第12章。
答案 1 :(得分:9)
有几种方法可以做到这一点,但我更喜欢的方法是将可用策略列表注入您的工厂,然后过滤它们以返回您感兴趣的那些。
使用您的示例,我修改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;
}
}
我喜欢这样使用它的主要原因是我永远不必回来修改工厂。如果我必须实施新战略,工厂不必改变。如果您使用自动注册容器,您甚至不必注册新策略,因此只需要花费更多时间编写新代码即可。< / p>
答案 2 :(得分:2)
所以我这样做了。我本来希望注入IDictionary,但是由于向构造函数中注入“ IEnumerable”的局限性(此局限性是特定于Unity的),我想出了一些解决方法。
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;
}
}
...
并调用代码:(例如,在“ 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);
记录输出:
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
因此,每个混凝土都有一个静态字符串,以一种强类型的方式提供其名称。 (“ FriendlyName”)
然后我有一个实例string-get属性,该属性使用完全相同的值来使事物保持同步。 (“ FriendlyNameInstance”)
通过在接口上使用属性(在部分代码下方)来强制问题
public interface IShipper
{
string FriendlyNameInstance { get;}
}
我可以用它从托运人集合中“找到”我的托运人。
内部方法“ FindIShipper”有点像工厂,但是不需要单独的IShipperFactory和ShipperFactory接口和类。从而简化了整体设置。并且仍然尊重构造函数注入和Composition root。
如果有人知道如何使用IDictionary<string, IShipper>
(并通过构造函数注入),请告诉我。
但是我的解决方案可以...有点令人眼花乱。
...........................
我的第三方dll依赖项列表。 (我使用的是dotnet核心,但是带有半新版本的Unity的dotnet框架也应该可以使用)。 (请参阅下面的PackageReference)
<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>
答案 3 :(得分:1)
请参阅John H和Silas Reinagel的答案。他们都很有帮助。
我最终做了两个答案的组合。
我更新了工厂和界面,正如John H所提到的那样。
然后在Unity容器中,我添加了具有新命名参数的实现,如Silas Reinagel show。
然后我按照这里的答案使用Unity注册集合以注入策略工厂。 Way to fill collection with Unity
现在,每个策略都可以单独实施,而无需修改上游。
谢谢大家。