Flask view functions und Messaging als Programmierparadigma 2
Es handelt sich hier um den Zweiten Post zum Thema. Der Erste Post ist
hierzu finden.
Kurze Zusammenfassung
Ich habe begonnen ein übliches Codebeispiel für flask_login einem Refactoring zu
unterziehen. An dem Ursprünglichen Beispiel konnte man gut das Problem der häufig
zu findenden Implementierungen von Flask View-Functions sehen.
In dem Beispiel wurden wie bei vielen in freier wildbahn auch produktiv zu
findenden Codes folgende Prinzipien nicht beachtet:
Nach meiner Erfahrung haben die Beispiele, die keine clean-code Prinzipien
verfolgen einen negativen Einfluss auf die Entwicklung von Software.
Die beispiele werden meistens für den eigenen Code angepasst oder im besten Fall
wird sich nur an ihnen orientiert. doch selbst dann kann man sich dem negativen
Einfluss eigentlich nur durch komplettes neudenken in form eines Refactorings
entziehen.
Mein Ziel ist es für mich und interessierte positive Beispiele zu schaffen.
Orientierung für mein Refactoring geben mir:
Ein Router muss her
Ich habe als mein Hauptproblem identifiziert, dass die View-Function, die den
POST /login
behandelt zwei verschiedene Antworten zurückliefert und es definitiv
an einer Stelle einer verzweigung bedarf.
Da in der View-Function allerdings andere Funktionen integriert werden, sollte
dies dem IOSP folgend nicht in der View-function selbst sein.
Ich bin bereits auf das
Pipes und Filter Patternaufmerksam geworden. Das ermöglicht mir aber nicht eine verzweigung zu bauen.
Eine Kleine modifikation eines Filters aber schon.
Der
RouterHier noch einmal kurz der letzte Stand des Beispielcodes:
- @app.route("/login", methods=["POST"])
- def login():
- username = request.form['username']
- password = request.form['password']
- if password == username + "_secret":
- id = username.split('user')[1]
- user = User(id)
- login_user(user)
- return redirect(request.args.get("next"))
- else:
- return abort(401)
-
- @app.route("/login", methods=["GET"])
- def get_login_form
- return Response('''
- <form action="" method="post">
- <p><input type=text name=username>
- <p><input type=password name=password>
- <p><input type=submit value=Login>
- </form>
- ''')
Da ich von der Idee befangen war mir direkt eine Message-Pipeline zu bauen habe
ich mir zunächst eine Login-Message gebaut:
- class LoginMessage(object):
-
- def __init__(self, username, password):
- self.username = username
- self.password = password
- self.authenticated = False
Als Idee dahinter stand folgende Pipeline:
--login_message--> authenticate --login_message--> create_response --response->
Die Funktions
authenticate
sollte von einem Authenticator Object bereitgestellt
werden. Damit der Authenticator aber nichts von anderen Klassen wissen muss,
außer wie er mit die Login-Message liest muss diesem ein handler für einen
erfolgreichen login und ein handler für den Fehlerfall übergeben werdne können.
Ich habe das so implementiert:
- class Authenticator(object):
-
- def __init__(self, on_auth, on_no_auth):
- self.on_auth =on_auth
- self.on_no_auth = on_no_auth
-
- def authenticate(self, login_message):
- if login_message.password == login_message.username + "_secret":
- return self.on_auth(login_message)
- return self.on_no_auth(login_message)
Damit ist es die Authentifizierungslokgic gekapselt und kann von der View-Function
integriert werden.
- authenticator = Authenticator(
- on_auth=lambda msg: do_user_login(msg),
- on_no_auth=lambda msg: abort_user_login(msg)
- )
Lambda in python sind einfach anonyme funktionen. Sie werden hier benötigt damit
der Aufruf von
do_user_login(msg)
erst erfolgt wenn die Authentifizierung
erfolgreich war und nicht bereits wenn das Authenticator Object erzeugt wird.
Mehr zum Thema python lambda gibt es auf Real Python.Dabei wird schnell klar, das wir die information ob der Benutzer angemeldet ist
eigentlich gar nicht in unserer Login-Message brauchen.
Und wenn es sich bei LoginMessage um einen dummen Datenspeicher handelt
wozu sollte man dann in Python gleich eine ganze Klasse schreiben.
Im Grunde kann unsere View Function mit dem neuen Wissen doch ganz
knackig aussehen:
- @app.route("/login", methods=["POST"])
- def login():
- login_message = dict(
- username=request.form['username'],
- password=request.form['password']
- )
- authenticator = Authenticator(
- on_auth=do_user_login,
- on_no_auth=lambda msg: abort(401)
- )
- return authenticator.authenticate(login_message)
In Python kann man auch referenzen auf Funktionen übergeben. Damit braucht es
für do_user_login kein lambda, sofern diese Funktion ein Argument für die
Login-Message akzeptiert. Ddas Lambda für
abort(401)
muss bleiben, denn
hier wird die Login-Messga gewissermaßen ins leere Laufen gelassen.
Ich halte es für eine äußerst praktische idee in Python für die übertragung von
Nachrichten ein Dictionary kompatibles Objekt zu verwenden.
Wenn man aus irgendwelchen Gründen mehr als ein reines Dictionary braucht kann
mann ja die entsprechenden "magischen" Methoden implementieren.
Für meine zwecke reicht mir aber ein Dictionary.
Dann müssen aber die Nachrichten Filter oder mein Authenticator Router auch
mit Dictionaries arbeiten können.
Das gute daran ist, dass die Nachricht damit sehr leicht erweiterbar ist ohne
das irgendwo eine andere Klasse als Konsumenten oder Produzenten des betreffenden
Keys angepasst werden muss.
Der Neue Authenticator schaut dann so aus:
- class Authenticator(object):
-
- def __init__(self, on_auth, on_no_auth):
- self.on_auth =on_auth
- self.on_no_auth = on_no_auth
-
- def authenticate(self, login_message):
- if login_message['password'] == login_message['username'] + "_secret":
- return self.on_auth(login_message)
- return self.on_no_auth(login_message)
Es Fehlt nur noch die
do_user_login
Funktion. Diese ist ebenfalls eine reine
Integration der
User
Klasse, der
login_user
Funktion von
flask_login
und
von
redirect
.
- def do_user_login(login_message):
- id = login_message['username'].split('user')[1]
- user = User(id)
- login_user(user)
- return redirect(request.args.get("next"))
Auch schön, es ist dem
do_user_login
egal wie oder ob der Benutzert
autentifiziert wurde. Es besteht auch keine abhängigkeit zur eigentlichen
Anfrage. Diese Funktion kümmert sich darum eine Antwort für flask_login
und den Seitenaufrufer zu erzeugen.
Das vollständige Codebeispiel ist als
Gitlab Snippetzum ausprobieren verfügbar.
EDIT
Mir ist aufgefallen dass ich vergessen hatte dass man in Python auch Problemlos
referenzen auf Funktionen übergeben kann. Lambdas bleiben sinnvoll z.B. wenn
eine einfache konvertierung des Argumentes erfolgen soll oder die auf zu rufende
Funktion kein (oder kein Login-Message) Argument unterstützt / erhalten soll.