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 Réponses :
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.
"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:
Créez une classe qui gère les "Applications" (par exemple "ApplicationManager").
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.
Créez une sous-classe concrète pour chaque "Application": public class NewLicense : Application
, public class LicenseRenewal : Application
, etc. etc.
... ET ...
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 :)
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.
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.
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.