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 des logiciels est une bataille entre la complexité et la simplicité. Trouver l’équilibre entre les deux est difficile. Le compromis se situe entre les méthodes longues et 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 effectue également des tonnes de tests. Leur logiciel est littéralement critique pour la mission – une affaire à une seule chance. Pour la plupart des organisations, ce n’est pas le cas et de grandes quantités de tests sont coûteuses et impratiques. 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 si possible.

1. Écrire du code simple

Simple devrait être facile, mais ce ne l’est pas. É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 des articles ultérieurs.

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 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é la recommandation de Microsoft d’une complexité cyclomatique de 25 trop élevée. Pour la maintenabilité et la complexité, j’ai trouvé que la taille idéale de la méthode est 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 la traduction de votre code C# en code exécutable par la machine est un processus en deux étapes. Le compilateur C# génère du IL qui est livré dans les assemblies. Le compilateur JIT génère du code machine pour chaque méthode (ou groupe de méthodes, lorsque l’inlining est impliqué), selon les besoins. Les petites fonctions facilitent beaucoup la tâche du compilateur JIT pour amortir ce coût. Les petites fonctions sont également plus susceptibles d’être candidates pour l’inlining. Ce n’est pas seulement la petitesse : le flux de contrôle plus simple compte tout autant. Moins de branches de contrôle dans les fonctions facilitent la tâche du compilateur JIT pour enregistrer les variables. Ce n’est pas seulement une bonne pratique d’écrire un code plus clair ; c’est ainsi que vous créez un 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 simples — garder le code simple réduit votre taux de défauts.

2. Écrire du code testable

Des études ont montré que l’écriture de code testable, sans écrire les tests réels, réduit les incidents de défauts. C’est tellement important et profond que cela mérite d’être répété : Écrire du code testable, sans écrire les tests réels, réduit 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 simulées à partir d’un test. Un exemple de dépendance est une requête de base de données. Dans un test, les données sont simulées (fausses) et une assertion du comportement attendu est faite. Si l’assertion est vraie, le test réussit, sinon il échoue.

Écrire du code testable peut sembler difficile, mais en fait, c’est facile en suivant les principes Inversion of Control (Dependency Injection) et S.O.L.I.D. Vous serez surpris par la facilité et vous vous demanderez pourquoi cela a pris 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 la revue de code.

Les revues de code facilitent le partage des connaissances entre les 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 en écriture de code.

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

  • Une étude d’une organisation chez AT&T avec plus de 200 personnes a rapporté une augmentation de productivité de 14 % et une diminution de défauts de 90 % après que l’organisation ait introduit les revues.
    • La compagnie d’assurance Aetna a trouvé 82 % des erreurs dans un programme en utilisant des inspections et a pu réduire 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é libérés 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 erreurs pour 100. Les revues ont réduit les erreurs de plus de 80 %.

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

4. Tests unitaires

Je l’admets, quand je suis confronté à une date limite, les tests sont la première chose à abandonner. Mais les avantages des tests ne peuvent pas être niés comme les études suivantes l’illustrent.

Microsoft a effectué une étude sur l’efficacité des tests unitaires. Ils ont constaté que la version 2 du codage (la version 1 n’avait pas de tests) avec les tests automatisés a immédiatement réduit les défauts de 20 %, mais au coût d’une augmentation supplémentaire de 30 %.

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

Enfin, dans une étude sur la couverture de test et les défauts post-vérification :

… Nous constatons que dans les deux projets, l’augmentation de la couverture de test est
associée à une diminution des problèmes signalés sur le terrain lorsqu’elle est ajustée pour
le nombre de changements avant la libération…

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 inférieure à 5.

Y a-t-il des dépendances ?

Oui. Il y a 2 services AgencySettingsRepository et EmailService.

Les services sont-ils simulables ?

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

Le code est-il testable ?

Non, ce code n’est pas testable car nous ne pouvons pas simuler AgencySettingsRepository et EmailService.

Exemple de code refactorisé

Comment pouvons-nous rendre ce code testable ?

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

Voici la version refactorisée.

Remarquez comment les services sont injectés dans le constructeur. Cela nous permet de contrôler quelle implémentation est transmise au constructeur SendMail. Il est alors facile de transmettre des données factices et d’intercepter les appels de méthode 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

Voici un exemple de test en isolation. Nous utilisons le framework de simulation 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 du code sans défauts (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ésumer, Écrire du code simple consiste à maintenir la complexité cyclomatique autour de 5 et à garder la taille de la méthode petite. Écrire du code testable est facilement réalisable en suivant les principes Inversion of Control et 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 — le simple fait d’avoir à expliquer votre code révélera des problèmes. Et enfin, les tests unitaires peuvent considérablement améliorer la qualité de votre code et fournir une documentation pour les futurs développeurs.

Auteur : Chuck Conway est un ingénieur IA avec près de 30 ans d’expérience en génie logiciel. Il construit des systèmes IA pratiques — pipelines de contenu, agents d’infrastructure et outils qui résolvent des problèmes réels — et partage ce qu’il apprend en chemin. Connectez-vous avec lui sur les réseaux sociaux : X (@chuckconway) ou visitez-le sur YouTube et sur SubStack.

↑ Retour en haut

Vous aimerez peut-être aussi