Das Schreiben von Software ist ein Kampf zwischen Komplexität und Einfachheit. Das Gleichgewicht zwischen den beiden zu finden ist schwierig. Der Kompromiss liegt zwischen langen, wartungsunfähigen Methoden und zu viel Abstraktion. Eine zu starke Neigung in eine der beiden Richtungen beeinträchtigt die Codelesbarkeit und erhöht die Wahrscheinlichkeit von Fehlern.
Sind Fehler vermeidbar? Die NASA versucht es, aber sie führen auch Lastwagen voller Tests durch. Ihre Software ist buchstäblich missionskritisch – ein einmaliges Unterfangen. Für die meisten Organisationen ist dies nicht der Fall, und große Mengen an Tests sind kostspielig und unpraktisch. Während es keinen Ersatz für Tests gibt, ist es möglich, fehlerresistenten Code zu schreiben, ohne zu testen.
In 20 Jahren Programmierung und Anwendungsarchitektur habe ich vier Praktiken identifiziert, um Fehler zu reduzieren. Die ersten beiden Praktiken begrenzen die Einführung von Fehlern, und die letzten beiden Praktiken decken Fehler auf. Jede Praktik ist ein großes Thema für sich, über das viele Bücher geschrieben wurden. Ich habe jede Praktik auf ein paar Absätze verdichtet und habe Links zu zusätzlichen Informationen bereitgestellt, wenn möglich.
1. Schreiben Sie einfachen Code
Einfach sollte leicht sein, aber es ist nicht so. Einfachen Code zu schreiben ist schwer.
Einige werden dies lesen und denken, dass dies die Verwendung einfacher Sprachfunktionen bedeutet, aber das ist nicht der Fall – einfacher Code ist nicht dummer Code.
Um es objektiv zu halten, verwende ich die zyklomatische Komplexität als Maßstab. Es gibt andere Möglichkeiten, Komplexität zu messen, und andere Arten von Komplexität. Ich hoffe, diese Themen in späteren Artikeln zu erkunden.
Microsoft definiert zyklomatische Komplexität als:
Die zyklomatische Komplexität misst die Anzahl der linear unabhängigen
Pfade durch die Methode, die durch die Anzahl und
Komplexität der bedingten Verzweigungen bestimmt wird. Eine niedrige zyklomatische Komplexität
deutet im Allgemeinen auf eine Methode hin, die leicht zu verstehen, zu testen und
zu warten ist.
Was ist eine niedrige zyklomatische Komplexität? Microsoft empfiehlt, die zyklomatische Komplexität unter 25 zu halten.
Um ehrlich zu sein, habe ich Microsofts Empfehlung einer zyklomatischen Komplexität von 25 für zu hoch befunden. Für Wartbarkeit und Komplexität habe ich festgestellt, dass die ideale Methodengröße zwischen 1 und 10 Zeilen liegt, mit einer zyklomatischen Komplexität zwischen 1 und 5.
Bill Wagner in Effective C#, Second Edition schrieb über Methodengröße:
Denken Sie daran, dass die Übersetzung Ihres C#-Codes in maschinenausführbaren Code ein zweistufiger Prozess ist. Der C#-Compiler generiert IL, das in Assemblys bereitgestellt wird. Der JIT-Compiler generiert Maschinencode für jede Methode (oder Gruppe von Methoden, wenn Inlining beteiligt ist), je nach Bedarf. Kleine Funktionen machen es dem JIT-Compiler viel leichter, diese Kosten zu amortisieren. Kleine Funktionen sind auch eher Kandidaten für Inlining. Es geht nicht nur um Kleinheit: Ein einfacherer Kontrollfluss ist genauso wichtig. Weniger Kontrollverzweigungen in Funktionen machen es dem JIT-Compiler leichter, Variablen zu registrieren. Es ist nicht nur gute Praxis, klareren Code zu schreiben; es ist, wie Sie zur Laufzeit effizienteren Code erstellen.
Um die zyklomatische Komplexität in Perspektive zu setzen, hat die folgende Methode eine zyklomatische Komplexität von 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;
}
Eine allgemein akzeptierte Komplexitätshypothese postuliert eine positive Korrelation zwischen Komplexität und Fehlern.
Die vorherige Zeile ist etwas verworren. In einfachsten Worten – das Halten von Code einfach reduziert Ihre Fehlerquote.
2. Schreiben Sie testbaren Code
Studien haben gezeigt, dass das Schreiben von testbarem Code, ohne die eigentlichen Tests zu schreiben, die Anzahl der Fehler senkt. Dies ist so wichtig und tiefgreifend, dass es wiederholt werden muss: Das Schreiben von testbarem Code, ohne die eigentlichen Tests zu schreiben, senkt die Anzahl der Fehler.
Dies wirft die Frage auf: Was ist testbarer Code?
Ich definiere testbaren Code als Code, der isoliert getestet werden kann. Das bedeutet, dass alle Abhängigkeiten von einem Test aus gemockt werden können. Ein Beispiel für eine Abhängigkeit ist eine Datenbankabfrage. In einem Test werden die Daten gemockt (gefälscht) und eine Assertion des erwarteten Verhaltens wird gemacht. Wenn die Assertion wahr ist, besteht der Test, wenn nicht, schlägt er fehl.
Das Schreiben von testbarem Code mag schwierig klingen, aber tatsächlich ist es einfach, wenn man die Inversion of Control (Dependency Injection) und die S.O.L.I.D Prinzipien befolgt. Sie werden überrascht sein, wie einfach es ist, und werden sich fragen, warum es so lange gedauert hat, auf diese Weise zu schreiben.
3. Code Reviews
Eine der wirkungsvollsten Praktiken, die ein Entwicklungsteam einführen kann, sind Code Reviews.
Code Reviews erleichtern den Wissensaustausch zwischen Entwicklern. Aus meiner Erfahrung heraus hat die offene Diskussion von Code mit anderen Entwicklern den größten Einfluss auf meine Fähigkeiten beim Schreiben von Code gehabt.
In dem Buch Code Complete von Steve McConnell bietet Steve zahlreiche Fallstudien über die Vorteile von Code Reviews:
- Eine Studie einer Organisation bei AT&T mit mehr als 200 Personen berichtete von einer 14-prozentigen Produktivitätssteigerung und einer 90-prozentigen Reduktion von Fehlern, nachdem die Organisation Reviews einführte.
- Die Aetna Insurance Company fand 82 Prozent der Fehler in einem Programm durch Inspektionen und konnte ihre Entwicklungsressourcen um 20 Prozent reduzieren.
- In einer Gruppe von 11 Programmen, die von derselben Gruppe von Personen entwickelt wurden, wurden die ersten 5 ohne Reviews entwickelt. Die restlichen 6 wurden mit Reviews entwickelt. Nachdem alle Programme in die Produktion gingen, hatten die ersten 5 durchschnittlich 4,5 Fehler pro 100 Codezeilen. Die 6, die inspiziert worden waren, hatten durchschnittlich nur 0,82 Fehler pro 100. Reviews reduzierten die Fehler um über 80 Prozent.
Wenn diese Zahlen Sie nicht dazu bewegen, Code Reviews einzuführen, dann sind Sie dazu bestimmt, in ein schwarzes Loch zu driften, während Sie Johnny Paychecks Take This Job and Shove It singen.
4. Unit Testing
Ich gebe zu, wenn ich gegen eine Frist ankämpfe, ist Testing das erste, das wegfällt. Aber die Vorteile des Testens können nicht geleugnet werden, wie die folgenden Studien zeigen.
Microsoft führte eine Studie über die Effektivität von Unit Testing durch. Sie fanden heraus, dass die Codierung von Version 2 (Version 1 hatte keine Tests) mit automatisiertem Testing sofort Fehler um 20% reduzierte, aber zu Kosten von zusätzlichen 30%.
Eine weitere Studie untersuchte Test Driven Development (TDD). Sie beobachteten eine Verbesserung der Codequalität um mehr als das Zweifache im Vergleich zu ähnlichen Projekten, die TDD nicht verwendeten. TDD-Projekte dauerten durchschnittlich 15% länger zu entwickeln. Ein Nebeneffekt von TDD war, dass die Tests als Dokumentation für die Bibliotheken und APIs dienten.
Abschließend in einer Studie über Test Coverage and Post-Verification Defects:
… Wir stellen fest, dass in beiden Projekten die Zunahme der Testabdeckung
mit einer Abnahme der im Feld gemeldeten Probleme verbunden ist, wenn sie für
die Anzahl der Änderungen vor der Veröffentlichung angepasst wird…
Ein Beispiel
Der folgende Code hat eine zyklomatische Komplexität von 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;
});
}
}
}
Lassen Sie uns die Testbarkeit des obigen Codes untersuchen.
Ist dies einfacher Code?
Ja, es ist, die zyklomatische Komplexität liegt unter 5.
Gibt es irgendwelche Abhängigkeiten?
Ja. Es gibt 2 Services AgencySettingsRepository und EmailService.
Sind die Services mockbar?
Nein, ihre Erstellung ist in der Methode verborgen.
Ist der Code testbar?
Nein, dieser Code ist nicht testbar, weil wir AgencySettingsRepository und EmailService nicht mocken können.
Beispiel für refaktorierten Code
Wie können wir diesen Code testbar machen?
Wir injizieren (mit Constructor Injection) AgencySettingsRepository und EmailService als Abhängigkeiten. Dies ermöglicht es uns, sie von einem Test aus zu mocken und isoliert zu testen.
Unten ist die refaktorierte Version.
Beachten Sie, wie die Services in den Konstruktor injiziert werden. Dies ermöglicht es uns zu kontrollieren, welche Implementierung in den SendMail Konstruktor übergeben wird. Es ist dann einfach, Dummy-Daten zu übergeben und die Service-Methodenaufrufe abzufangen.
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;
});
}
}
}
}
Testing Beispiel
Unten ist ein Beispiel für das Testen isoliert. Wir verwenden das Mocking-Framework 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();
}
Abschluss
Das Schreiben von fehlerresistentem Code ist überraschend einfach. Verstehen Sie mich nicht falsch, Sie werden nie fehlerfreien Code schreiben (wenn Sie herausfinden, wie, sagen Sie mir Bescheid!), aber wenn Sie die 4 in diesem Artikel beschriebenen Praktiken befolgen, werden Sie eine Abnahme der in Ihrem Code gefundenen Fehler sehen.
Zusammenfassend ist das Schreiben von einfachem Code das Halten der zyklomatischen Komplexität um 5 und die Methodengröße klein. Das Schreiben von testbarem Code wird leicht erreicht, wenn man die Inversion of Control und die S.O.L.I.D Prinzipien befolgt. Code Reviews helfen Ihnen und dem Team, die Domäne und den Code, den Sie geschrieben haben, zu verstehen – allein die Notwendigkeit, Ihren Code zu erklären, wird Probleme offenbaren. Und schließlich kann Unit Testing Ihre Codequalität drastisch verbessern und Dokumentation für zukünftige Entwickler bereitstellen.
Autor: Chuck Conway ist ein KI-Ingenieur mit fast 30 Jahren Erfahrung in der Softwareentwicklung. Er entwickelt praktische KI-Systeme – Content-Pipelines, Infrastruktur-Agenten und Tools, die echte Probleme lösen – und teilt seine Erkenntnisse unterwegs. Verbinden Sie sich mit ihm in den sozialen Medien: X (@chuckconway) oder besuchen Sie ihn auf YouTube und auf SubStack.