Skip to content

Articles

4 Pratiques pour Réduire Votre Taux de Défauts

17 novembre 2015 • 10 min de lecture

4 Pratiques pour Réduire Votre Taux de Défauts

Écrire du logiciel est une bataille entre complexité et simplicité. Trouver l’équilibre entre les deux est difficile. Le compromis se situe entre de longues méthodes non maintenables et trop d’abstraction. Pencher trop loin dans l’une ou l’autre direction nuit à la lisibilité du code et augmente la probabilité de défauts.

Les défauts sont-ils évitables ? La NASA essaie, mais elle fait aussi des tonnes de tests. Leur logiciel est littéralement critique pour la mission – une affaire à un seul coup. Pour la plupart des organisations, ce n’est pas le cas et de grandes quantités de tests sont coûteuses et impraticables. Bien qu’il n’y ait pas de substitut aux tests, il est possible d’écrire du code résistant aux défauts, sans tests.

En 20 ans de codage et d’architecture d’applications, j’ai identifié quatre pratiques pour réduire les défauts. Les deux premières pratiques limitent l’introduction de défauts et les deux dernières pratiques exposent les défauts. Chaque pratique est un vaste sujet en soi sur lequel de nombreux livres ont été écrits. J’ai distillé chaque pratique en quelques paragraphes et j’ai fourni des liens vers des informations supplémentaires quand c’est possible.

1. Écrire du Code Simple

Simple devrait être facile, mais ce n’est pas le cas. Écrire du code simple est difficile.

Certains liront ceci et penseront que cela signifie utiliser des fonctionnalités de langage simples, mais ce n’est pas le cas — le code simple n’est pas du code stupide.

Pour rester objectif, j’utilise la complexité cyclomatique comme mesure. Il existe d’autres façons de mesurer la complexité et d’autres types de complexité, j’espère explorer ces sujets dans de futurs articles.

Microsoft définit la complexité cyclomatique comme :

La complexité cyclomatique mesure le nombre de chemins
linéairement indépendants à travers la méthode, qui est déterminé
par le nombre et la complexité des branches conditionnelles. Une
faible complexité cyclomatique indique généralement une méthode
qui est facile à comprendre, tester et maintenir.

Qu’est-ce qu’une faible complexité cyclomatique ? Microsoft recommande de maintenir la complexité cyclomatique en dessous de 25.

Pour être honnête, j’ai trouvé que la recommandation de Microsoft d’une complexité cyclomatique de 25 était trop élevée. Pour la maintenabilité et la complexité, j’ai trouvé que la taille idéale d’une méthode se situe entre 1 et 10 lignes avec une complexité cyclomatique entre 1 et 5.

Bill Wagner dans Effective C#, Second Edition a écrit sur la taille des méthodes :

Rappelez-vous que traduire votre code C# en code exécutable par la machine est un processus en deux étapes. Le compilateur C# génère de l’IL qui est livré dans des assemblies. Le compilateur JIT génère du code machine pour chaque méthode (ou groupe de méthodes, quand l’inlining est impliqué), selon les besoins. Les petites fonctions facilitent beaucoup l’amortissement de ce coût par le compilateur JIT. Les petites fonctions sont aussi plus susceptibles d’être candidates à l’inlining. Ce n’est pas seulement la petitesse : Un flux de contrôle plus simple compte tout autant. Moins de branches de contrôle à l’intérieur des fonctions facilitent l’enregistrement des variables par le compilateur JIT. Ce n’est pas seulement une bonne pratique d’écrire du code plus clair ; c’est ainsi que vous créez du code plus efficace à l’exécution.

Pour mettre la complexité cyclomatique en perspective, la méthode suivante a une complexité cyclomatique de 12.

public string ComplexityOf12(int status)
{
    var isTrue = true;
    var myString = "Chuck";

    if (isTrue)
    {
        if (isTrue)
        {
            myString = string.Empty;
            isTrue = false;

            for (var index = 0; index < 10; index++)
            {
                isTrue |= Convert.ToBoolean(new Random().Next());
            }

            if (status == 1 || status == 3)
            {
                switch (status)
                {
                    case 3:
                        return "Bye";
                    case 1:
                        if (status % 1 == 0)
                        {
                            myString = "Super";
                        }
                        break;
                }

                return myString;
            }
        }
    }

    if (!isTrue)
    {
        myString = "New";
    }

    switch (status)
    {
        case 300:
            myString = "3001";
            break;
        case 400:
            myString = "4003";
            break;

    }

    return myString;
}

Une hypothèse de complexité généralement acceptée postule qu’une corrélation positive existe entre la complexité et les défauts.

