Programvareutvikling er en kamp mellom kompleksitet og enkelhet. Det er vanskelig å finne balanse mellom de to. Avveiingen er mellom lange, ikke-vedlikeholdbare metoder og for mye abstraksjon. Hvis du vipper for langt i noen av retningene, forverres kodelesbarheten og sannsynligheten for defekter øker.
Er defekter unngåelige? NASA prøver, men de gjør også enorme mengder testing. Deres programvare er bokstavelig talt kritisk for oppdraget – en engangsaffære. For de fleste organisasjoner er dette ikke tilfelle, og store mengder testing er kostbar og upraktisk. Selv om det ikke finnes noen erstatning for testing, er det mulig å skrive defektresistent kode uten testing.
I løpet av 20 år med koding og arkitekturering av applikasjoner har jeg identifisert fire praksis for å redusere defekter. De to første praksisene begrenser introduksjonen av defekter, og de to siste praksisene avslører defekter. Hver praksis er et omfattende emne i seg selv som mange bøker har blitt skrevet om. Jeg har destillert hver praksis ned til et par avsnitt, og jeg 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 tro at det betyr å bruke enkle språkfunksjoner, men det er ikke tilfelle – 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, og jeg håper å utforske disse emnene i senere artikler.
Microsoft definerer syklomatisk kompleksitet som:
Syklomatisk kompleksitet måler antallet lineært uavhengige
stier gjennom metoden, som bestemmes av antallet og
kompleksiteten til betingede grener. 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 Microsofts anbefaling om syklomatisk kompleksitet på 25 å være 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 oversettelsen av C#-koden din til maskineksekvérbar kode er en to-trinns prosess. C#-kompilatoren genererer IL som leveres i samlinger. 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 denne kostnaden. Små funksjoner er også mer sannsynlig å være kandidater for inlining. Det er ikke bare småhet: Enklere kontrollflyt er like viktig. 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 hvordan du lager 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 defekter.
Den forrige linjen er litt innviklet. I de enkleste termer – å holde koden enkel reduserer defektfrekvensen din.
2. Skriv testbar kode
Studier har vist at å skrive testbar kode, uten å skrive de faktiske testene, reduserer forekomsten av defekter. Dette er så viktig og dyptgripende at det må gjentas: Å skrive testbar kode, uten å skrive de faktiske testene, reduserer forekomsten av defekter.
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 (falskt) og en påstand om forventet oppførsel gjøres. Hvis påstanden er sann, består testen, hvis ikke mislykkes den.
Å skrive testbar kode kan høres vanskelig ut, men faktisk er det enkelt når du følger Inversion of Control (Dependency Injection) og S.O.L.I.D prinsippene. Du vil bli overrasket over hvor enkelt det er og lure på hvorfor det tok så lang tid å begynne å skrive på denne måten.
3. Kodeanmeldelser
En av de mest virkningsfulle praksisene et utviklingsteam kan ta i bruk er kodeanmeldelser.
Kodeanmeldelser legger til rette for kunnskapsdeling mellom utviklere. Basert på erfaring har åpen diskusjon av kode med andre utviklere hatt den største innvirkningen på mine kodeskrivingsferdigheter.
I boken Code Complete, av Steve McConnell, gir Steve mange casestudier om fordelene med kodeanmeldelser:
- En studie av en organisasjon på AT&T med mer enn 200 personer rapporterte en 14 prosent økning i produktivitet og en 90 prosent reduksjon i defekter etter at organisasjonen introduserte anmeldelser.
- Aetna Insurance Company fant 82 prosent av feilene i et program ved å bruke inspeksjoner og var i stand til å redusere utviklingsressursene sine med 20 prosent.
- I en gruppe på 11 programmer utviklet av samme gruppe mennesker, ble de første 5 utviklet uten anmeldelser. De resterende 6 ble utviklet med anmeldelser. Etter at alle programmene ble utgitt til produksjon, hadde de første 5 et gjennomsnitt på 4,5 feil per 100 linjer kode. De 6 som hadde blitt inspektert hadde et gjennomsnitt på bare 0,82 feil per 100. Anmeldelser reduserte feilene med over 80 prosent.
Hvis disse tallene ikke overtaler deg til å ta i bruk kodeanmeldelser, er du bestemt til å drive inn i et svart hull mens du synger Johnny Paychecks Take This Job and Shove It.
4. Enhetstesting
Jeg innrømmer at når jeg er under tidspress er testing det første som må bort. Men fordelene med testing kan ikke fornektes som følgende studier illustrerer.
Microsoft gjennomførte en studie om Effectiveness on Unit Testing. De fant at kodingsversjon 2 (versjon 1 hadde ingen testing) med automatisert testing umiddelbart reduserte defekter med 20%, men til en kostnad av 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 bruker 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 Test Coverage and Post-Verification Defects:
… Vi finner at i begge prosjektene er økningen i testdekning
forbundet med reduksjon i feltrapporterte problemer når det justeres for
antallet endringer før utgivelse…
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.
Finnes det noen avhengigheter?
Ja. Det finnes 2 tjenester AgencySettingsRepository og EmailService.
Kan tjenestene mockes?
Nei, deres opprettelse er skjult inne i metoden.
Er koden testbar?
Nei, denne koden er ikke testbar fordi vi ikke kan mocke AgencySettingsRepository og EmailService.
Eksempel på refaktorisert kode
Hvordan kan vi gjøre denne koden testbar?
Vi injiserer (ved hjelp av konstruktørinjeksjon) AgencySettingsRepository og EmailService som avhengigheter. Dette lar oss mocke dem fra en test og teste isolert.
Nedenfor er den refaktoriserte 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 enkelt å sende dummy-data og avskjære servicemetodekallene.
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 mockrammeverket 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 defektresistent kode er overraskende enkelt. Ikke ta meg feil, du vil aldri skrive feilfri kode (hvis du finner ut hvordan, gi meg beskjed!), men ved å følge de 4 praksisene som er skissert i denne artikkelen vil du se en reduksjon i defekter som finnes i koden din.
For å oppsummere, Å skrive enkel kode er å holde den syklomatiske kompleksiteten rundt 5 og metodestørrelsen liten. Å skrive testbar kode oppnås enkelt når du følger Inversion of Control og S.O.L.I.D prinsippene. Kodeanmeldelser hjelper deg og teamet med å forstå domenet og koden du har skrevet – bare det å måtte forklare koden din vil avsløre problemer. Og til slutt, Enhetstesting kan drastisk forbedre kodekvaliteten din og gi dokumentasjon for fremtidige utviklere.
Forfatter: Chuck Conway er en AI-ingeniør med nesten 30 års erfaring innen programvareutvikling. Han bygger praktiske AI-systemer—innholdspipelines, infrastrukturagenter og verktøy som løser virkelige problemer—og deler det han lærer underveis. Koble til ham på sosiale medier: X (@chuckconway) eller besøk ham på YouTube og på SubStack.