Skip to content

Innlegg

4 Praksiser for å Redusere Din Feilrate

17. november 2015 • 9 min lesing

4 Praksiser for å Redusere Din Feilrate

Å skrive programvare er en kamp mellom kompleksitet og enkelhet. Å finne balansen mellom de to er vanskelig. Avveiningen er mellom lange uvedlikeholdbare metoder og for mye abstraksjon. Å lene for langt i begge retninger svekker kodelesbarhet og øker sannsynligheten for feil.

Er feil unngåelige? NASA prøver, men de gjør også lastebillass med testing. Deres programvare er bokstavelig talt oppdragskritisk – en engangsaffære. For de fleste organisasjoner er ikke dette tilfellet, og store mengder testing er kostbart og upraktisk. Selv om det ikke finnes noen erstatning for testing, er det mulig å skrive feilresistent kode, uten testing.

I 20 år med koding og arkitektur av applikasjoner har jeg identifisert fire praksiser for å redusere feil. De to første praksisene begrenser introduksjonen av feil og de to siste praksisene avslører feil. Hver praksis er et omfattende tema i seg selv som det er skrevet mange bøker om. Jeg har destillert hver praksis til et par avsnitt og har gitt lenker til tilleggsinformasjon når det er mulig.

1. Skriv Enkel Kode

Enkelt burde være lett, men det er det ikke. Å skrive enkel kode er vanskelig.

Noen vil lese dette og tenke at dette betyr å bruke enkle språkfunksjoner, men dette er ikke tilfellet — enkel kode er ikke dum kode.

For å holde det objektivt, bruker jeg syklomatisk kompleksitet som et mål. Det finnes andre måter å måle kompleksitet på og andre typer kompleksitet, jeg håper å utforske disse temaene i senere artikler.

Microsoft definerer syklomatisk kompleksitet som:

Syklomatisk kompleksitet måler antall lineært-uavhengige
stier gjennom metoden, som bestemmes av antallet og
kompleksiteten til betingede forgreninger. En lav syklomatisk kompleksitet
indikerer generelt en metode som er lett å forstå, teste og
vedlikeholde.

Hva er en lav syklomatisk kompleksitet? Microsoft anbefaler å holde syklomatisk kompleksitet under 25.

For å være ærlig, har jeg funnet at Microsofts anbefaling om syklomatisk kompleksitet på 25 er for høy. For vedlikeholdbarhet og kompleksitet har jeg funnet at den ideelle metodestørrelsen er mellom 1 til 10 linjer med en syklomatisk kompleksitet mellom 1 og 5.

Bill Wagner i Effective C#, Second Edition skrev om metodestørrelse:

Husk at oversettelse av din C#-kode til maskinutførbar kode er en to-trinns prosess. C#-kompilatoren genererer IL som leveres i assemblies. JIT-kompilatoren genererer maskinkode for hver metode (eller gruppe av metoder, når inlining er involvert), etter behov. Små funksjoner gjør det mye lettere for JIT-kompilatoren å amortisere den kostnaden. Små funksjoner er også mer sannsynlig å være kandidater for inlining. Det er ikke bare småhet: Enklere kontrollflyt betyr like mye. Færre kontrollgrener inne i funksjoner gjør det lettere for JIT-kompilatoren å registrere variabler. Det er ikke bare god praksis å skrive klarere kode; det er slik du skaper mer effektiv kode ved kjøretid.

For å sette syklomatisk kompleksitet i perspektiv, har følgende metode en syklomatisk kompleksitet på 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;
}

En generelt akseptert kompleksitetshypotese postulerer at det eksisterer en positiv korrelasjon mellom kompleksitet og feil.

Den forrige linjen er litt innviklet. I de enkleste termer — å holde kode enkel reduserer din feilrate.

2. Skriv Testbar Kode

Studier har vist at å skrive testbar kode, uten å skrive de faktiske testene, senker forekomsten av feil. Dette er så viktig og dyptgripende at det trenger gjentagelse: Å skrive testbar kode, uten å skrive de faktiske testene, senker forekomsten av feil.

Dette reiser spørsmålet, hva er testbar kode?

Jeg definerer testbar kode som kode som kan testes isolert. Dette betyr at alle avhengighetene kan mockes fra en test. Et eksempel på en avhengighet er en databasespørring. I en test blir dataene mocket (falsket) og en påstand om forventet oppførsel blir gjort. Hvis påstanden er sann, består testen, hvis ikke feiler den.

Å skrive testbar kode kan høres vanskelig ut, men faktisk er det lett når man følger Inversion of Control (Dependency Injection) og S.O.L.I.D prinsippene. Du vil bli overrasket over hvor lett det er og vil lure på hvorfor det tok så lang tid å begynne å skrive på denne måten.

3. Kodegjennomganger

En av de mest innflytelsesrike praksisene et utviklingsteam kan adoptere er kodegjennomganger.