La ligne précédente est un peu alambiquée. En termes les plus simples — garder le code simple réduit votre taux de défauts.

2. Écrire du Code Testable

Des études ont montré qu’écrire du code testable, sans écrire les tests réels, diminue les incidents de défauts. C’est si important et profond que cela mérite d’être répété : Écrire du code testable, sans écrire les tests réels, diminue les incidents de défauts.

Cela soulève la question, qu’est-ce que le code testable ?

Je définis le code testable comme du code qui peut être testé en isolation. Cela signifie que toutes les dépendances peuvent être mockées depuis un test. Un exemple de dépendance est une requête de base de données. Dans un test, les données sont mockées (simulées) et une assertion du comportement attendu est faite. Si l’assertion est vraie, le test passe, sinon il échoue.

Écrire du code testable peut sembler difficile, mais, en fait, c’est facile quand on suit l’Inversion de Contrôle (Injection de Dépendance) et les principes S.O.L.I.D. Vous serez surpris de la facilité et vous vous demanderez pourquoi il a fallu si longtemps pour commencer à écrire de cette façon.

3. Revues de Code

L’une des pratiques les plus impactantes qu’une équipe de développement peut adopter est les revues de code.

Les Revues de Code facilitent le partage de connaissances entre développeurs. D’après mon expérience, discuter ouvertement du code avec d’autres développeurs a eu le plus grand impact sur mes compétences d’écriture de code.

Dans le livre Code Complete, par Steve McConnell, Steve fournit de nombreuses études de cas sur les bénéfices des revues de code :

  • Une étude d’une organisation chez AT&T avec plus de 200 personnes a rapporté une augmentation de 14 % de la productivité et une diminution de 90 % des défauts après que l’organisation ait introduit les revues.
    • La compagnie d’assurance Aetna a trouvé 82 % des erreurs dans un programme en utilisant les inspections et a pu diminuer ses ressources de développement de 20 %.
    • Dans un groupe de 11 programmes développés par le même groupe de personnes, les 5 premiers ont été développés sans revues. Les 6 restants ont été développés avec des revues. Après que tous les programmes aient été mis en production, les 5 premiers avaient une moyenne de 4,5 erreurs pour 100 lignes de code. Les 6 qui avaient été inspectés avaient une moyenne de seulement 0,82 erreur. Les revues ont réduit les erreurs de plus de 80 %.

Si ces chiffres ne vous convainquent pas d’adopter les revues de code, alors vous êtes destiné à dériver dans un trou noir en chantant Johnny Paycheck’s Take This Job and Shove It.

4. Tests Unitaires

J’admets que quand je suis face à une échéance, les tests sont la première chose à disparaître. Mais les bénéfices des tests ne peuvent être niés comme l’illustrent les études suivantes.

Microsoft a effectué une étude sur l’Efficacité des Tests Unitaires. Ils ont trouvé que coder la version 2 (la version 1 n’avait pas de tests) avec des tests automatisés a immédiatement réduit les défauts de 20 %, mais au coût de 30 % supplémentaires.

Une autre étude a examiné le Développement Piloté par les Tests (TDD). Ils ont observé une augmentation de la qualité du code, plus de deux fois, comparé à des projets similaires n’utilisant pas TDD. Les projets TDD ont pris en moyenne 15 % de temps en plus à développer. Un effet secondaire du TDD était que les tests servaient de documentation pour les bibliothèques et API.

Enfin, dans une étude sur Couverture de Tests et Défauts Post-Vérification :

… Nous trouvons que dans les deux projets l’augmentation de la
couverture de tests est associée à une diminution des problèmes
rapportés sur le terrain quand ajustée pour le nombre de changements
pré-release…

Un Exemple

Le code suivant a une complexité cyclomatique de 4.

    public void SendUserHadJoinedEmailToAdministrator(DataAccess.Database.Schema.dbo.Agency agency, User savedUser)
    {
        AgencySettingsRepository agencySettingsRepository = new AgencySettingsRepository();
        var agencySettings = agencySettingsRepository.GetById(agency.Id);

        if (agencySettings != null)
        {
            var newAuthAdmin = agencySettings.NewUserAuthorizationContact;

            if (newAuthAdmin.IsNotNull())
            {
                EmailService emailService = new EmailService();

                emailService.SendTemplate(new[] { newAuthAdmin.Email }, GroverConstants.EmailTemplate.NewUserAdminNotification, s =>
                {
                    s.Add(new EmailToken { Token = "Domain", Value = _settings.Domain });
                    s.Add(new EmailToken
                    {
                        Token = "Subject",
                        Value =
                    string.Format("New User {0} has joined {1} on myGrover.", savedUser.FullName(), agency.Name)
                    });
                    s.Add(new EmailToken { Token = "Name", Value = savedUser.FullName() });

                    return s;
                });
            }
        }
    }

