Anmelde- und Abmeldeansichten. Model-Update-View-Muster und abhängige Typen Logout-HTML-Ansichtssatz

Django verfügt über viele integrierte Ressourcen für die häufigsten Anwendungsfälle einer Webanwendung. Die Registrierungs-App ist ein sehr gutes Beispiel und das Gute daran ist, dass die Funktionen sofort genutzt werden können.

Mit der Django-Registrierungs-App können Sie die folgenden Funktionen nutzen:

  • Anmeldung
  • Ausloggen
  • Melden Sie sich an
  • Passwort zurücksetzen

In diesem Tutorial konzentrieren wir uns auf die Anmelde- und Abmeldefunktionen. Informationen zur Anmeldung und zum Zurücksetzen des Passworts finden Sie in den folgenden Tutorials:

Erste Schritte

Bevor wir beginnen, stellen Sie sicher, dass Sie django.contrib.auth in Ihren INSTALLED_APPS haben und die Authentifizierungs-Middleware in den MIDDLEWARE_CLASSES-Einstellungen richtig konfiguriert ist.

Beide sind bereits konfiguriert, wenn Sie mit dem Befehl startproject ein neues Django-Projekt starten. Wenn Sie also die anfänglichen Konfigurationen nicht entfernt haben, sollte alles eingerichtet sein.

Falls Sie ein neues Projekt starten, nur um diesem Tutorial zu folgen, erstellen Sie über die Befehlszeile einen Benutzer, damit wir die Anmelde- und Abmeldeseiten testen können.

$ python manage.py createsuperuser

Am Ende dieses Artikels werde ich den Quellcode des Beispiels mit der minimalen Konfiguration bereitstellen.

Konfigurieren Sie die URL-Routen

Importieren Sie zunächst das Modul django.contrib.auth.views und fügen Sie eine URL-Route für die Anmelde- und Abmeldeansichten hinzu:

aus django.conf.urls URL aus django.contrib importieren Administrator aus django.contrib.auth importieren Ansichten als auth_views importieren urlpatterns = [ url (r"^login/$" , auth_views . login , name = "login" ), url ( r"^logout/$" , auth_views . logout , name = "logout" ), url (r"^admin/" , admin . site . urls ), ]

Erstellen Sie eine Login-Vorlage

Standardmäßig versucht die Ansicht „django.contrib.auth.views.login“, die Vorlage „registration/login.html“ darzustellen. Die Grundkonfiguration würde also darin bestehen, einen Ordner mit dem Namen „Registrierung“ zu erstellen und darin eine login.html-Vorlage abzulegen.

Befolgen Sie eine minimale Anmeldevorlage:

(% erweitert „base.html“ %) (% Blocktitel %)Login(% endblock %) (% Blockinhalt %)

Anmeldung

(% csrf_token %) (( form.as_p ))
(%endblock%)

Dieses einfache Beispiel validiert bereits Benutzername und Passwort und authentifiziert den Benutzer korrekt.

Anpassen der Anmeldeansicht

Es gibt einige Parameter, die Sie an die Anmeldeansicht übergeben können, um sie an Ihr Projekt anzupassen. Wenn Sie Ihre Anmeldevorlage beispielsweise an einem anderen Ort als „registration/login.html“ speichern möchten, können Sie den Namen der Vorlage als Parameter übergeben:

URL (r"^login/$" , auth_views . login , ("template_name" : "core/login.html" ), name = "login" ),

Sie können auch ein benutzerdefiniertes Authentifizierungsformular mithilfe des Parameters „authentication_form“ übergeben, sofern Sie ein benutzerdefiniertes Benutzermodell implementiert haben.

Jetzt wird eine sehr wichtige Konfiguration in der Datei „settings.py“ vorgenommen, bei der es sich um die URL handelt, die Django den Benutzer nach einer erfolgreichen Authentifizierung umleitet.

Fügen Sie in der Datei „settings.py“ Folgendes hinzu:

LOGIN_REDIRECT_URL = "Zuhause"

Der Wert kann eine fest codierte URL oder ein URL-Name sein. Der Standardwert für LOGIN_REDIRECT_URL ist /accounts/profile/ .

Es ist auch wichtig zu beachten, dass Django versuchen wird, den Benutzer zum nächsten GET-Parameter umzuleiten.

Abmeldeansicht einrichten

Nach dem Zugriff auf die Ansicht „django.contrib.auth.views.logout“ rendert Django die Vorlage „registration/logged_out.html“. Auf ähnliche Weise wie in der Anmeldeansicht können Sie eine andere Vorlage wie folgt übergeben:

URL (r"^logout/$" , auth_views . logout , ("template_name" : "logged_out.html" ), name = "logout" ),

Normalerweise verwende ich lieber den Parameter next_page und leite entweder zur Startseite meines Projekts oder zur Anmeldeseite weiter, wenn es sinnvoll ist.

Dieses Beispiel zeigt, wie Sie sich mit der standardmäßigen Spring-Sicherheitskonfiguration automatisch abmelden.

Zum Abmelden müssen wir lediglich mit einer POST-Anfrage auf die URL „/logout“ zugreifen.

