Zum Hauptinhalt springen

II. Grundprinzipien der Softwareentwicklung

Wenn wir uns mit den Grundlagen unserer Praktiken in der Softwareentwicklung befassen, ist es entscheidend, die Grundprinzipien zu verstehen, die unserem Ansatz zugrunde liegen. Diese Prinzipien, bewährt und in der Branche anerkannt, liefern eine Blaupause für die Erstellung von Software, die nicht nur funktional und effizient, sondern auch wartbar, skalierbar und verständlich ist. Sie liefern den theoretischen Hintergrund, der die praktischen Richtlinien in den folgenden Abschnitten prägt. Wir ermutigen alle Entwickler, intern wie extern, nachdrücklich dazu, diese Prinzipien zu verinnerlichen und sie konsequent in ihrer Arbeit anzuwenden.

Unsere Grundprinzipien beruhen auf drei fundamentalen Konzepten: den SOLID-Prinzipien, dem DRY-Prinzip (Don't Repeat Yourself) und dem KISS-Prinzip (Keep It Simple, Stupid). Diese Konzepte adressieren gemeinsam verschiedene Aspekte der Softwareentwicklung, darunter Design, Codierung und Komplexitätsmanagement, und dienen als Leitstern für Entwickler, die sich in der komplexen Landschaft eines Softwareprojekts bewegen.

A. Verständnis der SOLID-Prinzipien​

Die SOLID-Prinzipien bilden ein entscheidendes Rahmenwerk in der objektorientierten Programmierung und im objektorientierten Design mit dem Ziel, Softwaredesigns verständlicher, flexibler und wartbarer zu machen. Richtig angewandt, erzwingen sie ein hohes Maß an Modularität, reduzieren die Fragilität und erhöhen die Robustheit der Software, was zu Code führt, der leichter zu lesen, zu verstehen und zu modifizieren ist.

Die Prinzipien treiben Entwickler von Natur aus dazu, Software zu erstellen, die weniger anfällig für Bugs, leichter zu diagnostizieren und einfacher zu erweitern oder zu skalieren ist. Sie betonen die Erstellung von Softwarekomponenten mit klaren Verantwortlichkeiten und Abhängigkeiten und reduzieren so das Risiko unerwarteter Nebenwirkungen bei Änderungen.

Durch das Verinnerlichen der SOLID-Prinzipien können Entwickler Code produzieren, der von hoher Qualität und zukunftssicher ist und neue Anforderungen effektiv aufnimmt sowie auf Änderungen widerstandsfähig reagiert.

  1. Single Responsibility Principle (SRP)

Das Single Responsibility Principle besagt, dass eine Klasse oder ein Modul einen und nur einen Grund zur Änderung haben sollte. Dieses Prinzip ermutigt Entwickler, ihren Code in eigenständige Teile aufzuteilen, von denen jeder ein separates Anliegen oder eine separate Funktionalität adressiert.

Wenn das SRP befolgt wird, erfordern Änderungen an einem bestimmten Aspekt eines Programms nur Modifikationen an den Klassen oder Modulen, die direkt mit diesem Aspekt zusammenhängen. Diese Kompartimentierung macht die Software leichter verständlich, modifizierbar und diagnostizierbar und reduziert das Risiko, bei Änderungen Bugs in nicht zusammenhängenden Features einzuführen. Sie erhöht außerdem die Flexibilität der Software und erleichtert ihre Fähigkeit, sich im Laufe der Zeit weiterzuentwickeln und an neue Anforderungen oder sich ändernde Bedingungen anzupassen.

Beispiel für einen Verstoß gegen das SRP:

public class Report
{
public string Title { get; set; }
public string Date { get; set; }
public string Content { get; set; }

public void GenerateReport()
{
// Code to generate report
}

public void SaveReport(string filePath)
{
// Code to save report to a file
}

public void PrintReport()
{
// Code to print the report
}
}

Im obigen Beispiel hat die Klasse Report mehr als eine Verantwortung. Sie generiert den Bericht, speichert ihn in einer Datei und druckt ihn. Wenn wir die Art und Weise ändern müssen, wie der Bericht gespeichert wird, riskieren wir, Bugs im Code für die Berichtsgenerierung oder das Drucken einzuführen. Diese Funktionalitäten sollten in verschiedene Klassen aufgeteilt werden, von denen jede eine einzige Verantwortung hat. Dies ist ein Verstoß gegen das SRP.

  1. Open-Closed Principle (OCP)

Das Open-Closed Principle ist ein grundlegendes Prinzip im objektorientierten Design, das besagt: "software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification." Im Wesentlichen sollten Sie in der Lage sein, einem System neue Funktionalität oder neues Verhalten hinzuzufügen, ohne bestehenden Code zu ändern, und so das Risiko minimieren, bestehende Funktionalität zu beschädigen.

Der typische Weg, dies zu erreichen, ist die Verwendung von Interfaces oder abstrakten Klassen, sodass neue Funktionalitäten als neue Klassen hinzugefügt werden können, die diese Interfaces implementieren oder von diesen abstrakten Klassen erben.

Betrachten wir das Beispiel eines Systems, das verschiedene Arten von Zahlungen verarbeitet:

public class PaymentProcessor
{
public void ProcessPayment(string paymentType)
{
if (paymentType == "CreditCard")
{
// Process credit card payment
}
else if (paymentType == "PayPal")
{
// Process PayPal payment
}
// As we add more payment types, this method keeps changing
}
}

Im obigen Code müsste die Klasse PaymentProcessor jedes Mal geändert werden, wenn wir einen neuen Zahlungstyp hinzufügen. Dies ist ein Verstoß gegen das OCP.

So kann es verbessert werden:

public interface IPaymentProcessor
{
void ProcessPayment();
}

public class CreditCardPaymentProcessor : IPaymentProcessor
{
public void ProcessPayment()
{
// Process credit card payment
}
}

public class PayPalPaymentProcessor : IPaymentProcessor
{
public void ProcessPayment()
{
// Process PayPal payment
}
}

Nun kann jedes Mal, wenn ein neuer Zahlungstyp hinzugefügt werden muss, eine neue Klasse erstellt werden, die das Interface IPaymentProcessor implementiert. Dies erlaubt es, das System um die Unterstützung neuer Zahlungstypen zu erweitern, ohne die bestehende Klasse PaymentProcessor oder ihre Methode zu modifizieren.

  1. Liskov Substitution Principle (LSP)

Das Liskov Substitution Principle (LSP) besagt: "if S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program." Einfacher ausgedrückt stellt das LSP sicher, dass eine abgeleitete Klasse ihre Basisklasse effektiv ersetzen kann, ohne die Korrektheit des Programms zu verändern.

Manchmal wird das LSP missverstanden und so gedeutet, dass ein Subtyp lediglich das Verhalten des Basistyps nachahmen kann. Ein präziseres Verständnis ist jedoch, dass ein Subtyp den Vertrag des Basistyps erfüllen können muss. Das bedeutet, ein Subtyp sollte alles tun können, was der Basistyp kann, und darf zusätzliche Fähigkeiten haben, aber er sollte nicht weniger leisten.

Hier ein Beispiel für einen Verstoß gegen das LSP in C#:

public class Bird
{
public virtual void Fly()
{
// code to fly
}
}

public class Penguin : Bird
{
public override void Fly()
{
throw new NotSupportedException("Penguins can't fly");
}
}

Im obigen Beispiel kann Penguin, obwohl er ein Subtyp von Bird ist, den Vertrag des Typs Bird nicht erfüllen, weil er nicht fliegen kann. Somit würde es zu einem Fehler führen, wenn Sie versuchten, ein Penguin-Objekt überall dort zu verwenden, wo ein Bird-Objekt erwartet wird, und dies verstößt gegen das Liskov Substitution Principle.

Ein verbessertes Design könnte so aussehen:

public class Bird
{
}

public class FlyingBird : Bird
{
public virtual void Fly()
{
// code to fly
}
}

public class Penguin : Bird
{
// Penguin does not override the Fly method
}

In diesem refaktorierten Code erben nur flugfähige Vögel von FlyingBird. Dies erlaubt es, ein FlyingBird-Objekt durch einen beliebigen seiner Subtypen wie Eagle oder Sparrow zu ersetzen, wodurch das Programm korrekt bleibt und so das Liskov Substitution Principle gewahrt wird.

  1. Interface Segregation Principle (ISP)

Das Interface Segregation Principle plädiert dafür, dass Clients nicht gezwungen sein sollten, von Interfaces abzuhängen, die sie nicht verwenden. Im Wesentlichen ist es besser, viele spezifische Interfaces zu haben als ein einziges Allzweck-Interface. Auf diese Weise stellen wir sicher, dass eine Klasse nur über die Methoden Bescheid wissen muss, die sie tatsächlich verwendet, wodurch das Fehlerpotenzial reduziert und das Design des Systems vereinfacht wird.

Hier ein Beispiel, das gegen das ISP in C# verstößt:

public interface IWorker
{
void Work();
void Eat();
}

public class HumanWorker : IWorker
{
public void Work()
{
// human working
}

public void Eat()
{
// human eating
}
}

public class RobotWorker : IWorker
{
public void Work()
{
// robot working
}

public void Eat()
{
throw new NotImplementedException("Robots can't eat");
}
}

Im obigen Beispiel ist die Klasse RobotWorker gezwungen, die Methode Eat() zu implementieren, die sie nicht benötigt, weil sie Teil des Interfaces IWorker ist. Dies verstößt gegen das Interface Segregation Principle.

Ein verbessertes Design würde so aussehen:

public interface IWorker
{
void Work();
}

public interface IEater
{
void Eat();
}

public class HumanWorker : IWorker, IEater
{
public void Work()
{
// human working
}

public void Eat()
{
// human eating
}
}

public class RobotWorker : IWorker
{
public void Work()
{
// robot working
}
}

Im refaktorierten Code wird das Interface IWorker in zwei Interfaces aufgeteilt: IWorker und IEater. Nun implementiert HumanWorker sowohl IWorker als auch IEater, während RobotWorker nur IWorker implementiert, wie es sein sollte. Dieses Design hält sich an das Interface Segregation Principle.

  1. Dependency Inversion Principle (DIP)

Das Dependency Inversion Principle ist eine Methode, um Abhängigkeiten zwischen Modulen in einem Softwaresystem zu verwalten. Es besagt, dass High-Level-Module nicht directly von Low-Level-Modulen abhängen sollten; beide sollten von Abstraktionen abhängen. Darüber hinaus sollten Abstraktionen nicht von Details abhängen; Details sollten von Abstraktionen abhängen.

Indem wir dem DIP folgen, machen wir unsere Module wiederverwendbarer und das System flexibler, da die Abhängigkeiten auf Abstraktionen basieren, die sich leicht austauschen lassen, statt auf konkreten Implementierungen.

Betrachten Sie das folgende Beispiel in C#:

public class MySQLDatabase
{
public void Add(string item)
{
// Add item to MySQL database
}
}

public class Inventory
{
private MySQLDatabase _database;

public Inventory(MySQLDatabase database)
{
_database = database;
}

public void AddItem(string item)
{
_database.Add(item);
}
}

Im obigen Beispiel hängt Inventory direkt von MySQLDatabase ab. Wenn wir die Datenbank auf einen anderen Typ ändern wollen, müssten wir die Klasse Inventory ändern. Dies verstößt gegen das DIP.

So kann es verbessert werden:

public interface IDatabase
{
void Add(string item);
}

public class MySQLDatabase : IDatabase
{
public void Add(string item)
{
// Add item to MySQL database
}
}

public class Inventory
{
private IDatabase _database;

public Inventory(IDatabase database)
{
_database = database;
}

public void AddItem(string item)
{
_database.Add(item);
}
}

In diesem refaktorierten Code hängt Inventory von der Abstraktion IDatabase ab, nicht direkt von MySQLDatabase. Nun können wir den Datenbanktyp einfach ändern, indem wir eine andere IDatabase-Implementierung in Inventory injizieren, ohne die Klasse Inventory selbst zu ändern. Dieses Design hält sich an das Dependency Inversion Principle.

B. DRY-Prinzip (Don't Repeat Yourself)​

  1. Verständnis und Anwendung

Das DRY-Prinzip (Don't Repeat Yourself) ist eine grundlegende Praxis in der Softwareentwicklung, die die Reduzierung von Wiederholungen im Code betont. Das Prinzip beruht auf dem Ziel, für jede Information innerhalb eines Systems eine einzige, autoritative und eindeutige Repräsentation zu haben.

In der Praxis impliziert DRY, dass Entwickler wiederkehrende Muster und Funktionalität in wiederverwendbare Komponenten abstrahieren sollten, seien es Funktionen, Klassen, Module oder Services. Auf diese Weise minimieren wir den Bedarf an doppeltem Code und schaffen so einen einzigen Punkt der Wahrheit. Dies macht den Code nicht nur sauberer und leichter lesbar, sondern vereinfacht auch den Änderungsprozess. Wenn eine Änderung erforderlich ist, muss sie nur an einer Stelle umgesetzt werden, statt durch mehrere Instanzen dupliziertem Codes propagiert zu werden.

Es ist jedoch wesentlich zu verstehen, dass die Anwendung von DRY nicht absolut ist. Es gibt Situationen, in denen strikte Befolgung von DRY zu unnötiger Komplexität führen kann, insbesondere wenn versucht wird, die Duplizierung sehr einfacher oder trivialer Code-Schnipsel zu vermeiden, die keine Geschäftslogik kapseln. In solchen Fällen kann die Anwendung von DRY unnötige Abhängigkeiten einführen oder ein Abstraktionsniveau schaffen, das den Code schwerer verständlich und wartbar macht.

Darüber hinaus können Instanzen von Code-Duplizierung oft darauf hinweisen, dass das Abstraktionsniveau nicht geeignet ist. Wenn ähnliche Code-Teile über verschiedene Teile eines Systems verstreut sind, könnte dies darauf hindeuten, dass ein höheres Abstraktionsniveau oder ein anderes Entwurfsmuster von Vorteil sein könnte. Folglich reduziert die Einhaltung des DRY-Prinzips das Fehlerpotenzial, erhöht die Wartbarkeit und hilft, das richtige Abstraktionsniveau innerhalb der Codebasis zu halten.

  1. Vermeidung von Code-Duplizierung

Code-Duplizierung ist eine Praxis, die dem DRY-Prinzip zuwiderläuft. Sie tritt auf, wenn dieselben oder sehr ähnliche Code-Blöcke mehr als einmal in einer Codebasis erscheinen. Auch wenn es kurzfristig wie eine schnelle Lösung erscheinen mag, kann duplizierter Code auf lange Sicht zu einer Vielzahl von Problemen führen.

Erstens kann er Bugs und Fehler in die Software einführen. Wenn ein Bug in einem Stück dupliziertem Code gefunden wird, müsste jede Kopie dieses Codes einzeln gefunden und behoben werden. Dies ist nicht nur zeitaufwendig, sondern erhöht auch die Wahrscheinlichkeit, eine oder mehrere Instanzen zu übersehen, wodurch verbleibende Bugs im System zurückbleiben.

Zweitens kann Code-Duplizierung das System schwerer verständlich und wartbar machen. Mit jeder Wiederholung steigt die kognitive Belastung des Entwicklers, da er mehrere Code-Teile verstehen muss, die dieselbe Aufgabe erfüllen. Diese erhöhte Komplexität kann die Entwicklung verlangsamen und das System fehleranfälliger machen.

Um diese Probleme zu vermeiden, muss man bestrebt sein, Code-Duplizierung zu eliminieren. Dies kann erreicht werden, indem gemeinsame Code-Blöcke in separate Methoden oder Klassen abstrahiert werden oder indem Entwurfsmuster verwendet werden, die Code-Wiederverwendung fördern. Eine wirksame Strategie ist es, stets auf wiederholten Code zu achten und ihn in wiederverwendbare Module zu refaktorieren, sobald eine Duplizierung entdeckt wird.

Allerdings muss auch hier ein Gleichgewicht gefunden werden. Es ist wichtig, nicht vorzeitig oder auf das falsche Niveau zu abstrahieren, nur um Code-Duplizierung zu vermeiden. Dies könnte zu übermäßig komplexen und ineinander verwobenen Systemen führen, die schwer zu entschlüsseln und zu warten sind. Daher geht es beim Eliminieren von Code-Duplizierung nicht nur um die Reduzierung von Wiederholungen, sondern darum, den Code klarer, leichter verständlich und wartbarer zu machen.

  1. Code-Wiederverwendbarkeit und Modularität

Die Einhaltung des DRY-Prinzips fördert auf natürliche Weise die Wiederverwendbarkeit und Modularität von Code, die beide zentral für eine effiziente Softwareentwicklung sind.

Code-Wiederverwendbarkeit bezieht sich auf die Praxis, Code so zu schreiben, dass er in verschiedenen Teilen der Anwendung oder sogar in verschiedenen Anwendungen wiederverwendet werden kann. Wiederverwendbarer Code spart Zeit und Aufwand, da er den Bedarf reduziert, dieselbe Logik mehrfach neu zu schreiben. Darüber hinaus verbessert er die Zuverlässigkeit, da der wiederverwendete Code bereits getestet und debuggt wurde, was die Wahrscheinlichkeit verringert, neue Bugs einzuführen.

Modularität hingegen bezieht sich auf die Entwurfstechnik, die die Funktionalität eines Programms in unabhängige, austauschbare Module trennt. Jedes Modul ist eigenständig und in der Lage, einen einzigartigen Teil der gewünschten Systemfunktionalität auszuführen. Modularität macht ein System leichter verständlich, entwerfbar und wartbar, da Änderungen an einem Modul minimale Auswirkungen auf andere Module haben.

Durch das Abstrahieren gemeinsamer Funktionalitäten in wiederverwendbare Komponenten und Module hilft das DRY-Prinzip direkt dabei, diese wünschenswerten Eigenschaften in einer Codebasis zu erreichen. Jede wiederverwendbare Komponente dient als einzige Quelle der Wahrheit für eine bestimmte Funktionalität und reduziert so Redundanz und erhöht die Kohärenz im System.

Es sei jedoch angemerkt, dass das Streben nach Wiederverwendbarkeit und Modularität nicht zu einem übermäßig verallgemeinerten System führen sollte. Übergeneralisierung kann zu Komplexität führen und das System schwerer verständlich und wartbar machen. Daher ist es wesentlich, ein Gleichgewicht zwischen Code-Wiederverwendung, Modularität und den spezifischen Bedürfnissen und Einschränkungen Ihres Systems zu finden.

C. KISS-Prinzip (Keep It Simple, Stupid)​

  1. Einfachheit vor Komplexität

Das KISS-Prinzip (Keep It Simple, Stupid) ist eine Designrichtlinie, die besagt, dass Einfachheit stets das Hauptziel sein sollte. Dieses Prinzip tritt für die Erstellung geradliniger und leicht verständlicher Lösungen anstelle komplizierter und verworrener ein.

Im Sinne des KISS-Prinzips ist die einfachste Lösung, die die geforderte Funktionalität erfüllt, immer die beste. Dies liegt daran, dass einfache Lösungen leichter verständlich, weniger fehleranfällig und mit höherer Wahrscheinlichkeit in Zukunft korrekt wartbar und erweiterbar sind.

Es ist jedoch wesentlich klarzustellen, dass einfach nicht "quick and dirty" bedeutet. Eine Lösung, die hastig zusammengeschustert wird, ohne die Architektur der Software und zukünftige Wartungsbedürfnisse angemessen zu berücksichtigen, ist nicht einfach; sie ist lediglich kurzsichtig. Wahre Einfachheit entsteht aus gut durchdachten und wohlüberlegten Lösungen, die leicht zu begreifen und zu modifizieren sind.

Darüber hinaus ist es unerlässlich anzuerkennen, dass schwer verständlicher Code im Allgemeinen schlechter Code ist, egal wie clever oder ausgeklügelt er erscheinen mag. Der primäre Wert von Code liegt nicht in der Fähigkeit, einen Computer eine Aufgabe ausführen zu lassen, sondern vielmehr in der Fähigkeit, anderen Entwicklern zu vermitteln, wie diese Aufgabe erledigt wird. Wenn ein Stück Code von einem anderen Entwickler nicht ohne Weiteres verstanden werden kann, versagt er in einem seiner entscheidendsten Aspekte.

  1. Lesbarkeit und Wartbarkeit

Ein wesentliches Ergebnis des KISS-Prinzips ist die erhöhte Lesbarkeit und Wartbarkeit des Codes. Code wird viel häufiger gelesen als geschrieben, was Lesbarkeit zu einem Schlüsselaspekt guter Software macht. Code, der leicht zu lesen ist, ist leichter zu verstehen, leichter zu debuggen und leichter zu warten.

Lesbarkeit entsteht aus Einfachheit. Wenn Code einfach ist, ist es geradlinig zu verstehen, was er tut, was es leichter macht, Fehler zu erkennen und zu beheben. Darüber hinaus ist lesbarer Code auch für andere Entwickler zugänglicher, was die Zusammenarbeit effektiver und produktiver macht.

Wartbarkeit wiederum ist die Leichtigkeit, mit der ein Softwaresystem modifiziert werden kann, um Fehler zu korrigieren, die Performance zu verbessern oder sich an eine sich ändernde Umgebung anzupassen. Wartbarkeit ist entscheidend, weil sie direkt die Geschwindigkeit und die Kosten zukünftiger Änderungen und Erweiterungen beeinflusst.

Indem es Einfachheit und Lesbarkeit fördert, erhöht das KISS-Prinzip die Wartbarkeit der Codebasis. Es wird leichter, Probleme zu erkennen und zu beheben, neue Features hinzuzufügen oder die Software an sich ändernde Anforderungen anzupassen. Deshalb geht es bei KISS nicht nur darum, die Dinge um der Einfachheit willen einfach zu machen, sondern auch darum, die langfristige Gesundheit und Anpassungsfähigkeit des Softwaresystems zu gewährleisten.