Kodegjennomganger tilrettelegger for kunnskapsdeling mellom utviklere. Ut fra erfaring har åpen diskusjon av kode med andre utviklere hatt størst innvirkning på mine kodeskrivingsferdigheter.

I boken Code Complete, av Steve McConnell, gir Steve tallrike casestudier om fordelene med kodegjennomganger:

  • En studie av en organisasjon hos AT&T med mer enn 200 personer rapporterte en 14 prosent økning i produktivitet og en 90 prosent reduksjon i feil etter at organisasjonen introduserte gjennomganger.
    • Aetna Insurance Company fant 82 prosent av feilene i et program ved å bruke inspeksjoner og klarte å redusere sine utviklingsressurser med 20 prosent.
    • I en gruppe på 11 programmer utviklet av samme gruppe mennesker, ble de første 5 utviklet uten gjennomganger. De resterende 6 ble utviklet med gjennomganger. Etter at alle programmene ble sluppet til produksjon, hadde de første 5 et gjennomsnitt på 4,5 feil per 100 linjer kode. De 6 som hadde blitt inspisert hadde et gjennomsnitt på bare 0,82 feil. Gjennomganger kuttet feilene med over 80 prosent.

Hvis disse tallene ikke overtaler deg til å adoptere kodegjennomganger, så er du bestemt til å drive inn i et svart hull mens du synger Johnny Paycheck’s Take This Job and Shove It.

4. Enhetstesting

Jeg innrømmer, når jeg er opp mot en frist er testing det første som forsvinner. Men fordelene med testing kan ikke benektes som følgende studier illustrerer.

Microsoft utførte en studie på Effektiviteten av Enhetstesting. De fant at koding av versjon 2 (versjon 1 hadde ingen testing) med automatisert testing umiddelbart reduserte feil med 20%, men til en kostnad på ytterligere 30%.

En annen studie så på Test Driven Development (TDD). De observerte en økning i kodekvalitet, mer enn to ganger, sammenlignet med lignende prosjekter som ikke brukte TDD. TDD-prosjekter tok i gjennomsnitt 15% lengre tid å utvikle. En bieffekt av TDD var at testene fungerte som dokumentasjon for bibliotekene og API-ene.

Til slutt, i en studie om Testdekning og Post-Verifikasjonsfeil:

… Vi finner at i begge prosjektene er økningen i testdekning
assosiert med reduksjon i feltrapporterte problemer når justert for
antall pre-release endringer…

Et Eksempel

Følgende kode har en syklomatisk kompleksitet på 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;
                });
            }
        }
    }

La oss undersøke testbarheten til koden ovenfor.

Er dette enkel kode?

Ja, det er det, den syklomatiske kompleksiteten er under 5.

Er det noen avhengigheter?

Ja. Det er 2 tjenester AgencySettingsRepository og EmailService.

Er tjenestene mockbare?

Nei, deres opprettelse er skjult innenfor metoden.

Er koden testbar?

Nei, denne koden er ikke testbar fordi vi ikke kan mocke AgencySettingsRepository og EmailService.

Eksempel på Refaktorert Kode

Hvordan kan vi gjøre denne koden testbar?

Vi injiserer (ved å bruke konstruktørinjeksjon) AgencySettingsRepository og EmailService som avhengigheter. Dette lar oss mocke dem fra en test og teste isolert.

Nedenfor er den refaktorerte versjonen.

Legg merke til hvordan tjenestene injiseres inn i konstruktøren. Dette lar oss kontrollere hvilken implementering som sendes inn i SendMail konstruktøren. Det er da lett å sende dummy-data og fange opp tjenestemetodekallene.

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;
                });
            }
        }
    }
}

Testeksempel

Nedenfor er et eksempel på testing isolert. Vi bruker mocking-rammeverket 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();

    }

Avslutning

Å skrive feilresistent kode er overraskende lett. Ikke misforstå meg, du vil aldri skrive feilfri kode (hvis du finner ut hvordan, gi meg beskjed!), men ved å følge de 4 praksisene skissert i denne artikkelen vil du se en reduksjon i feil funnet i koden din.

For å oppsummere, Å Skrive Enkel Kode er å holde den syklomatiske kompleksiteten rundt 5 og metodestørrelsen liten. Å Skrive Testbar Kode oppnås lett når man følger Inversion of Control og S.O.L.I.D Prinsippene. Kodegjennomganger hjelper deg og teamet å forstå domenet og koden du har skrevet — bare det å måtte forklare koden din vil avdekke problemer. Og til slutt kan Enhetstesting drastisk forbedre kodekvaliteten din og gi dokumentasjon for fremtidige utviklere.

Forfatter: Chuck Conway spesialiserer seg på programvareutvikling og Generativ AI. Koble til ham på sosiale medier: X (@chuckconway) eller besøk ham på YouTube.

↑ Tilbake til toppen

Du liker kanskje også