Im POST-/Logout-Formular müssen wir auch das CSRF-Token einschließen, das einen Schutz vor CSRF-Angriffen darstellt.

Sehen wir uns das Beispiel an, wie das geht.

Java-Konfigurationsklasse

@Configuration @EnableWebSecurity @EnableWebMvc @ComponentScan öffentliche Klasse AppConfig erweitert WebSecurityConfigurerAdapter ( protected void configure(HttpSecurity http) throws Exception ( http.authorizeRequests() .anyRequest().authenticated() .and() .formLogin(); ) @Override public void configure(AuthenticationManagerBuilder builder) throws Exception ( builder.inMemoryAuthentication() .withUser("joe") .password("123") .roles("ADMIN"); ) @Bean public ViewResolver viewResolver() ( InternalResourceViewResolver viewResolver = new InternalResourceViewResolver (); viewResolver.setPrefix("/WEB-INF/views/"); viewResolver.setSuffix(".jsp");

Beachten Sie, dass wir in der obigen Konfiguration auch configure(HttpSecurity http) überschreiben, um die Standard-Basisauthentifizierung wegzulassen (siehe die ursprüngliche Methode im WebSecurityConfigurerAdapter-Quellcode) und die formularbasierte Authentifizierung zu verwenden. Wir tun dies, weil Browser die Basiaggressiv zwischenspeichern (nach der ersten erfolgreichen Anmeldung) und es keine Möglichkeit gibt, den Benutzer in der aktuellen Sitzung abzumelden. In den meisten Beispielen verwenden wir keinen Standardauthentifizierungsmechanismus.

Ein Controller

@Controller öffentliche Klasse BeispielController ( @RequestMapping("/") öffentlicher String handleRequest2(ModelMap-Karte) ( map.addAttribute("time", LocalDateTime.now().toString()); return "my-page"; ) )

Die JSP-Seite

src/main/webapp/WEB-INF/views/my-page.jsp

Beispiel für Spring-Sicherheit

Zeit: $(Zeit)

Um Beispiele auszuprobieren, führen Sie eingebettetes Tomcat aus (konfiguriert in pom.xml des Beispielprojekts unten):

Mvn tomcat7:run-war

Ausgabe

Beim ersten Zugriff auf den URI „/“ wird zu „/login“ umgeleitet:

Nach der Übermittlung von Benutzername und Passwort beim Einrichten unserer AppConfig-Klasse:

Klicken Sie auf die Schaltfläche „Abmelden“:


Beispielprojekt

Abhängigkeiten und verwendete Technologien:

  • spring-security-web 4.2.3.VERÖFFENTLICHUNG: spring-security-web.
  • spring-security-config 4.2.3.RELEASE: spring-security-config.
  • spring-webmvc 4.3.9.RELEASE: Spring Web MVC.
  • javax.servlet-api 3.1.0 Java-Servlet-API
  • JDK 1.8
  • Maven 3.3.9

Bearbeiten Sie die Datei urls.py Anwendungen Konto:

aus django.conf.urls URL importieren von . Ansichten importieren urlpatterns = [ # vorherige Anmeldeansicht # url(r"^login/$", views.user_login, name="login"),# Anmelde-/Abmelde-URLs url(r"^login/$" , „django.contrib.auth.views.login“, name="login" ), url(r"^logout/$" , „django.contrib.auth.views.logout“, name="logout" ), url(r"^logout-then-login/$" , „django.contrib.auth.views.logout_then_login“, name="logout_then_login" ), ]

Wir haben die URL-Vorlage für die Ansicht auskommentiert Benutzer-Anmeldung, zuvor erstellt, um die Ansicht zu verwenden Anmeldung Django.

Erstellen Sie ein neues Verzeichnis im Anwendungsvorlagenverzeichnis Konto und benennen Sie es Anmeldung. Erstellen Sie eine neue Datei in einem neuen Verzeichnis und benennen Sie sie login.html

(% erweitert „base.html“ %) (% Blocktitel %)Anmelden(% endblock %) (% Blockinhalt %)

Anmeldung

(% wenn form.errors %)

Ihr Benutzername und Ihr Passwort stimmen nicht überein. Bitte versuchen Sie es erneut.

(%anders%)

Bitte nutzen Sie zum Login das folgende Formular:

(%endif%) (%endblock%)

Diese Anmeldevorlage ist der zuvor erstellten sehr ähnlich. Django verwendet Authentifizierungsform, gelegen in django.contrib.auth.forms. Dieses Formular versucht, den Benutzer zu authentifizieren und generiert einen Validierungsfehler, wenn der Benutzername falsch war. In diesem Fall können wir mit dem Befehl (% if form.errors %) nach Fehlern suchen. Bitte beachten Sie, dass wir ein verstecktes Element hinzugefügt haben um den Wert einer Variablen mit dem Namen zu senden nächste.

Parameter nächste muss eine URL sein. Wenn dieser Parameter angegeben ist, wird der Benutzer nach der Anmeldung zur angegebenen URL umgeleitet.

Erstellen Sie nun eine Vorlage login_out.html im Vorlagenverzeichnis Anmeldung und fügen Sie den folgenden Code ein:

(% erweitert „base.html“ %) (% Blocktitel %)Abgemeldet(% endblock %) (% Blockinhalt %)

Abgemeldet

Du hast dich erfolgreich abgemeldet. Sie können sich erneut anmelden.

(%endblock%)

Dies ist die Vorlage, die angezeigt wird, nachdem sich der Benutzer angemeldet hat.

Nach dem Hinzufügen der URL-Vorlagen und Vorlagen für die Eingabe- und Ausgabeansichten ist die Site für Anmeldungen mithilfe der Authentifizierungsansichten von Django bereit.

Bitte beachten Sie, dass die Präsentation logout_then_login, in unserem enthalten URLconf, benötigt keine Vorlage, da es weiterleitet Anmeldeansicht.

Erstellen wir nun eine neue Ansicht, um dem Benutzer ein Dashboard anzuzeigen, damit wir wissen, wann sich der Benutzer bei seinem Konto anmeldet. Öffne die Datei Ansichten.py Anwendungen Konto und fügen Sie den folgenden Code hinzu:

aus django.contrib.auth.decorators import login_required @login_required def Dashboard(Anfrage): return render(request, „account/dashboard.html“, („section“: „dashboard“))

Wir fügen unserer Ansicht einen Dekorateur hinzu Anmeldung erforderlich Authentifizierungsrahmen. Dekorateur Anmeldung erforderlich prüft, ob der aktuelle Benutzer authentifiziert ist. Wenn der Benutzer authentifiziert ist, wird die Übermittlung ausgeführt. Wenn der Benutzer nicht authentifiziert ist, wird er zur Anmeldeseite weitergeleitet.

Wir haben auch eine Variable definiert Abschnitt. Wir werden diese Variable verwenden, um zu verfolgen, welchen Abschnitt der Website der Benutzer ansieht.

Jetzt müssen Sie eine Vorlage für die Dashboard-Ansicht erstellen. Erstellen Sie eine neue Datei in „Vorlagen/Konto“. Vorlagen/Konto/ und benennen Sie es Dashboard.html :

(% erweitert „base.html“ %) (% Blocktitel %)Dashboard(% endblock %) (% Blockinhalt %)

Armaturenbrett

Willkommen in Ihrem Dashboard.

(%endblock%)

Fügen Sie dann das folgende URL-Muster für diese Änderungsdatei hinzu urls.py Anwendungen Konto:

Urlpatterns = [ # ... url(r"^$" , views.dashboard, name="dashboard" ), ]

Bearbeiten Sie nun die Datei Settings.py:

aus django.core.urlresolvers import reverse_lazy LOGIN_REDIRECT_URL = reverse_lazy("dashboard" ) LOGIN_URL = reverse_lazy("login" ) LOGOUT_URL = reverse_lazy("logout" )
  • LOGIN_REDIRECT_URL: Gibt an, zu welcher URL der Benutzer nach der Anmeldung weitergeleitet werden soll.
  • LOGIN_URL: URL, um den Benutzer zur Anmeldung umzuleiten (z. B. mithilfe eines Dekorators). Anmeldung erforderlich)
  • LOGOUT_URL: URL, um den Benutzer zum Beenden umzuleiten

Jetzt fügen wir Anmelde- und Abmeldelinks zu unserer Basisvorlage hinzu.

Hierzu muss ermittelt werden, ob der aktuelle Benutzer angemeldet ist oder nicht, um einen dem aktuellen Benutzerstatus entsprechenden Link anzuzeigen. Der aktuelle Benutzer wird in angegeben HttpRequest Objekt der Authentifizierungszwischenklasse. Es kann über darauf zugegriffen werden request.user. Die Anfrage findet ein Benutzerobjekt, auch wenn der Benutzer nicht authentifiziert ist. Nicht authentifizierter Benutzer, in der Anfrage als Instanz angegeben Anonymer Benutzer. Der beste Weg, den Authentifizierungsstatus des aktuellen Benutzers zu überprüfen, ist ein Anruf request.user.is_authenticated()

Bearbeiten Sie die Vorlage base.html

mit ID-Header:

Wie Sie sehen, wird das Site-Menü nur für authentifizierte Benutzer angezeigt. Wir überprüfen auch den aktuellen Abschnitt, um das ausgewählte Klassenattribut zum entsprechenden Element hinzuzufügen

  • um den aktuellen Abschnitt im Menü mithilfe von CSS hervorzuheben. Außerdem werden der Benutzername und ein Link zum Abmelden angezeigt, wenn der Benutzer authentifiziert ist, bzw. ein Link zum Anmelden.

    Öffnen Sie http://127.0.0.1:8000/account/login/ in Ihrem Browser. Sie sollten eine Anmeldeseite sehen. Geben Sie einen gültigen Benutzernamen und ein Passwort ein. Sie werden Folgendes sehen:

    Sie können sehen, dass der Abschnitt „Mein Dashboard“ mit CSS hervorgehoben ist, da er über eine Klasse verfügt ausgewählt. Da der Benutzer authentifiziert wurde, wird der Benutzername auf der rechten Seite der Kopfzeile angezeigt. Klicken Sie auf den Link Ausloggen. Sie sehen die folgende Seite:

    Auf dieser Seite können Sie erkennen, dass der Benutzer abgemeldet ist und daher das Website-Menü nicht mehr angezeigt wird. Der Link auf der rechten Seite der Kopfzeile wird jetzt angezeigt Anmeldung.

    Wenn Sie die Abmeldeseite der Django-Administratorseite und nicht Ihre eigene Abmeldeseite sehen, überprüfen Sie Ihre INSTALLED_APPS-Einstellungen und stellen Sie sicher, dass dies der Fall ist django.contrib.admin ist danach Konto. Beide Vorlagen befinden sich im selben relativen Pfad und der Django-Vorlagenlader verwendet die erste, die er findet.

    Hauptsächlich für die Entwicklung von Benutzeroberflächen. Um es zu verwenden, müssen Sie einen Modelltyp erstellen, der den vollständigen Status des Programms darstellt, einen Nachrichtentyp, der externe Umgebungsereignisse beschreibt, auf die das Programm mit einer Statusänderung reagieren muss, und eine Aktualisierungsfunktion, die einen neuen Status des Programms erstellt aus dem alten Status und der Nachricht sowie einer Ansichtsfunktion, die basierend auf dem Status des Programms die erforderlichen Auswirkungen auf die externe Umgebung berechnet, die Ereignisse vom Typ Nachricht generieren. Das Muster ist sehr praktisch, hat aber einen kleinen Nachteil: Es lässt sich nicht beschreiben, welche Ereignisse für bestimmte Programmzustände sinnvoll sind.

    Ein ähnliches Problem entsteht (und wird gelöst), wenn das State OO-Muster verwendet wird.

    Die Elm-Sprache ist einfach, aber sehr streng – sie prüft, ob die Updater-Funktion irgendwie alle möglichen Kombinationen von Modellstatus- und Nachrichtenereignissen verarbeitet. Daher müssen Sie zusätzlichen, wenn auch trivialen Code schreiben, der das Modell normalerweise unverändert lässt. Ich möchte zeigen, wie dies in komplexeren Sprachen vermieden werden kann – Idris, Scala, C++ und Haskell.

    Der gesamte hier gezeigte Code steht zum Experimentieren auf GitHub zur Verfügung. Schauen wir uns die interessantesten Orte an.


    Die msg-Funktion ist ungewöhnlich – sie gibt einen Typ zurück, keinen Wert. Während der Ausführung ist nichts über die Werttypen bekannt – der Compiler löscht alle unnötigen Informationen. Das heißt, eine solche Funktion kann nur in der Kompilierungsphase aufgerufen werden.

    MUV ist ein Konstruktor. Es akzeptiert folgende Parameter: model – der Anfangszustand des Programms, updater – eine Funktion zum Aktualisieren des Zustands bei einem externen Ereignis und view – eine Funktion zum Erstellen einer externen Ansicht. Beachten Sie, dass der Typ der Aktualisierungs- und Ansichtsfunktionen vom Wert des Modells abhängt (unter Verwendung der msg-Funktion aus den Typparametern).

    Sehen wir uns nun an, wie diese Anwendung gestartet wird

    MuvRun: (Application modelType msgType IO) -> IO a muvRun (MUV model updater view) = do msg<- view model muvRun (MUV (updater model msg) updater view)
    Wir haben eine Eingabe-/Ausgabeoperation als externe Darstellung (Ansicht) gewählt (in Idris, wie auch in Haskell, sind Eingabe-/Ausgabeoperationen erstklassige Werte; damit sie ausgeführt werden können, müssen zusätzliche Aktionen durchgeführt werden, die normalerweise eine solche Operation zurückgeben aus der Hauptfunktion).

    Kurz über IO

    Bei der Ausführung einer Operation vom Typ (IO a) treten einige Auswirkungen auf die Außenwelt auf, möglicherweise ist sie leer, und ein Wert vom Typ a wird an das Programm zurückgegeben, aber die Funktionen der Standardbibliothek sind so konzipiert, dass dies möglich ist nur verarbeitet werden, indem ein neuer Wert vom Typ IO b generiert wird. Auf diese Weise werden reine Funktionen von Funktionen mit Nebenwirkungen getrennt. Das ist für viele Programmierer ungewöhnlich, hilft aber dabei, zuverlässigeren Code zu schreiben.


    Da die Funktion muvRun I/O generiert, sollte sie IO zurückgeben. Da sie jedoch nie abgeschlossen wird, kann der Operationstyp ein beliebiger Typ sein – IO a.

    Beschreiben wir nun die Arten von Entitäten, mit denen wir arbeiten werden

    Datenmodell = Abgemeldet | Angemeldete String-Daten MsgOuted = Login-String-Daten MsgIned = Logout | Gruß insgesamt msgType: Modell -> Typ msgType Logouted = MsgOuted msgType (Logined _) = MsgIned
    Hier beschreiben wir einen Modelltyp, der das Vorhandensein von zwei Schnittstellenzuständen widerspiegelt: Der Benutzer ist nicht angemeldet, und der Benutzer mit einem Namen vom Typ String ist angemeldet.

    Als nächstes beschreiben wir zwei verschiedene Arten von Nachrichten, die für verschiedene Varianten des Modells relevant sind – wenn wir abgemeldet sind, können wir uns nur unter einem bestimmten Namen anmelden, und wenn wir bereits angemeldet sind, können wir uns entweder abmelden oder Hallo sagen. Idris ist eine stark typisierte Sprache, die die Möglichkeit einer Vermischung verschiedener Typen nicht zulässt.

    Und schließlich eine Funktion, die die Übereinstimmung des Modellwerts mit dem Nachrichtentyp festlegt.

    Die Funktion wird als vollständig deklariert, d. h. sie sollte nicht abstürzen oder einfrieren. Der Compiler wird versuchen, dies zu überwachen. msgType wird zur Kompilierungszeit aufgerufen und seine Gesamtheit bedeutet, dass die Kompilierung aufgrund unseres Fehlers nicht einfriert, obwohl nicht garantiert werden kann, dass die Ausführung dieser Funktion die Systemressourcen erschöpft.
    Es ist auch garantiert, dass „rm -rf /“ nicht ausgeführt wird, da in seiner Signatur kein IO enthalten ist.

    Beschreiben wir den Updater:

    Total updater: (m:Model) -> (msgType m) -> Model updater Logouted (Login name) = Angemeldeter Name updater (Login name) Logout = Logouted updater (Login name) Greet = Logined name
    Ich denke, die Logik dieser Funktion ist klar. Ich möchte noch einmal auf die Gesamtheit hinweisen – das bedeutet, dass der Idris-Compiler überprüft, ob wir alle vom Typsystem zugelassenen Alternativen berücksichtigt haben. Elm führt diese Prüfung ebenfalls durch, kann jedoch nicht wissen, dass wir uns nicht abmelden können, wenn wir noch nicht angemeldet sind, und erfordert eine explizite Verarbeitung der Bedingung

    Updater abgemeldet Abmelden = ???
    Idris wird bei einer unnötigen Prüfung Typkonflikte feststellen.

    Kommen wir nun zur Ansicht – wie in der Benutzeroberfläche üblich, wird dies der schwierigste Teil des Codes sein.

    Total loginPage: IO MsgOuted loginPage = do putStr "Login: " map Login getLine total genMsg: String -> MsgIned genMsg "" = Logout genMsg _ = Greet total workPage: String -> IO MsgIned workPage name = do putStr ("Hallo, " ++ Name ++ „\n“) putStr „Geben Sie eine leere Zeichenfolge für die Abmeldung oder eine nicht leere Zeichenfolge für die Begrüßung ein\n“ map genMsg getLine total view: (m: Model) -> IO (msgType m) view Logouted = loginPage view (Angemeldeter Name ) = Arbeitsseitenname
    Die Ansicht muss eine E/A-Operation erstellen, die Nachrichten zurückgibt, deren Typ wiederum vom Wert des Modells abhängt. Wir haben zwei Möglichkeiten: loginPage, das eine „Login:“-Nachricht druckt, eine Zeichenfolge von der Tastatur liest und sie in eine Login-Nachricht einschließt, und workPage mit einem Benutzernamen-Parameter, der eine Begrüßung druckt und verschiedene Nachrichten (jedoch vom gleichen Typ) zurückgibt - MsgIned), abhängig davon, ob der Benutzer eine leere oder eine nicht leere Zeichenfolge eingibt. view gibt abhängig vom Wert des Modells eine dieser Operationen zurück und der Compiler überprüft deren Typ, auch wenn er unterschiedlich ist.

    Jetzt können wir unsere Anwendung erstellen und ausführen

    App: Anwendungsmodell Main.msgType IO app = MUV Abgemeldete Updater-Ansicht main: IO () main = muvRun app
    Hier sollte ein subtiler Punkt beachtet werden: Die Funktion muvRun kehrt zurück IO a, wobei a nicht angegeben wurde und der Wert main vom Typ ist IO(), Wo () ist der Name eines Typs, der üblicherweise aufgerufen wird Einheit, das einen einzelnen Wert hat, auch als leeres Tupel geschrieben () . Aber der Compiler kann damit problemlos umgehen. Ersetzen Sie stattdessen a().

    Scala und pfadabhängige Typen

    Scala bietet keine vollständige Unterstützung für abhängige Typen, aber es gibt Typen, die von der Instanz des Objekts abhängen, über das darauf verwiesen wird (pfadabhängige Typen). In der Theorie der abhängigen Typen können sie als Variante des Sigma-Typs beschrieben werden. Pfadabhängige Typen ermöglichen es, die Addition von Vektoren aus verschiedenen Vektorräumen zu verhindern oder zu beschreiben, wer wen küssen darf. Aber wir werden sie für einfachere Aufgaben verwenden.

    Versiegelte abstrakte Klasse MsgLogouted-Fallklasse Login(name: String) erweitert MsgLogouted-versiegelte abstrakte Klasse MsgLogined-Fallklasse Logout() erweitert MsgLogined-Fallklasse Greet() erweitert MsgLogined-Abstraktklasse View ( def run() : Msg ) versiegelte abstrakte Klasse Model ( Typ Message def view() : View ) case class Logouted() erweitert Model ( type Message = MsgLogined override def view() : View .... ) case class Logined(name: String) erweitert Model ( type Message = MsgLogined override def view( ): Sicht .... )
    Algebraische Typen in Scala werden durch Vererbung modelliert. Der Typ entspricht einigen versiegelte abstrakte Klasse, und jeder Konstruktor erbte davon Fallklasse. Wir werden versuchen, sie genau als algebraische Typen zu verwenden und alle Variablen als zum übergeordneten Element gehörend zu beschreiben versiegelte abstrakte Klasse.

    Die Klassen MsgLogined und MsgLogouted in unserem Programm haben keinen gemeinsamen Vorfahren. Die Ansichtsfunktion musste auf verschiedene Klassen des Modells verteilt werden, um Zugriff auf einen bestimmten Nachrichtentyp zu haben. Das hat seine Vorteile, die OO-Anhänger zu schätzen wissen – der Code ist nach Geschäftslogik gruppiert, alles, was mit einem Anwendungsfall zu tun hat, ist in der Nähe. Aber ich würde die Ansicht lieber in eine separate Funktion aufteilen, deren Entwicklung auf eine andere Person übertragen werden könnte.

    Jetzt implementieren wir den Updater

    Object Updater ( def update(model: Model)(msg: model.Message) : Model = ( model match ( case Logouted() => msg match ( case Login(name) => Logined(name) ) case Logined(name) => msg match ( case Logout() => Logouted() case Greet() => model ) ) ) )
    Hier verwenden wir pfadabhängige Typen, um den Typ des zweiten Arguments ausgehend vom Wert des ersten zu beschreiben. Damit Scala solche Abhängigkeiten akzeptiert, müssen Funktionen in Curry-Form beschrieben werden, also als Funktion aus dem ersten Argument, die eine Funktion aus dem zweiten Argument zurückgibt. Leider führt Scala zu diesem Zeitpunkt nicht viele Typprüfungen durch, für die der Compiler über genügend Informationen verfügt.

    Lassen Sie uns nun eine vollständige Implementierung des Modells und der Ansicht geben

    Die Fallklasse Logouted() erweitert das Modell ( type Message = MsgLogouted override def view() : View = new View ( override def run() = ( println("Enter name ") val name = scala.io.StdIn.readLine() Login (name) ) ) case class Logined(name: String) erweitert Model ( type Message = MsgLogined override def view() : View = new View ( override def run() = ( println(s"Hallo, $name") println ( „Leere Zeichenfolge zum Abmelden, keine leere Zeichenfolge zur Begrüßung.“) scala.io.StdIn.readLine() match ( case „“ => Logout() case _ => Greet() ) ) ) abstract class View ( def run( ) : Msg ) Objekt-Viewer ( def view(model: Model): View = ( model.view() ) )
    Der Rückgabetyp einer Ansichtsfunktion hängt von der Instanz ihres Arguments ab. Aber für die Umsetzung kommt es auf das Modell an.

    Die so erstellte Anwendung wird wie folgt gestartet:

    Objekt Main ( import scala.annotation.tailrec @tailrec def Process(m: Model) ( val msg = Viewer.view(m).run() Process(Updater.update(m)(msg)) ) def main(args: Array) = ( Process(Logouted()) ) )
    Der Code des Laufzeitsystems weiß somit nichts über die interne Struktur von Modellen und Nachrichtentypen, der Compiler kann jedoch prüfen, ob die Nachricht zum aktuellen Modell passt.

    Hier benötigten wir nicht alle Fähigkeiten, die pfadabhängige Typen bieten. Interessante Eigenschaften ergeben sich, wenn wir parallel mit mehreren Instanzen von Model-Updater-View-Systemen arbeiten, beispielsweise bei der Simulation einer Multi-Agenten-Welt (die Ansicht würde dann den Einfluss des Agenten auf die Welt und den Erhalt von Feedback darstellen). In diesem Fall überprüfte der Compiler, ob die Nachricht von genau dem Agenten verarbeitet wurde, für den sie bestimmt war, obwohl alle Agenten den gleichen Typ haben.

    C++

    C++ reagiert immer noch auf die Reihenfolge der Definitionen, auch wenn diese alle in derselben Datei erstellt werden. Dies führt zu einigen Unannehmlichkeiten. Ich werde den Code in einer Reihenfolge präsentieren, die zur Demonstration von Ideen geeignet ist. Eine kompilierbare Version finden Sie auf GitHub.

    Algebraische Typen können auf die gleiche Weise wie in Scala implementiert werden – eine abstrakte Klasse entspricht einem Typ und konkrete Nachkommen entsprechen Konstruktoren (nennen wir sie „Konstruktorklassen“, um sie nicht mit gewöhnlichen C++-Konstruktoren zu verwechseln) der Algebra Typ.

    C++ unterstützt pfadabhängige Typen, aber der Compiler kann den Typ nicht abstrakt verwenden, ohne den tatsächlichen Typ zu kennen, dem er zugeordnet ist. Daher ist es unmöglich, Model-Updater-View mit ihrer Hilfe zu implementieren.

    Aber C++ verfügt über ein leistungsstarkes Vorlagensystem. Die Abhängigkeit des Typs vom Modellwert kann in einem Vorlagenparameter einer spezialisierten Version des Ausführungssystems ausgeblendet werden.

    Struct Processor ( virtual const Processor *next() const = 0; ); Vorlage struct ProcessorImpl: öffentlicher Prozessor ( const CurModel * model; ProcessorImpl (const CurModel* m) : model(m) ( ); const Prozessor *next() const ( const View * view = model->view(); const typename CurModel::Message * msg = view->run(); Ansicht löschen; const Model * newModel = msg->process(model); Nachricht löschen; return newModel->processor(); ) );
    Wir beschreiben ein abstraktes Ausführungssystem mit einer einzigen Methode, um alles zu tun, was erforderlich ist, und ein neues Ausführungssystem zurückzugeben, das für die nächste Iteration geeignet ist. Die spezifische Version verfügt über einen Vorlagenparameter und wird für jede „Konstruktorklasse“ des Modells spezialisiert. Hierbei ist es wichtig, dass alle Eigenschaften des CurModel-Typs während der Spezialisierung der Vorlage mit einem bestimmten Typparameter überprüft werden und zum Zeitpunkt der Kompilierung der Vorlage selbst nicht beschrieben werden müssen (obwohl dies möglich ist). Konzepte oder andere Arten der Implementierung von Typklassen). Scala verfügt auch über ein recht leistungsfähiges System parametrisierter Typen, überprüft jedoch die Eigenschaften von Parametertypen während der Kompilierung des parametrisierten Typs. Dort ist die Implementierung eines solchen Musters schwierig, aber dank der Unterstützung von Typklassen möglich.

    Beschreiben wir das Modell.

    Struct Model ( virtual ~Model() (); virtual const Processor *processor() const = 0; ); struct Angemeldet: öffentliches Modell ( struct Message ( const virtual Model * Process(const Logined * m) const = 0; virtual ~Message() (); struct Logout: öffentliche Nachricht ( const Model * Process(const Logined * m) const; struct Greet: public Message ( const Model * Process(const Logined * m) const; ); const std::string name; (...); const-Ansicht * view() const ( return new LoginedView(name); ); const Processor *processor() const ( neues ProcessorImpl zurückgeben (Das); (...); const-Ansicht ); ); struct Logouted: public Model ( struct Message ( const virtual Model * Process(const Logouted * m) const = 0; virtual ~Message() (); struct Login: public Message ( const std::string name; Login(std :: string lname) : name(lname) ( ); const Model * Process(const Logouted * m) const ; * view() const ( return new LogoutedView(); ); const Processor *processor() const ( neues ProcessorImpl zurückgeben
    (Das);

    ); );

    Const Model * Logouted::Login::process(const Logouted * m) const ( delete m; return new Logined(name); ); const Model * Logined::Logout::process(const Logined * m) const ( delete m; return new Logouted(); ); const Model * Logined::Greet::process(const Logined * m) const ( return m; );
    Lassen Sie uns nun alles zusammenstellen, was mit der Ansicht zusammenhängt, einschließlich der internen Einheiten der Modelle

    Vorlage struct View ( virtual const Message * run() const = 0; virtual ~View () (); ); struct Logined: public Model ( struct LoginedView: public View ( const std::string name; LoginedView(std::string lname) : name(lname) (); virtual const Message * run() const ( char buf; printf("Hallo %s", name.c_str()) ; fgets(buf, 15, stdin); return (*buf == 0 || *buf == "\n" || *buf == "\r") ? (new Logout()) : static_cast (neuer Gruß); ); ); const-Ansicht * view() const ( return new LoginedView(name); ); ); struct Logouted: public Model ( struct LogoutedView: public View ( virtual const Message * run() const ( char buf; printf("Login: "); fgets(buf, 15, stdin); return new Login(buf); ); ); const-Ansicht * view() const ( return new LogoutedView(); ); );
    Und zum Schluss schreiben wir main

    Int main(int argc, char ** argv) ( const Processor * p = new ProcessorImpl (new Logouted()); while(true) ( ​​​​const Processor * pnew = p->next(); delete p; p = pnew; ) return 0; )

    Und wieder Scala, dieses Mal mit Typklassen

    In der Struktur repliziert diese Implementierung fast vollständig die C++-Version.

    Ähnlicher Teil des Codes

    abstrakte Klasse View ( def run(): Message ) abstrakte Klasse Processor ( def next(): Processor; ) versiegelte abstrakte Klasse Model ( defprocessor(): Processor ) versiegelte abstrakte Klasse LoginedMessage-Fallklasse Logout() erweitert LoginedMessage-Fallklasse Greet( ) erweitert die LoginedMessage-Fallklasse Logined(val name: String) erweitert Model ( override defprocessor(): Processor = new ProcessorImpl(this) ) versiegelte abstrakte Klasse LogoutedMessage-Fallklasse Login(name: String) erweitert LogoutedMessage-Fallklasse Logouted() erweitert Model ( override def Processor(): Processor = new ProcessorImpl(this) ) object Main ( import scala.annotation.tailrec @tailrec def Process(p: Processor) ( Process(p.next()) ) def main(args: Array) = (process(new ProcessorImpl(Logouted())) ) )


    Doch bei der Umsetzung der Laufzeitumgebung ergeben sich Feinheiten.

    Klasse ProcessorImpl(model: M)(impliziter Updater: (M, Message) => Model, view: M => View) erweitert Processor ( def next(): Processor = ( val v = view(model) val msg = v. run() val newModel = updater(model,msg) newModel.processor() ) )
    Hier sehen wir neue mysteriöse Parameter (Impliziter Updater: (M, Message) => Modell, Ansicht: M => Ansicht). Das Schlüsselwort implicit bedeutet, dass der Compiler beim Aufruf dieser Funktion (genauer gesagt des Klassenkonstruktors) nach Objekten geeigneter Typen sucht, die im Kontext als implizit markiert sind, und diese als entsprechende Parameter übergibt. Dies ist ein ziemlich komplexes Konzept, dessen Anwendung die Implementierung von Typklassen ist. Hier versprechen sie dem Compiler, dass für bestimmte Implementierungen des Modells und der Nachricht alle notwendigen Funktionen von uns bereitgestellt werden. Nun lasst uns dieses Versprechen einlösen.

    Objektaktualisierungsprogramme ( implicit def logoutedUpdater(model: Logouted, msg: LogoutedMessage): Model = ( (model, msg) match ( case (Logouted(), Login(name)) => Logined(name) ) ) implicit def viewLogouted(model : Logouted) = new View ( override def run() : LogoutedMessage = ( println("Enter name ") val name = scala.io.StdIn.readLine() Login(name) ) ) implicit def loginedUpdater(model: Logined, msg : LoginedMessage): Model = ( (model, msg) match ( case (Logined(name), Logout()) => Logouted() case (Logined(name), Greet()) => model ) ) implicit def viewLogined( model: Logined) = new View ( val name = model.name override def run() : LoginedMessage = ( println(s"Hello, $name") println("Leere Zeichenfolge für die Abmeldung, nonempy für die Begrüßung.") scala.io .StdIn.readLine() match ( case "" => Logout() case _ => Greet() ) ) ) import updaters._

    Haskell

    Im Mainstream-Haskell gibt es keine abhängigen Typen. Es fehlt auch die Vererbung, die wir bei der Implementierung des Musters in Scala und C++ maßgeblich genutzt haben. Aber die einstufige Vererbung (mit Elementen abhängiger Typen) kann mithilfe von mehr oder weniger standardmäßigen Spracherweiterungen modelliert werden – TypeFamilies und ExistentialQuantification. Für die gemeinsame Schnittstelle untergeordneter OOP-Klassen wird eine Typklasse erstellt, in der es einen abhängigen „Familien“-Typ gibt, die untergeordneten Klassen selbst durch einen separaten Typ dargestellt werden und dann mit einem einzigen Konstruktor in einen „existenziellen“ Typ verpackt werden .

    Datenmodell = für alle m. (Aktualisierbar m, Sichtbar m) => Modell m-Klasse Aktualisierbar m, wobei Daten Nachricht m:: * update:: m -> (Nachricht m) -> Modellklasse (Aktualisierbar m) => Sichtbar m, wobei Ansicht:: m -> (Ansicht (Nachricht m)) data Logouted = Abgemeldete Daten Logined = Angemeldete Zeichenfolge
    Ich habe versucht, Updater und View so weit wie möglich zu trennen, also habe ich zwei verschiedene Typklassen erstellt, aber bisher hat es nicht gut geklappt.

    Die Updater-Implementierung ist einfach

    Instanz aktualisierbar abgemeldet, wo Daten abgemeldet sind Begrüßung Update m Abmelden = Modell Abgemeldet Update m Begrüßung = Modell m
    Ich musste IO als View festlegen. Versuche, es abstrakter zu machen, verkomplizierten alles erheblich und erhöhten die Koppelung des Codes – der Modelltyp muss wissen, welche Ansicht wir verwenden werden.

    System.IO-Typ importieren View a = IO eine Instanz Sichtbar Abgemeldet wo view Abgemeldet = do putStr "Login: " !\n" hFlush stdout l<- getLine pure $ if l == "" then Logout else Greeting
    Nun, die ausführbare Umgebung unterscheidet sich kaum von der ähnlichen in Idris

    RunMUV::Model -> IO a runMUV (Model m) = do msg<- view m runMUV $ update m msg main:: IO () main = runMUV (Model Logouted)

  • mob_info