Je travaille sur un projet parallèle afin de mieux comprendre l'inversion du contrôle et l'injection de dépendance ainsi que différents modèles de conception.
Je me demande s’il existe les meilleures pratiques pour utiliser DI avec les modèles d’usine et de stratégie?
Mon défi survient lorsqu'une stratégie (construite à partir d'une usine) requiert différents paramètres pour chaque constructeur et chaque implémentation possibles. En conséquence, je me retrouve à déclarer toutes les interfaces possibles dans le point d'entrée de service et à les transmettre via l'application. Par conséquent, le point d'entrée doit être modifié pour les nouvelles et différentes implémentations de classe de stratégie.
J'ai rassemblé un exemple apparié à titre d'illustration ci-dessous. Ma pile pour ce projet est .NET 4.5/C # et Unity pour IoC/DI.
Dans cet exemple d'application, j'ai ajouté une classe de programme par défaut responsable de l'acceptation d'une commande fictive et, en fonction des propriétés de la commande et du fournisseur de transport sélectionné, calculait les frais de livraison. Il existe différents calculs pour UPS, DHL et Fedex, et chaque mise en œuvre peut ou non faire appel à des services supplémentaires (pour frapper une base de données, une 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; }
}
Programme ou service fictif pour calculer les frais de livraison
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();
}
}
}
Passons maintenant à l'interface et aux implémentations de la stratégie. UPS est un calcul facile, alors que DHL et Fedex peuvent nécessiter différents services (et différents paramètres de constructeur).
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);
}
}
Le problème avec ce qui précède est que chaque stratégie nécessite des services supplémentaires et différents pour exécuter la méthode «CalculateShippingCost». Ces interfaces/implémentations doivent-elles être enregistrées avec le point d’entrée (la classe Program) et transmises par les constructeurs?
Existe-t-il d'autres modèles qui conviendraient mieux pour réaliser le scénario ci-dessus? Peut-être quelque chose que Unity pourrait gérer spécifiquement ( https://msdn.Microsoft.com/en-us/library/dn178463(v=pandp.30).aspx )?
J'apprécie beaucoup toute aide ou un coup de pouce dans la bonne direction.
Merci, Andy
Il y a plusieurs façons de procéder, mais je préfère utiliser une liste des stratégies disponibles dans votre usine, puis les filtrer pour renvoyer la ou les stratégies qui vous intéressent.
En travaillant avec votre exemple, je modifierais IShippingStrategy
pour ajouter une nouvelle propriété:
public interface IShippingStrategy
{
int CalculateShippingCost(Order order);
string SupportedShippingMethod { get; }
}
Ensuite, je mettrais en place l'usine comme suit:
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;
}
}
La principale raison pour laquelle j'aime l'utiliser de cette façon est que je ne dois jamais revenir et modifier l'usine. Si jamais je dois mettre en œuvre une nouvelle stratégie, l'usine ne doit pas être changée. Si vous utilisez l'enregistrement automatique avec votre conteneur, vous n'avez même pas besoin d'enregistrer la nouvelle stratégie. Il s'agit simplement de vous permettre de passer plus de temps à écrire du nouveau code.
Lors de l’application de l’injection de dépendance, nous définissons toutes les dépendances de la classe en tant qu’arguments requis dans le constructeur. Cette pratique s'appelle Constructor Injection. Cela alourdit le fardeau de créer la dépendance de la classe à son consommateur. La même règle s’applique toutefois aux consommateurs de la classe. Ils doivent également définir leurs dépendances dans leur constructeur. Cela va jusqu'au bout de la pile d'appels, ce qui signifie que les "graphes d'objet" peuvent devenir assez profonds à certains endroits.
L'injection de dépendance entraîne la responsabilité de créer des classes jusqu'au point d'entrée de l'application; la Composition Racine . Cela signifie toutefois que le point d'entrée doit connaître toutes les dépendances. Si nous n'utilisons pas DI Container - une pratique appelée Pure DI - cela signifie qu'à ce stade, toutes les dépendances doivent être créées dans l'ancien code C #. Si nous utilisons un conteneur DI, nous devons toujours informer le conteneur DI de toutes les dépendances.
Cependant, nous pouvons parfois utiliser une technique appelée batch ou Auto-Registration, dans laquelle le conteneur DI utilise la réflexion sur nos projets et enregistre les types à l'aide de Convention over Configuration . Cela nous évite d'avoir à enregistrer tous les types un par un et nous empêche souvent de modifier la racine Composition - chaque fois qu'une nouvelle classe est ajoutée au système.
Ces interfaces/implémentations doivent-elles être enregistrées avec le point d’entrée (la classe Program) et transmises par les constructeurs?
Absolument.
En conséquence, je me retrouve à déclarer toutes les interfaces possibles dans le point d'entrée de service et à les transmettre via l'application. Par conséquent, le point d'entrée doit être modifié pour les nouvelles et différentes implémentations de classe de stratégie.
Le point d'entrée de l'application est la partie la plus volatile du système. C'est toujours le cas, même sans DI. Mais avec DI, nous rendons le reste du système beaucoup moins volatile. Là encore, nous pouvons réduire le nombre de modifications de code que nous devons effectuer au point d’entrée en appliquant Auto-Registration.
Je me demande s’il existe des pratiques optimales d’utilisation de DI avec les modèles d’usine et de stratégie.
Je dirais que la meilleure pratique concernant les usines est d’en avoir le moins possible, comme expliqué dans cet article . En fait, votre interface d'usine est redondante et ne complique que les consommateurs qui en ont besoin (comme expliqué dans l'article). Votre application peut facilement s'en passer et vous pouvez à la place injecter une IShippingStrategy
directement, car c'est la seule chose qui intéresse le consommateur: obtenir le coût d'expédition d'une commande. Peu importe qu'il y ait une ou des dizaines d'implémentations derrière. Il veut juste obtenir les frais de transport et poursuivre son travail:
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);
}
Cela signifie toutefois que la stratégie d’expédition injectée doit désormais permettre de déterminer le calcul du coût en fonction de la propriété Order.Method
. Mais il existe un modèle pour cela appelé modèle de proxy. Voici un exemple:
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);
}
}
}
Ce proxy agit en interne un peu comme une usine, mais il y a deux différences importantes ici:
IShippingStrategy
.Ce proxy transfère simplement l'appel entrant à une implémentation de stratégie sous-jacente qui effectue le travail réel.
Il existe une variété de moyens pour implémenter un tel proxy. Par exemple, vous pouvez toujours créer les dépendances ici manuellement, ou vous pouvez transférer l'appel au conteneur, qui créera les dépendances pour vous. De plus, la façon dont vous injectez les dépendances peut varier en fonction des solutions les mieux adaptées à votre application.
Et même si un tel proxy fonctionne en interne comme une usine, l’important est qu’il n’existe aucune usine Abstraction ici; cela ne ferait que compliquer les consommateurs.
Tout ce qui a été mentionné ci-dessus est traité plus en détail dans le livre Principes, pratiques et modèles d’injection de dépendance , par Mark Seemann et moi-même. Par exemple, Composition Root est traité dans le § 4.1, Le constructeur injecté dans le § 4.2, l'utilisation abusive des Fabriques Abstraites au § 6.2 et l'Auto-enregistrement au chapitre 12.
Alors je l'ai fait comme ça. J'aurais préféré injecter un IDictionary, mais en raison de la limitation avec l'injection de "IEnumerable" dans le constructeur (cette limitation est spécifique à Unity), j'ai proposé une solution de contournement.
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;
}
}
...
Et code appelant: (ce serait dans quelque chose comme "Program.cs" par exemple)
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);
Enregistrement de la sortie:
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
Ainsi, chaque béton a une chaîne statique qui donne son nom de manière très typée. ("Nom familier")
Et puis j'ai une propriété d'instance string-get qui utilise exactement la même valeur pour garder les choses synchronisées. ("FriendlyNameInstance")
En forçant le problème en utilisant une propriété sur l'interface (sous le code partiel)
public interface IShipper
{
string FriendlyNameInstance { get;}
}
Je peux l'utiliser pour "trouver" mon expéditeur dans la collection d'expéditeurs.
La méthode interne "FindIShipper" est la kinda-factory, mais évite d'avoir à utiliser une interface et une classe IShipperFactory et ShipperFactory distinctes. Simplifiant ainsi la configuration générale. Et honore encore Constructor-Injection et Composition root .
Si quelqu'un sait comment utiliser IDictionary<string, IShipper>
(et s'injecter via le constructeur), merci de me le faire savoir.
Mais ma solution fonctionne ... avec un petit éclat.
...........................
Ma liste de dépendance tierce-dll. (J'utilise dotnet core, mais le framework dotnet avec une version semi-nouvelle de Unity devrait également fonctionner). (Voir PackageReference's ci-dessous)
<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>
Enregistrez-les et résolvez-les à l'aide de vos chaînes de type de stratégie.
Comme ça:
// 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");
Veuillez voir les réponses de John H et Silas Reinagel. Ils sont tous les deux très utiles.
J'ai fini par faire une combinaison des deux réponses.
J'ai mis à jour l'usine et l'interface comme le mentionne John H.
Et puis dans le conteneur Unity, j'ai ajouté les implémentations avec les nouveaux paramètres nommés, comme le montre Silas Reinagel.
J'ai ensuite suivi la réponse ici pour utiliser Unity pour enregistrer la collection pour l'injection dans l'usine de stratégies . Manière de remplir la collection avec Unity
Désormais, chaque stratégie peut être mise en œuvre séparément sans avoir à modifier en amont.
Merci à tous.