0
votes

Trop d'injection de dépendances

J'ai actuellement une classe avec environ 40 injections de dépendances. C'est un test unitaire difficile à maintenir. Je ne suis pas sûr d'un bon moyen de contourner.

Le code est fait pour tout type de processus de candidature nécessaire au traitement (nouvelle licence, renouvellement de licence, inscription d'étudiant, ...), il existe environ 80 types d'applications différents et les sections associées à chaque type d'application sont déterminées par un table de base de données.

J'ai une classe avec toutes les propriétés possibles, il y en a plusieurs plus que répertoriées mais vous devriez avoir l'idée. Chacune des propriétés a son propre ensemble de propriétés qui sont des types de données de base ou des objets pointant vers d'autres classes.

Load(applicationTypeId, applicationId)
{
  Get the sections for the application type
  For each section in the sections
     switch sectionid
        case Documents
           Load all of the documents required for the application type and get any documents uploaded
        case Accounting
           Load the payment details, if no payment made calculate the payment
        case IndividualAddressContact
           Load the person name/address/contact and set a few defaults if the person hasn't started.
        .....
  next
}

Save()
{
     Save the application
     switch current section
        case Documents
           Save all of the documents for the application
        case Accounting
           Save the payment details for the application
        case IndividualAddressContact
           Save the person name/address/contact for the application
        .....       
     get the next section
     Update the application current section
}

Donc, cette classe est envoyée / reçue à un frontal Angular 10 à l'aide d'API Web.

Lorsqu'une application est demandée, les sections et les différentes propriétés sont lancées et si l'application a été lancée, la progression sera rechargée. Il est donc possible que certaines propriétés soient extraites de la base de données et envoyées à l'application Angular.

Alors j'ai quelque chose comme

class Application
{
        [JsonProperty(PropertyName = "accounting")]
        public Accounting Accounting { get; set; }
   
        [JsonProperty(PropertyName = "application")]
        public Application Application { get; set; }

        [JsonProperty(PropertyName = "applicationType")]
        public ApplicationType ApplicationType { get; set; }
   
        [JsonProperty(PropertyName = "document")]
        public List<Attachment> Document { get; set; }

        [JsonProperty(PropertyName = "employment")]
        public List<Employment> Employment { get; set; }

        [JsonProperty(PropertyName = "enrollment")]
        public Enrollment Enrollment { get; set; }
       
        [JsonProperty(PropertyName = "individualAddressContact")]
        public IndividualAddressContact IndividualAddressContact { get; set; }

        [JsonProperty(PropertyName = "instructors")]
        public List<Instructor> Instructors { get; set; }

        [JsonProperty(PropertyName = "license")]
        public License License { get; set; }

        [JsonProperty(PropertyName = "licenseRenewal")]
        public LicenseRenewal LicenseRenewal { get; set; }
   
        [JsonProperty(PropertyName = "MilitaryService")]
        public List<MilitaryService> MilitaryService { get; set; }

        [JsonProperty(PropertyName = "paymentDetail")]
        public PaymentDetail PaymentDetail { get; set; }

        [JsonProperty(PropertyName = "photo")]
        public List<Attachment> Photo { get; set; }

        [JsonProperty(PropertyName = "portal")]
        public Portal Portal { get; set; }
   
        [JsonProperty(PropertyName = "section")]
        public List<Section> Section { get; set; }

        [JsonProperty(PropertyName = "testingCalendar")]
        public TestingCalendar TestingCalendar { get; set; }

        [JsonProperty(PropertyName = "testingScore")]
        public List<TestingScore> TestingScore { get; set; }

        [JsonProperty(PropertyName = "USCitizen")]
        public USCitizen USCitizen { get; set; }
}

J'ai mis tous les éléments du commutateur dans leurs propres classes, mais à la fin, j'ai toujours 1 point pour la sérialisation / désérialisation et je me retrouve toujours avec de nombreuses dépendances injectées. Créer un test unitaire avec plus de 40 dépendances semble difficile à maintenir et étant donné que je ne saurai pas quelles propriétés seront / ne seront pas utilisées jusqu'à ce qu'une application soit demandée et chargée à partir de la base de données. Je ne sais pas comment contourner le commutateur, sans avoir à un moment donné toutes les dépendances injectées dans une classe.

J'apprécierais quelques idées sur la façon de contourner ce problème.


3 commentaires

Vous devez disposer d'un contrôleur et / ou d'une action distincts pour chaque type d'application. Ayez également des modèles séparés pour chacun d'eux ... puis ajoutez uniquement les dépendances nécessaires aux contrôleurs spécifiques. Et appelez l'API appropriée à partir de l'angulaire.


@ChetanRanpariya faux, plusieurs points de terminaison APi sont une recette pour un SPA super bavard et lent.


Le point de terminaison de niveau supérieur doit être un courtier pour d'autres services, chacun avec leurs propres dépendances, chargés à la demande à partir d'un IServiceProvider . Considérez la conception de MVC, il ne charge pas toutes les dépendances de chaque point de terminaison pour chaque demande. Tu as besoin de faire la même chose.


3 Réponses :


1
votes

Je recommanderais d'utiliser la convention sur le principe de configuration, avec le localisateur de service.

Déclarez quelque chose comme l'interface IApplicationHandler dans votre programme, par exemple

    [TestClass]
    public class AccountingApplicationQueryHandlerTests 
    {
        [TestMethod]
        public void TestPopulate()
        {
            // arrange
            var application = new Application();
            var handler = new AccountingApplicationQueryHandler(); // inject mocks here

            // act
            var result = handler.Populate(application);

            // Assert
            Assert.AreEqual(result. PaymentDetail, "whatever");
        }
    }

Ensuite, écrivez des morceaux de votre code, avec des dépendances et autres, par exemple

var queryHandlers = Assembly.GetAssembly(typeof(IApplicationQueryHandler)).GetExportedTypes()
            .Where(x => x.GetInterfaces().Any(y => y == typeof(IApplicationQueryHandler)));

foreach(queryHandler in queryHandlers) {
  services.AddTransient(typeof(IApplicationQueryHandler), queryHandler);
}
// repeat the same for IApplicationSaveHandler

Ensuite, dans votre contrôleur, faites quelque chose comme

public class ApplicationController: Controller
{
   public readonly IServiceProvider _serviceProvider;
   
   public ApplicationController(IServiceProvider sp) {
     _serviceProvider = sp;
   }

  public Application Load(string applicationTypeId, string applicationId)
  {
     var application = new Application(); // or get from db or whatever
     var queryHandlers = _serviceProvider.GetServices(typeof(IApplicationQueryHandler));

     foreach(var handler in queryHandlers) {
       application = handler.Populate(application);
     }
     
     return application;
  }

  [HttpPost]
  public bool Save(Application application) 
  {
    var result = true;
    var saveHandlers = _serviceProvider.GetServices(typeof(IApplicationSaveHandler));

     foreach(var handler in queryHandlers) {
       result = handler. Save(application);
     }
     
    return result;
  }
}

Vous devrez enregistrer vos gestionnaires, ce que vous pouvez faire par exemple comme ceci:

public class AccountingApplicationQueryHandler : IApplicationQueryHandler
{
   public Application Populate(Application application) {
     //// Load the payment details, if no payment made calculate the payment
    return application;
   }
}

public class AccountingApplicationSaveHandler : IApplicationSaveHandler
{
   public Bool Save(Application application) {
     //// Save the payment details for the application
    return true; // this just flags for validation
   }
}

// repeat for all other properties

Maintenant, enfin, vous pouvez écrire des tests unitaires pour une partie du code comme ceci

public interface IApplicationQueryHandler 
{
   Application Populate(Application application);
}

public interface IApplicationSaveHandler 
{
   Bool Save(Application application);
}

Et vous pouvez tester que votre contrôleur appelle les bonnes choses en se moquant de IServiceProvider et en l'injectant avec quelques gestionnaires factices pour confirmer qu'ils sont appelés correctement.


0 commentaires

3
votes

"J'ai actuellement une classe avec environ 40 injections de dépendances ..." - Oh mon Dieu!

"C'est un test unitaire difficile à maintenir ..." - Je n'en doute pas du tout!

REFACTORISATION SUGGÉRÉE:

  1. Créez une classe qui gère les "Applications" (par exemple "ApplicationManager").

  2. Créez une classe abstraite "Application".

    Un avantage de la "classe abstraite" par rapport à "l'interface" ici est que vous pouvez mettre du "code commun" dans la classe de base abstraite.

  3. Créez une sous-classe concrète pour chaque "Application": public class NewLicense : Application , public class LicenseRenewal : Application , etc. etc.

... ET ...

  1. Utilisez DI principalement pour les "services" dont chaque classe concrète a besoin.

    Je parie que les constructeurs de vos classes concrètes individuelles n'auront besoin que d'injecter trois ou quatre services ... au lieu de 40. Qui sait - peut-être que votre classe de base n'aura pas du tout besoin de DI.

C'est en fait une conception que nous utilisons dans l'un de nos systèmes de production. C'est simple; c'est robuste; c'est flexible. Cela fonctionne bien pour nous :)


1 commentaires

Les classes concrètes ne fonctionneront pas, je préfère ma méthode. J'ai ajouté la plupart du processus de candidature sans ajouter de code de ligne. Je dis simplement au système quels composants utiliser. J'ai essayé de refactoriser un peu plus, mais la plupart des classes passées sont des super-classes qui ont chacune 5 à 10 dépendances.



1
votes

Suite à la réponse de zaitsman, vous pouvez également créer AggregatedApplicationQueryHandler et AggregatedApplicationSaveHandler et transmettre une collection d'implémentation concrète de IApplicationQueryHandler et IApplicationSaveHandler à son constructeur.

Ensuite, vous n'avez pas besoin de foreach boucle à l'intérieur du contrôleur (vous bouclez sur les gestionnaires à l'intérieur du gestionnaire agrégé) et n'avez toujours qu'un seul gestionnaire passé au contrôleur. Passer son paramètre par constructeur ne devrait pas être si pénible.

Vous pouvez également créer une façade sur certains petits services et regrouper leurs fonctions en un seul service de façade plus grand.


0 commentaires