Technische Ansätze
Techniken
Was es schon alles gibt
Techniken
Was es schon alles gibt
Es gibt viele Programmieransätze, die sich bewährt haben und die in vielen Projekten eingesetzt werden. Hier werden einige dieser Ansätze vorgestellt, die in der Softwareentwicklung eine wichtige Rolle spielen und nicht neu erfunden werden müssen.
Allen Kapiteln wurde eine eindeutige Nummerierung, der Richtliniennummer, hinzugefügt, um eine eindeutige Identifikation zu ermöglichen. Jede Richtliniennummer besteht aus dem Buchstaben DPT(Design Pattern Technical) gefolgt von einer Nummer, die den Abschnitt identifiziert. Damit kann eine Regel eindeutig identifiziert werden, z.B. für ein Code-Review.
Feature Flags sind Mechanismen, die es Entwicklern ermöglichen, bestimmte Funktionen oder Codeabschnitte zur Laufzeit zu aktivieren oder zu deaktivieren, ohne den Code neu zu deployen. Dies unterstützt eine flexiblere und sicherere Softwarebereitstellung.
Feature-Flags sind keine Konfigurationsdateien
Feature Flags sollten nicht als Ersatz für Konfigurationsdateien verwendet werden. Sie sind dazu gedacht, die Bereitstellung von Funktionen zu steuern, nicht die Konfiguration von Anwendungen. Die Bereitstellung von Funktionen soll dabei sofort und ohne Neustart der Anwendung erfolgen.
Durch Feature Flags wird das Deployment vom Release entkoppelt, was folgende Vorteile bietet:
Viele Sprachen ermöglichen es, Variablen mit einem null
-Wert zu initialisieren (oder nil
, None
, undefined
, etc.). Dieser spezielle Fall wird von Entwicklern und Entwicklerinnen verwendet, um zu signalisieren, dass ein Wert nicht vorhanden ist. Allerdings wird dieser Fall nicht immer korrekt kommuniziert oder einfach vom Benutzer übersehen. Dies führt dann zur Laufzeit zu NullPointerExceptions
oder noch schlimmeren Fehlern wie Speicherüberläufen.
Das Null-Objekt-Muster ist ein technischer Ansatz, um dieses Problem zu umgehen, indem ein spezielles Objekt erstellt oder verwendet wird, das den null
-Wert repräsentiert, sich jedoch sonst wie ein normales Objekt mit leeren Werte verhält.
Ein Beispiel für die Verwendung des Null-Objekt-Musters ist die Verwendung eines leeren Arrays oder einer leeren Liste anstelle von null
, um anzuzeigen, dass keine Elemente vorhanden sind.
public List<String> getNames() {
if (hasNames()) {
return new ArrayList<>(Arrays.asList("Alice", "Bob", "Charlie"));
} else {
return Collections.emptyList();
}
}
Ein String sollte niemals null
sein, sondern stattdessen ein leerer String oder eine spezielle Konstante, die als Referenz verwendet werden.
public String getName() {
// ...
return "";
}
final static String NOT_DEFINED = "";
public String getName() {
// ...
return NOT_DEFINED;
}
// ...
boolean isUnDefined = getName() == NOT_DEFINED; // kein equals, da es sich um eine Referenz handelt
// isUnDefined == true
boolean isEmpty = getName().equals("");
// isEmpty == true
Sobald ein Datenobjekt zurückgegeben soll, die Methode jedoch keinen Wert zurückgeben kann, sollte ein Null-Objekt zurückgegeben werden, das die Schnittstelle implementiert, jedoch keine Daten enthält. Auch hier kann wieder ein globales Datenobjekt definiert werden, das verwendet werden kann, wenn der Benutzer prüfen möchte, ob ein Wert vorhanden ist (nicht-null) oder nicht (null).
Ein Null-Objekt hat keine Auswirkungen auf die weitere Verarbeitung, da es keine Daten enthält und die weitere Verarbeitung nicht beeinflusst.
public Data getData() {
if (hasData()) {
return new Data("important data");
} else {
return Data.NULL;
}
}
// ...
Data data = getData();
// Zugriff immer sicher
data.doSomething();
if (data != Data.NULL) {
// Daten verarbeiten
}
In Legacy-Code kann es schwierig sein, das Null-Objekt-Muster zu implementieren, da es viele Stellen gibt, an denen null
-Werte verwendet werden. Um das Null-Objekt-Muster in Legacy-Code zu implementieren, kann es erforderlich sein, den Code schrittweise zu refaktorisieren, um die Verwendung von null
-Werten zu reduzieren. Eine Möglichkeit besteht darin, Methoden-Aufrufe, die null
zurückgeben können, in einer neuen Methode zu kapseln (Proxy-Muster), die das Null-Objekt-Muster implementiert. Allerdings kann es erforderlich sein, dass weiter Code angepasst werden muss, da es sonst in UI-Elementen zu leeren Anzeigen, allerdings mit Funktionalität (z.B. Senden von leeren Daten) kommen kann.
Schritte für eine Umstellung:
null
-Werten: Suchen nach Stellen im Code, an denen null
-Werte verwendet werden. Prüfen, welche Datenobjekte null
sein können und wie diese umstrukturiert werden können.null
sein können. Die einfachsten sind meist Listen, Maps und Arrays, welche durch leere Listen oder Arrays ersetzt werden können.null
-Werten: Ersetzen von null
-Werten durch Null-Objekte in eine eigenen gekapselten Methode. Beachte, dass Fehler auch als solche behandelt werden müssen und nicht durch leere Datenobjekte ersetzt werden dürfen.null
prüften und nun auf leere Datenobjekte prüfen müssen.Optional
Das Null-Objekt darf nicht mit einem Optional-Typ verwechselt werden, der in einigen Programmiersprachen wie Java oder Kotlin verwendet wird, um einen Wert zu kapseln, der möglicherweise null
sein kann.
null
-Werten zu reduzieren und das Null-Objekt-Muster zu implementieren.Ein häufig vorgebrachtes Argument gegen das Null-Objekt-Muster ist, dass es Fehler verschleiern könnte, anstatt sie sofort erkennbar zu machen. Tatsächlich ist es in vielen Situationen essenziell, dass Fehler frühzeitig entdeckt werden, um deren Ursache schnell zu identifizieren und zu beheben.
Das Null-Objekt-Muster dient jedoch nicht dazu, Fehler zu verbergen, sondern vielmehr dazu, Fehler zu vermeiden, die durch vergessene oder inkonsistente null
-Prüfungen entstehen. Durch die konsequente Verwendung von Null-Objekten wird sichergestellt, dass anstelle von null
immer ein gültiges Objekt zurückgegeben wird, wodurch spezielle null
-Prüfungen in der weiteren Verarbeitung überflüssig werden.
Ein einfaches Beispiel verdeutlicht, wie sich der Einsatz von Null-Objekten auf die Verarbeitung potenziell fehlender Daten auswirkt, ohne dabei explizite null
-Prüfungen erforderlich zu machen:
final static Integer NO_NUMBER = 0;
public Integer getNumber() {
return NO_NUMBER;
}
// ...
Integer sum = getNumber() + 5;
sum = sum + 10;
// sum == 15
Durch die Rückgabe eines Null-Objekts (NO_NUMBER
in diesem Fall) entfällt die Notwendigkeit, auf null
zu prüfen, da der Code jederzeit mit einem gültigen Wert arbeitet. Dies ist möglich, da Null-Werte durch leere Werte ersetzt werden, die keine Auswirkungen auf die weitere Verarbeitung haben. Auf diese Weise wird das Risiko von NullPointerExceptions effektiv minimiert, ohne die Nachvollziehbarkeit des Codes einzuschränken.
Weiterhin können Null-Objekte in einer Liste leicht aussortiert werden, indem dessen leeren Werte geprüft werden, denn der Zugriff auf dessen Methoden ist immer sicher. Oberfläche-Elemente können auf leere Datenobjekte prüfen und entsprechend reagieren, ohne dass der Benutzer eine Fehlermeldung erhält.
Hier ist die überarbeitete Version des Abschnitts:
Null-Werte sollten nicht als Ersatz für eine ordnungsgemäße Fehlerbehandlung verwendet werden, sondern als spezielle Werte, die eine klar definierte Bedeutung haben. Ein Gegenbeispiel könnte die Verwendung eines NoUserFound
-Objekts als Null-Objekt für Benutzer sein. Dies ist nur in solchen Fällen sinnvoll, in denen die weitere Verarbeitung auch ohne einen Benutzer möglich ist. Andernfalls sollte ein Fehler geworfen werden, um explizit darauf hinzuweisen, dass kein Benutzer gefunden wurde.
Das folgende schlechte Beispiel zeigt die Verwendung eines Null-Objekts in einem Szenario, in dem das Fehlen eines Benutzers eigentlich einen Fehler darstellen sollte:
public User getUser() {
if (hasUser()) {
return new RealUser(...);
}
return User.NO_USER_FOUND;
}
// ...
User user = getUser();
if (user != User.NO_USER_FOUND) {
// Benutzer verarbeiten
}
In diesem Fall wird das NO_USER_FOUND
-Objekt verwendet, um das Fehlen eines Benutzers anzuzeigen. Dies kann jedoch zu Sicherheits-Problemen führen, da das Fehlen eines Benutzers hier ein Ausnahmefall sein sollte, der die weitere Verarbeitung verhindert. Das Codebeispiel erfordert zusätzliche Prüfungen auf das spezielle Objekt, wodurch potenziell Fehler übersehen werden können.
Stattdessen sollte in einem solchen Fall ein Fehler durch eine Ausnahme signalisiert werden, wie im folgenden guten Beispiel:
public User getUser() {
if (hasUser()) {
return new RealUser(...);
}
throw new UserNotFoundException("User not found");
}
Durch das Werfen einer spezifischen Ausnahme (UserNotFoundException
) wird klar signalisiert, dass das Fehlen eines Benutzers ein Fehler ist und die weitere Verarbeitung nicht sinnvoll fortgesetzt werden kann. Dieses Vorgehen unterstützt das Fail-Fast-Prinzip und erleichtert die Fehlersuche, da Fehler frühzeitig erkannt und behandelt werden.