asp.net-mvc
Injection de dépendance
Recherche…
Remarques
Le point entier de l'injection de dépendance (DI) est de réduire le couplage de code. Imaginez tout type d'interaction qui implique de changer quelque chose comme dans "l'exemple de la dépendance codée en dur".
Une grande partie de l'écriture du code est la possibilité de le tester. Chaque fois que nous découvrons une nouvelle dépendance, nous rendons notre code difficile à tester car nous n'avons aucun contrôle sur cette dépendance.
Comment testeriez-vous le code qui dépend de DataTime.Now par exemple? Cela change toujours, donc vous n'avez aucune référence. C'est à ce moment que vous injectez un paramètre stable comme point de départ. Vous pouvez le contrôler, vous pouvez écrire des tests en fonction de différentes valeurs et vous assurer que vous obtenez toujours le bon résultat.
Une bonne option est donc de passer une interface ou une classe abstraite en tant que paramètre dans le constructeur DI.
Une interface représente un contrat bien défini, vous pouvez toujours compter sur les méthodes pour y être et vous pouvez toujours compter sur les signatures de méthode.
Une fois que vous commencez à utiliser DI, d'autres aspects vont s'ouvrir. Par exemple, même si vous passez une interface à un moment donné, vous aurez besoin d’une véritable implémentation pour faire tout travail. C'est là que d'autres concepts apparaissent. Nous pouvons utiliser IOC (Inversion of Control) pour résoudre nos dépendances. Cela signifie que nous demandons à notre code de toujours utiliser une implémentation spécifique pour tout contrat. Bien sûr, il existe d’autres moyens de le faire. Nous pourrions toujours instancier chaque contrat avec une implémentation spécifique et à partir de ce moment, notre code peut utiliser cette partie:
public ILogging Logging { get; set }
à un moment donné, nous l'initialisons.
Logging = new FileLogging();
cela fonctionnera toujours tant que notre classe remplit le contrat prévu:
public class FileLogging : ILogging
à partir du moment d'initialisation, nous utilisons toujours l'objet Logging. Cela facilite la vie car si nous décidons de changer et d'utiliser un objet DatabaseLogging par exemple, il suffit de changer le code à un seul endroit et c'est exactement là que nous initialisons la classe Logging.
Est-ce que le DI n'est bon que pour les tests? Non, DI est également important lors de la rédaction du code maintenable. Cela permet de séparer les préoccupations.
Lorsque vous écrivez un code, pensez-vous que ... est-il testable? Puis-je écrire un test, c'est-à-dire injecter une valeur DateTime au lieu d'utiliser DateTime.
Configurations Ninject
Après l'installation d'un conteneur IoC (Inversion of Control), des ajustements sont nécessaires pour que cela fonctionne. Dans ce cas, je vais utiliser Ninject. Dans le fichier NinjectWebCommon, situé dans le dossier App_Start, remplacez la méthode CreateKernel par:
private static IKernel CreateKernel()
{
// Create the kernel with the interface to concrete bindings
var kernel = RegisterServices();
try
{
kernel.Bind<Func<IKernel>>().ToMethod(ctx => () => new Bootstrapper().Kernel);
kernel.Bind<IHttpModule>().To<HttpApplicationInitializationHttpModule>();
return kernel;
}
catch
{
kernel.Dispose();
throw;
}
}
Et la méthode RegisterServices avec:
private static StandardKernel RegisterServices()
{
Container container = new Container();
// encapsulate the interface to concrete bindings in another class or even in another layer
StandardKernel kernel = container.GetServices();
return kernel;
}
Créez une nouvelle classe pour la liaison appelée ici Container:
public class Container
{
public StandardKernel GetServices()
{
// It is good practice to create a derived class of NinjectModule to organize the binding by concerns. In this case one for the repository, one for service and one for app service bindings
return new StandardKernel(new NinjectRepositoryModule(),
new NinjectServiceModule(),
new NinjectAppServiceModule());
}
}
Enfin, dans chaque classe NinjectModule dérivée, modifiez les liaisons surchargeant la méthode Load comme:
public class NinjectRepositoryModule: NinjectModule
{
public override void Load()
{
// When we need a generic IRepositoryBase<> to bind to a generic RepositoryBase<>
// The typeof keyword is used because the target method is generic
Bind(typeof (IRepositoryBase<>)).To(typeof (RepositoryBase<>));
// When we need a IUnitOfWorkbind to UnitOfWork concrete class that is a singleton
Bind<IUnitOfWork>().To<UnitOfWork>().InSingletonScope();
}
}
Un autre exemple de NinjectModule dérivé:
public class NinjectServiceModule :NinjectModule
{
public override void Load()
{
// When we need a IBenefitService to BenefitService concrete class
Bind<IBenefitService>().To<BenefitService>();
// When we need a ICategoryService to CategoryService concrete class
Bind<ICategoryService>().To<CategoryService>();
// When we need a IConditionService to ConditionService concrete class
Bind<IConditionService>().To<ConditionService>();
}
}
Utilisation des interfaces
Dans la classe concrète qui a besoin du service, utilisez l'interface pour accéder au service au lieu de son implémentation comme:
public class BenefitAppService
{
private readonly IBenefitService _service;
public BenefitAppService(IBenefitService service)
{
_service = service;
}
public void Update(Benefit benefit)
{
if (benefit == null) return
_service.Update(benefit);
_service.Complete();
}
}
Maintenant, si vous avez besoin de quelque chose dans la classe concrète, n'interférez pas dans le code ci-dessus. Vous pouvez changer l'implémentation du service pour une autre différence, et tant que cela satisfait l'interface, vous êtes prêt à partir. En outre, il est très facile de le tester.
Injection de dépendance du constructeur
L'injection de dépendance de constructeur nécessite des paramètres dans le constructeur pour injecter des dépendances. Vous devez donc passer les valeurs lorsque vous créez un nouvel objet.
public class Example
{
private readonly ILogging _logging;
public Example(ILogging logging)
{
this._logging = logging;
}
}
Dépendance codée en dur
public class Example
{
private FileLogging _logging;
public Example()
{
this._logging = new FileLogging();
}
}
paramètre DI
public DateTime SomeCalculation()
{
return DateTime.Now.AddDays(3);
}
contre
public DateTime SomeCalculation(DateTime inputDate)
{
return inputDate.AddDays(3);
}
Injection de dépendances à injecter
Le résolveur de dépendance est utilisé pour éviter les classes étroitement couplées, améliorer la flexibilité et faciliter les tests. Vous pouvez créer votre propre injecteur de dépendance (non recommandé) ou utiliser un injecteur de dépendance bien écrit et testé. Dans cet exemple, je vais utiliser Ninject .
Première étape: créez un résolveur de dépendance.
Tout d'abord, téléchargez Ninject depuis NuGet. Créez le dossier nommé Infrastructure et ajoutez la classe nommée NinjectDependencyResolver :
using Ninject;
using System;
using System.Collections.Generic;
using System.Web.Mvc;
public class NinjectDependencyResolver
: IDependencyResolver
{
private IKernel kernel;
public NinjectDependencyResolver()
{
// Initialize kernel and add bindings
kernel = new StandardKernel();
AddBindings();
}
public object GetService(Type serviceType)
{
return kernel.TryGet(serviceType);
}
public IEnumerable<object> GetServices(Type serviceType)
{
return kernel.GetAll(serviceType);
}
private void AddBindings()
{
// Bindings added here
}
}
MVC Framework appellera les méthodes GetService et GetServices lorsqu'elles ont besoin d'une classe pour traiter une demande entrante.
Etape 2: Enregistrez le résolveur de dépendance.
Nous avons maintenant notre résolveur de dépendances personnalisé et nous devons l'enregistrer pour que le framework MVC utilise notre résolveur de dépendances. Enregistrez le résolveur de dépendance dans le fichier Global.asax.cs :
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
DependencyResolver.SetResolver(new NinjectDependencyResolver());
// .....
}
Troisième étape: Ajouter des liaisons.
Imaginez que nous avons l'interface et l'implémentation suivantes:
public interface ICustomCache
{
string Info { get; }
}
public class CustomCache : ICustomCache
{
public string Info
{
get
{
return "Hello from CustomCache.";
}
}
}
Si nous voulons utiliser CustomCache dans notre contrôleur sans coupler étroitement notre contrôleur avec CustomCache, nous devons lier ICustomCache à CustomCache et l'injecter à l'aide de Ninject. Tout d'abord, liez ICustomCache à CustomCache en ajoutant le code suivant à la méthode AddBindings () de NinjectDependencyResolver :
private void AddBindings()
{
// Bindings added here
kernel.Bind<ICustomCache>().To<CustomCache>();
}
Ensuite, préparez votre contrôleur pour l'injection comme ci-dessous:
public class HomeController : Controller
{
private ICustomCache CustomCache { get; set; }
public HomeController(ICustomCache customCacheParam)
{
if (customCacheParam == null)
throw new ArgumentNullException(nameof(customCacheParam));
CustomCache = customCacheParam;
}
public ActionResult Index()
{
// cacheInfo: "Hello from CustomCache."
string cacheInfo = CustomCache.Info;
return View();
}
}
Ceci est un exemple d' injection de costructor et c'est une forme d'injection de dépendance . Comme vous le voyez, notre contrôleur domestique ne dépend pas de la classe CustomCache. Si nous voulons utiliser une autre implémentation de ICustomCache dans notre application, la seule chose que nous devons changer est de lier ICustomCache à une autre implémentation et c'est la seule étape que nous devons prendre. Ce qui est arrivé ici est-framework MVC a demandé à notre résolveur enregistré de dépendance de créer une instance de la classe HomeController via la méthode GetService. La méthode GetService demande au noyau Ninject de créer l'objet demandé et le noyau Ninject examine le type dans son terme et découvre que le constructeur de HomeController exige un ICustomCache et que la liaison a déjà été ajoutée pour ICustomCache . Ninject crée une instance de classe liée, l'utilise pour créer HomeController et la renvoie à MVC Framework.
Chaînes de dépendance.
Lorsque Ninject essaie de créer un type, il examine d'autres dépendances entre les types et les autres types et s'il existe un Ninject, il essaie également de les créer. Par exemple, si notre classe CustomCache nécessite ICacheKeyProvider et si la combinaison ajoutée pour ICacheKeyProvider Ninject peut la fournir pour notre classe.
Interface ICacheKeyProvider et implémentation SimpleCacheKeyProvider :
public interface ICacheKeyProvider
{
string GenerateKey(Type type);
}
public class SimpleCacheKeyProvider
: ICacheKeyProvider
{
public string GenerateKey(Type type)
{
if (type == null)
throw new ArgumentNullException(nameof(type));
return string.Format("{0}CacheKey", type.Name);
}
}
Classe CustomCache modifiée
public class CustomCache : ICustomCache
{
private ICacheKeyProvider CacheKeyProvider { get; set; }
public CustomCache(ICacheKeyProvider keyProviderParam)
{
if (keyProviderParam == null)
throw new ArgumentNullException(nameof(keyProviderParam));
CacheKeyProvider = keyProviderParam;
}
...........
}
Ajouter une liaison pour ICacheKeyProvider :
private void AddBindings()
{
// Bindings added here
kernel.Bind<ICustomCache>().To<CustomCache>();
kernel.Bind<ICacheKeyProvider>().To<SimpleCacheKeyProvider>();
}
Maintenant , quand nous naviguons à HomeController Ninject crée instance de SimpleCacheKeyProvider utilise pour créer CustomCache et utilise par exemple pour créer CustomCache HomeController.
Ninject possède un grand nombre de fonctionnalités comme l'injection de dépendances en chaîne et vous devriez les examiner si vous souhaitez utiliser Ninject.