Inspiriert von dem Thema “Funktionieren ist nicht genug” möchte ich heute mit dir über Namen und Strukturen von Funktionen sprechen.
Wenn du dir jetzt sagst, “Es ist doch nur ein Name”, solltest du unbedingt weiter lesen. Ohne gute Strukturen und damit auch Namen wirst du dir immer wieder selbst im Weg stehen.
Wir werden uns in diesem Beitrag primär auf Funktionen konzentrieren, dies lässt sich aber auch im Großen und Ganzen auf andere Bereiche übertragen.
Es ist doch nur ein Name
Du wirst immer wieder damit konfrontiert sein, Dinge zu benennen. Sei es nun Variablen, Funktionen, Klassen oder etwas anderes, Code besteht nun einmal zu großen Teilen aus Namen.
Du hast sicherlich schon von verschiedenen Methoden zur Schreibweise zur Benennung von Dingen in der Entwicklung gehört. Heute soll es aber nicht um SnakeCase VS CamelCase vs PascalCase vs KebabCase usw. gehen, sondern um den Inhalt. In welcher Form du es schlussendlich schreibst, musst du mit deinem Team oder für dein Projekt festlegen.
Die Kunst ist es aber nun, eine Bezeichnung zu finden, die für den aktuellen Kontext sinnvoll ist.
Der Zeitaufwand in sinnvolle Namen ist eine notwendige Investition in deine Entwicklung und die Zukunft deines Projektes.
Wann spielt der Name eine Rolle?
Um diese Frage zu beantworten, musst du für dich herausfinden, warum du gerade eine Funktion schreiben möchtest. Wir wollen in diesem Beitrag primär zwei Gründe für die Erstellung von Funktionen betrachten (global vs lokal):
- Wenn du eine Logik wiederholt verwenden möchtest, schreibst du dazu eine Funktion. Diese kann an verschiedenen Stellen deines Projektes aufgerufen werden und stellt damit die Funktionalität “global” zur Verfügung.
- Wenn eine Funktion eine hohe Komplexität aufweist, kannst du sie in mehrere Funktionen aufteilen, um deinen Code zu strukturieren. Diese zusätzlichen Funktionen zeichnen sich dadurch aus, dass sie in der Regel nur räumlich begrenzt (lokal) verwendet werden.
In beiden Fällen ist es dein Ziel, eine Logik in einem Block zusammenzufassen, den du je nach Bedarf aufrufen kannst. Was denkst du, warum ich diese Unterscheidung gewählt habe?
Du musst Probleme lösen
Der Hauptunterschied liegt in der Art der Verwendung. Und damit ist diese Unterscheidung ein großer Schritt in Richtung der Wart- und Nutzbarkeit deines Codes.
Lass uns das genauer angucken: Als Entwickler ist der Kern unseres Jobs das Lösen von Problemen. Dein Auftraggeber (was auch du selbst sein kannst) hat ein Problem, kommt zu dir und möchte, dass du das Problem für ihn löst. Deine Aufgabe besteht jetzt darin, dieses Problem zu strukturieren, indem du Teillösungen implementierst und diese zu einer Gesamtlösung zusammensetzt.
Damit ist die Aufgabe einer Funktion, ein Problem zu lösen. Deine Aufgabe ist es nun, herauszufinden, was für den Problemsteller wichtig ist und was er weiß oder wissen möchte.
Stell dir vor, du möchtest in einem Teil deines Webprojektes auf dem Server die Berechtigungen für den Zugriff prüfen. Die Benutzer dürfen auf einen Teil der Seiten nur zugreifen, wenn sie entweder die Admin-Rolle oder eine individuelle Berechtigung haben. Öffentliche Seiten dürfen immer betrachtet werden.
Nimm dir einen Moment und überlege für dich, welche Problemstellungen in diesen Anforderungen versteckt sind.
Um das große Problem “Darf der Benutzer die Seite betrachten?” zu lösen, müssen wir folgende kleinere Probleme lösen:
- Ist die Seite öffentlich?
- Hat der Benutzer die Admin-Rolle?
- Hat der Benutzer eine individuelle Berechtigung?
Der Verwender deiner Funktion
In deinem Projekt arbeiten mehrere Teilnehmer. Du bist für das Berechtigungssystem zuständig, während dein Kollege den Webserver implementiert. In diesem Webserver soll eine Middleware eingesetzt werden, die vor dem Zugriff auf die Seite die Berechtigung prüft. Versetz dich jetzt für einen Moment in die Rolle deines Kollegen, der den Webserver umsetzt: Was weiß er? Was muss er über die Berechtigung wissen? Was ist also sein Problem?
Sein Problem ist: Darf Benutzer X die Seite Y sehen? Ob die Seite öffentlich ist, der Benutzer Rollen oder Berechtigungen hat, ist ihm egal und auch nicht seine Aufgabe. Du bist in der Verantwortung, sein Problem zu lösen.
Warum sollst du die Probleme von anderen lösen? Deine Teilaufgabe im Projekt ist es, das Berechtigungssystem zu implementieren. Damit musst du die Prüfung der Berechtigungen so implementieren, dass auch andere diese Funktionalität ohne größeres Wissen nutzen können.
Du könntest deinem Kollegen also die folgende Funktion zur Verfügung stellen:
function userHasAccessToPage(user: User, page: Page): bool {
// Sichtbarkeit prüfen
// Rolle prüfen
// Berechtigung prüfen
// true oder false zurückgeben
}
Mit der Wahl dieser Signatur legst du eine Funktion (nach Definition global) fest, die den Verwender dort abholt, wo er ist. Er hat je ein Objekt vom Typ User und Page und möchte wissen, ob der Zugriff erlaubt ist oder nicht, ohne sich dabei mit Rollen und Berechtigungen zu beschäftigen. Damit löst du sein Problem: “Darf Benutzer X die Seite Y sehen?”.
Nachdem wir jetzt die Probleme deiner Kollegen gelöst haben, müssen wir uns noch um deine kümmern. Dazu schauen wir uns die Methode jetzt genauer an.
Um die Methode userHasAccessToPage zu implementieren, müssen wir wie gesagt mehrere Teilprobleme lösen:
- Ist die Seite öffentlich?
- Hat der Benutzer die Admin-Rolle?
- Hat der Benutzer eine individuelle Berechtigung?
So wie du die Probleme anderer löst, kannst du auch erwarten, dass andere deine Probleme lösen. Für die Frage der Öffentlichkeit und der Rolle des Benutzers darfst du also dein Problem an deine Kollegen auslagern. Diese stellen dir je eine Funktion (global) Page.isPublic und User.hasRole zur Verfügung.
Damit sieht unsere Funktion bisher wie folgt aus:
function userHasAccessToPage(user: User, page: Page): bool {
if(page.isPublic()) return true;
if(user.hasRole('admin')) return true;
// ...
}
Dein Problem beschränkt sich im Eigentlichen also darauf, dass du die individuellen Berechtigungen prüfen musst, wofür du eine Funktion schreiben willst. Wie können wir jetzt festlegen, ob diese Funktion global oder lokal ist?
Stell dir folgende Frage: “Gibt es ein Szenario, in dem deine Funktion in einem anderen Teil des Projektes (außerhalb des Berechtigungssystems) verwendet wird?”
In diesem Kontext würde ich das klar mit Nein beantworten. Wir stellen die "userHasAccessToPage"-Funktion zur Verfügung, damit der Anwender sich nicht um Details kümmern muss. Stellen wir die Funktion zur spezifischen Prüfung zur Verfügung, überlassen wir jemandem die Entscheidung, der unter Umständen keine tiefen Kenntnisse über das Berechtigungssystem hat. Wir wollen diese Funktion also ausschließlich lokal verwenden.
Warum ist das für den Entwurf unserer Funktion relevant?
Wir gestalten unsere Funktionen immer so, dass sie den Anwender dort abholen, wo er ist. Wenn wir die Funktion also nur lokal verwenden, können wir die Voraussetzungen für die Verwendung deutlich höher ansetzen, da wir mehr Wissen unterstellen können.
Um dein Problem zu lösen, könntest du also die folgenden Funktionen entwerfen:
function isRessourceIdMatchingPermission(permission: Permission, ressourceId: string): bool;
function hasUserIndividualPermissionToRessource(user: User, ressource: Ressource): bool {
const resourceId = ressource.getId();
for(const permission of user.getIndividualPermissions()) {
if(isRessourceIdMatchingPermission(permission, resourceId)) {
return permission.isAllowed();
}
}
return false;
}
An diesem Beispiel siehst du, wie die Funktionsnamen sehr viel spezifischer werden. Sie setzen Kenntnisse voraus, die jemand außerhalb deines Kontexts nicht hat, aber sie ermöglichen dir, aus dem Funktionsnamen konkret abzuleiten, welches Problem gelöst wird.
Wir merken uns an dieser Stelle also, dass wir die Benennung der Funktion immer so wählen, dass der Benutzer der Funktion in seinem Kontext abgeholt wird und die Funktionen nach innen hin immer spezifischer werden dürfen.
Fazit
Die Wahl der Namen und der Struktur deiner Funktion hat einen entscheidenden Einfluss auf die Wiederverwendbarkeit. Dabei wählen wir den Namen der Funktion prinzipiell so, dass sie den Verwender der Funktion dort abholt, wo er ist.
Ist es eine globale Funktion, unterstellen wir kein Wissen über spezifische Details; ist die Funktion lokal, unterstellen wir nur Wissen, das im Kontext verfügbar ist.
Ist das alles viel Arbeit? Ja, insbesondere am Anfang. Du wirst aber mit der Zeit ein Gefühl dafür entwickeln und somit deutlich schneller darin werden, die Namen zu wählen. Die wichtigere Frage ist aber: Ist es das wert?
Betrachte den Aufwand als Investition in die Zukunft deines Projektes. Klug gewählte Namen und Strukturen sind die Basis für die Zusammenarbeit im Team und für die langfristige Wartbarkeit deines Projektes.