
Das Schreiben von Software ist ein Kampf zwischen Komplexität und Einfachheit. Das Gleichgewicht zwischen beiden zu finden ist schwierig. Der Kompromiss liegt zwischen langen, nicht wartbaren Methoden und zu viel Abstraktion. Eine zu starke Neigung in eine der beiden Richtungen beeinträchtigt die Lesbarkeit des Codes und erhöht die Wahrscheinlichkeit von Fehlern.
Sind Fehler vermeidbar? Die NASA versucht es, aber sie führt auch unmengen von Tests durch. Ihre Software ist buchstäblich missionskritisch – eine einmalige Angelegenheit. Für die meisten Organisationen ist dies nicht der Fall und große Mengen an Tests sind kostspielig und unpraktisch. Obwohl es keinen Ersatz für Tests gibt, ist es möglich, fehlerresistenten Code zu schreiben, ohne zu testen.
In 20 Jahren des Programmierens und Architekturdesigns von Anwendungen 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 umfangreiches Thema für sich, über das viele Bücher geschrieben wurden. Ich habe jede Praktik auf ein paar Absätze destilliert und Links zu zusätzlichen Informationen bereitgestellt, wo möglich.
1. Einfachen Code schreiben
Einfach sollte leicht sein, aber das ist es nicht. Einfachen Code zu schreiben ist schwer.
Einige werden dies lesen und denken, dass dies bedeutet, einfache Sprachfeatures zu verwenden, aber das ist nicht der Fall — einfacher Code ist nicht dummer Code.
Um objektiv zu bleiben, verwende ich die zyklomatische Komplexität als Maß. Es gibt andere Wege, 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
zeigt im Allgemeinen eine Methode an, 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 festgestellt, dass Microsofts Empfehlung einer zyklomatischen Komplexität von 25 zu hoch ist. Für Wartbarkeit und Komplexität habe ich festgestellt, dass die ideale Methodengröße zwischen 1 bis 10 Zeilen mit einer zyklomatischen Komplexität zwischen 1 und 5 liegt.
Bill Wagner schrieb in Effective C#, Second Edition ü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 Assemblies geliefert wird. Der JIT-Compiler generiert Maschinencode für jede Methode (oder Gruppe von Methoden, wenn Inlining beteiligt ist), nach Bedarf. Kleine Funktionen machen es dem JIT-Compiler viel einfacher, diese Kosten zu amortisieren. Kleine Funktionen sind auch eher Kandidaten für Inlining. Es geht nicht nur um Kleinheit: Einfacherer Kontrollfluss ist genauso wichtig. Weniger Kontrollverzweigungen innerhalb von Funktionen machen es dem JIT-Compiler einfacher, 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 — Code einfach zu halten reduziert Ihre Fehlerrate.
2. Testbaren Code schreiben
Studien haben gezeigt, dass das Schreiben von testbarem Code, ohne die tatsächlichen Tests zu schreiben, die Häufigkeit von Fehlern senkt. Das ist so wichtig und tiefgreifend, dass es wiederholt werden muss: Das Schreiben von testbarem Code, ohne die tatsächlichen Tests zu schreiben, senkt die Häufigkeit von Fehlern.
Das wirft die Frage auf, was ist testbarer Code?
Ich definiere testbaren Code als Code, der isoliert getestet werden kann. Das bedeutet, alle Abhängigkeiten können von einem Test gemockt werden. Ein Beispiel für eine Abhängigkeit ist eine Datenbankabfrage. In einem Test werden die Daten gemockt (gefälscht) und eine Behauptung über das erwartete Verhalten aufgestellt. Wenn die Behauptung wahr ist, besteht der Test, wenn nicht, schlägt er fehl.
Das Schreiben von testbarem Code mag schwer klingen, aber tatsächlich ist es einfach, wenn man der Inversion of Control (Dependency Injection) und den S.O.L.I.D Prinzipien folgt. Sie werden von der Leichtigkeit überrascht sein und sich fragen, warum es so lange gedauert hat, auf diese Weise zu schreiben.
3. Code-Reviews
Eine der wirkungsvollsten Praktiken, die ein Entwicklungsteam übernehmen kann, sind Code-Reviews.
Code-Reviews erleichtern den Wissensaustausch zwischen Entwicklern. Aus Erfahrung kann ich sagen, dass die offene Diskussion von Code mit anderen Entwicklern den größten Einfluss auf meine Code-Schreibfähigkeiten hatte.
In dem Buch Code Complete, von Steve McConnell, liefert 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 Steigerung der Produktivität und einer 90-prozentigen Verringerung der Fehler, nachdem die Organisation Reviews eingeführt hatte.
- Die Aetna Insurance Company fand 82 Prozent der Fehler in einem Programm durch Inspektionen und konnte ihre Entwicklungsressourcen um 20 Prozent verringern.
- In einer Gruppe von 11 Programmen, die von derselben Gruppe von Personen entwickelt wurden, wurden die ersten 5 ohne Reviews entwickelt. Die verbleibenden 6 wurden mit Reviews entwickelt. Nachdem alle Programme in die Produktion freigegeben wurden, hatten die ersten 5 durchschnittlich 4,5 Fehler pro 100 Codezeilen. Die 6, die inspiziert worden waren, hatten durchschnittlich nur 0,82 Fehler. Reviews reduzierten die Fehler um über 80 Prozent.
Wenn diese Zahlen Sie nicht dazu bewegen, Code-Reviews zu übernehmen, 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-Tests
Ich gebe zu, wenn ich unter Zeitdruck stehe, ist das Testen das Erste, was wegfällt. Aber die Vorteile des Testens können nicht geleugnet werden, wie die folgenden Studien veranschaulichen.
Microsoft führte eine Studie über die Wirksamkeit von Unit-Tests durch. Sie fanden heraus, dass die Programmierung von Version 2 (Version 1 hatte keine Tests) mit automatisierten Tests sofort die Fehler um 20% reduzierte, aber zu Kosten von zusätzlichen 30%.
Eine andere Studie betrachtete Test Driven Development (TDD). Sie beobachteten eine Steigerung der Codequalität um mehr als das Doppelte im Vergleich zu ähnlichen Projekten, die TDD nicht verwendeten. TDD-Projekte dauerten im Durchschnitt 15% länger zu entwickeln. Ein Nebeneffekt von TDD war, dass die Tests als Dokumentation für die Bibliotheken und APIs dienten.
Schließlich, in einer Studie über Testabdeckung und Post-Verifikations-Fehler:
… Wir stellen fest, dass in beiden Projekten die Erhöhung der Testabdeckung
mit einer Verringerung der im Feld gemeldeten Probleme verbunden ist, wenn sie für
die Anzahl der Vorab-Änderungen 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 das einfacher Code?
Ja, das ist er, die zyklomatische Komplexität liegt unter 5.
Gibt es Abhängigkeiten?
Ja. Es gibt 2 Services AgencySettingsRepository
und EmailService
.
Sind die Services mockbar?
Nein, ihre Erstellung ist innerhalb 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. Das ermöglicht es uns, sie von einem Test zu mocken und isoliert zu testen.
Unten ist die refaktorierte Version.
Beachten Sie, wie die Services in den Konstruktor injiziert werden. Das 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;
});
}
}
}
}
Test-Beispiel
Unten ist ein Beispiel für das Testen in Isolation. 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();
}
Fazit
Das Schreiben von fehlerresistentem Code ist überraschend einfach. Verstehen Sie mich nicht falsch, Sie werden niemals fehlerfreien Code schreiben (wenn Sie herausfinden wie, lassen Sie es mich wissen!), aber durch das Befolgen der 4 in diesem Artikel beschriebenen Praktiken werden Sie eine Verringerung der in Ihrem Code gefundenen Fehler sehen.
Zur Zusammenfassung: Einfachen Code schreiben bedeutet, die zyklomatische Komplexität um 5 zu halten und die Methodengröße klein. Testbaren Code schreiben wird leicht erreicht, wenn man der Inversion of Control und den S.O.L.I.D Prinzipien folgt. Code-Reviews helfen Ihnen und dem Team, die Domäne und den Code, den Sie geschrieben haben, zu verstehen — allein die Tatsache, dass Sie Ihren Code erklären müssen, wird Probleme aufdecken. Und schließlich können Unit-Tests Ihre Codequalität drastisch verbessern und Dokumentation für zukünftige Entwickler bereitstellen.
Autor: Chuck Conway ist spezialisiert auf Software-Engineering und Generative KI. Verbinden Sie sich mit ihm in den sozialen Medien: X (@chuckconway) oder besuchen Sie ihn auf YouTube.