Examinons la testabilité du code ci-dessus.

Est-ce du code simple ?

Oui, c’est le cas, la complexité cyclomatique est en dessous de 5.

Y a-t-il des dépendances ?

Oui. Il y a 2 services AgencySettingsRepository et EmailService.

Les services sont-ils mockables ?

Non, leur création est cachée dans la méthode.

Le code est-il testable ?

Non, ce code n’est pas testable parce que nous ne pouvons pas mocker AgencySettingsRepository et EmailService.

Exemple de Code Refactorisé

Comment pouvons-nous rendre ce code testable ?

Nous injectons (en utilisant l’injection par constructeur) AgencySettingsRepository et EmailService comme dépendances. Cela nous permet de les mocker depuis un test et de tester en isolation.

Ci-dessous se trouve la version refactorisée.

Notez comment les services sont injectés dans le constructeur. Cela nous permet de contrôler quelle implémentation est passée dans le constructeur SendMail. Il est alors facile de passer des données factices et d’intercepter les appels de méthodes de service.

public class SendEmail
{
    private IAgencySettingsRepository _agencySettingsRepository;
    private IEmailService _emailService;


    public SendEmail(IAgencySettingsRepository agencySettingsRepository, IEmailService emailService)
    {
        _agencySettingsRepository = agencySettingsRepository;
        _emailService = emailService;
    }

    public void SendUserHadJoinedEmailToAdministrator(DataAccess.Database.Schema.dbo.Agency agency, User savedUser)
    {
        var agencySettings = _agencySettingsRepository.GetById(agency.Id);

        if (agencySettings != null)
        {
            var newAuthAdmin = agencySettings.NewUserAuthorizationContact;

            if (newAuthAdmin.IsNotNull())
            {
                _emailService.SendTemplate(new[] { newAuthAdmin.Email },
                GroverConstants.EmailTemplate.NewUserAdminNotification, s =>
                {
                    s.Add(new EmailToken { Token = "Domain", Value = _settings.Domain });
                    s.Add(new EmailToken
                    {
                        Token = "Subject",
                        Value = string.Format("New User {0} has joined {1} on myGrover.", savedUser.FullName(), agency.Name)
                    });
                    s.Add(new EmailToken { Token = "Name", Value = savedUser.FullName() });

                    return s;
                });
            }
        }
    }
}

Exemple de Test

Ci-dessous se trouve un exemple de test en isolation. Nous utilisons le framework de mocking FakeItEasy.

    [Test]
    public void TestEmailService()
    {
        //Given

        //Using FakeItEasy mocking framework
        var repository = A<IAgencySettingsRepository>.Fake();
        var service = A<IEmailService>.Fake();

        var agency = new Agency { Name = "Acme Inc." };
        var user = new User { FirstName = "Chuck", LastName = "Conway", Email = "chuck.conway@fakedomain.com" }

        //When

        var sendEmail = new SendEmail(repository, service);
        sendEmail.SendUserHadJoinedEmailToAdministrator(agency, user);


        //Then
        //An exception is thrown when this is not called.
        A.CallTo(() => service.SendTemplate(A<Agency>.Ignore, A<User>.Ignore)).MustHaveHappened();

    }

Conclusion

Écrire du code résistant aux défauts est étonnamment facile. Ne vous méprenez pas, vous n’écrirez jamais de code sans défaut (si vous découvrez comment, faites-le moi savoir !), mais en suivant les 4 pratiques décrites dans cet article, vous verrez une diminution des défauts trouvés dans votre code.

Pour récapituler, Écrire du Code Simple consiste à maintenir la complexité cyclomatique autour de 5 et la taille des méthodes petite. Écrire du Code Testable est facilement réalisé en suivant l’Inversion de Contrôle et les Principes S.O.L.I.D. Les Revues de Code vous aident, vous et l’équipe, à comprendre le domaine et le code que vous avez écrit — juste avoir à expliquer votre code révélera des problèmes. Et enfin, Les Tests Unitaires peuvent drastiquement améliorer la qualité de votre code et fournir de la documentation pour les futurs développeurs.

Auteur : Chuck Conway se spécialise dans l’ingénierie logicielle et l’IA générative. Connectez-vous avec lui sur les réseaux sociaux : X (@chuckconway) ou visitez-le sur YouTube.

↑ Retour en haut

Vous pourriez aussi aimer