

Discover more from INUVEON
Helpers
In fast jedem Software Repository gibt es sie. Statische Klassen mit dem Namen "Helpers" oder "Utils." Diese Klassen deuten auf ein eher suboptimales Design der Software hin.
In diesem Beitrag möchte ich klären, warum Klassen mit einer Ansammlung statischer Hilfsmethoden grundsätzlich keine besonders gute Idee sind. Meistens finden wir diese Methoden in Sammelklassen mit den Namen Helpers oder Utils, manchmal sind sie auch spezieller und heißen beispielsweise UserHelper.
Es gibt mehrere Gründe, warum ich diese Art des Software-Designs für ungünstig oder gar für einen Designfehler halte. Einer davon ist, dass wir Wissen über den gesamten Code verstreuen und dieser dadurch in den meisten Fällen schwerer verständlich wird. Oftmals enthalten diese Klassen oder Methoden spezifisches Domänenwissen, welches besser innerhalb einer Entität oder einem Domain-Service platziert sein sollte. Bei genauer Betrachtung gibt es im Regelfall meist keine Existenzberechtigung für derartige Klassen. Meine Betrachtungen sind unabhängig von Programmiersprachen, beziehen sich aber durchaus auf objektorientiertes Design. Nach meinem Verständnis sind ziemlich alle Dinge, die wir mithilfe von Software abbilden, Objekte mit Verhalten und Struktur.
Diese Thematik ist aus meiner Sicht kein so neues Thema, dennoch ist im Zusammenhang mit der inflationären Verwendung von TypeScript wieder aktuell. Ich bin ein großer Freund von TypeScript, wenn man dadurch einen Mehrwert im Code erzeugt, aber nicht, wenn das Konzept dahinter nicht verstanden wurde und TypeScript nur genutzt wird, weil es gerade alle machen.
Wie der Name schon sagt, geht es darum zu typisieren und Typen zu verwenden. Das bringt Typsicherheit und führt natürlich auch zu einer objektorientierten Struktur des Codes. Ich meine damit, dass jedes Ding im Code in einer Objektstruktur mit Verhalten definiert werden kann und damit die Prinzipien der Entkoppelung und Information Hiding hervorragend realisiert werden.
Ein einfaches Beispiel: Wir erhalten einen API Request und müssen prüfen, ob der gelieferte String das Format einer E-Mail-Adresse hat. Das typische Vorgehen dafür in vielen JavaScript/ TypeScript sieht wie folgt aus.
module.exports.isEmail = (email) => {
return /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/.test(email);
}
Vielleicht hat sich in TypeScript schon jemand die Mühe gemacht und einen Klasse mit statischen Methoden erstellt, die überall verwendet werden können.
export class Helper{
public static validateEmail(value: string): boolean{
return /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/.test(value);
}
public static validateSomethingElse(value: int){
...
}
}
Nutzer dieser Methoden rufen in ihrem Code die Validierungsmethode dann wie folgt auf.
const isValid: boolean = Helper.validateEmail(email);
Was passiert dadurch? Wir nutzen keine Typen, auch wenn uns die Konventionen von TypeScript Rückgabetypen zu definieren. Das ist nicht genug, denn wir legen Wissen (ja, es ist ein simples Beispiel) irgendwo im Code ab.
Ein besserer Weg besteht darin, einen eigenen Typ “EmailAddress” zu definieren und stattdessen in der übergeordneten Entität zu verwenden.
Zunächst benötigen wir ein Interface.
export interface IEmailAddress {
value: string
valid: boolean
};
Die konkrete Implementierung sieht dann wie folgt aus.
export class EmailAddress implements IEmailAddress{
readonly valid: boolean;
readonly value: string;
private constructor(value: string) {
this.valid = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/.test(value);
this.value = value;
}
public static create(value: string): IEmailAddress {
const address = new EmailAddress(value);
if(!address.valid){
throw new Error();
}
return address;
}
}
Wir sind nun in der Lage bzw. werden gezwungen, eine typisierte, konkrete Instanz einer E-Mail-Adresse zu erzeugen. Die Validierung ist implizit und kann nicht umgangen werden. Es handelt sich um ein Value Object, da die Attribute nicht von außen bzw. nach Erzeugung änderbar sind. Für eine neue E-Mail-Adresse muss ich ein neues Objekt instanziieren.
Der Konstruktor ist auch private, sodass die statische Builder-Methode Create genutzt werden muss. Grund dafür ist, dass ich ungern im Konstruktor eine Exception werfen möchte. Hier ist es Geschmackssache, ob die Builder-Methode eine Exception werfen soll oder nicht. Sie könnte ebenso gut die E-Mail-Adresse nur als invalide markieren. Das konkrete Design hängt am Ende von den Gegebenheiten herum ab.
Im nächsten Beispiel habe ich die Exception entfernt, damit der Aufrufer selbst entscheiden kann, was er mit einer invaliden Adresse tut. Den Konstruktor habe ich dafür public gemacht (Builder Methode würde aber auch funktionieren).
Das sieht nun wie folgt aus.
export class EmailAddress implements IEmailAddress{
readonly valid: boolean;
readonly value: string;
public constructor(value: string) {
this.valid = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/.test(value);
this.value = value;
}
}
Der Aufrufer kann in anderen Objekten die E-Mail-Adresse nun als Typ verwenden anstelle die E-Mail-Adresse als String zu verwenden, in die im Grunde jeder alles hineinschreiben könnten und Validierungen nur von außen möglich sind.
Die Verwendung des Typs sieht nun so aus.
const emailAddress = new EmailAddress('test@your-domain.com')
if (!emailAddress.valid){
// do something...
}
In einer übergeordneten Klasse, zum Beispiel User ändern wir den Typ des Attributes Email von String auf unseren neuen Typ EmailAddress. Im Grunde können wir das an vielen Stellen tun, um unseren Code (typ-)sicherer und übersichtlicher zu machen.
Das Hauptanliegen dieses Artikels ist also die Kapselung von Methodenlogik in eine Klasse mit den Daten und dem impliziten Wissen über Verhalten. Dies führt zu besser lesbaren und wartbaren Code. Wenn du das nächste Mal dabei bist einen statischen Helfer zu schreiben, versuche einfach umzudenken und das beschrieben Muster zu nutzen.