De weergaven voor inloggen en uitloggen. Model-update-Bekijk patroon en afhankelijke typen
Voornamelijk voor het ontwikkelen van gebruikersinterfaces. Om het te gebruiken, moet u een modeltype maken dat de volledige status van het programma vertegenwoordigt, het berichttype dat de gebeurtenissen van de externe omgeving beschrijft, waarop het programma moet reageren door zijn status te wijzigen, de updater-functie, die van de oude staat en message creëert een nieuwe status van het programma en de view-functie, die de vereiste effecten op de externe omgeving berekent uit de status van het programma, die gebeurtenissen van het type Message oproepen. Het patroon is erg handig, maar het heeft een klein nadeel: het laat niet toe om te beschrijven welke gebeurtenissen zinvol zijn voor specifieke staten van het programma.
Een soortgelijk probleem doet zich voor (en wordt opgelost) bij het gebruik van de OO-patroonstatus.
De Elm-taal is eenvoudig, maar zeer strikt - het controleert of de updater-functie op de een of andere manier alle mogelijke combinaties van modelstatus en berichtgebeurtenis afhandelt. Daarom moet u onnodige, zij het triviale - schrijven - in de regel, het model ongewijzigd laten, code. Ik wil laten zien hoe dit kan worden vermeden in complexere talen - Idris, Scala, C ++ en Haskell.
Alle hier getoonde code is beschikbaar op GitHub om te experimenteren. Laten we eens kijken naar de meest interessante plaatsen.
De msg-functie is ongebruikelijk - het retourneert geen waarde, maar een type. Tijdens runtime is er niets bekend over waardetypes - de compiler wist alle onnodige informatie. Dat wil zeggen, een dergelijke functie kan alleen worden aangeroepen tijdens het compileren.
MUV is een constructeur. Het accepteert parameters: model - de initiële status van het programma, updater - de functie van het bijwerken van de status van een externe gebeurtenis en bekijken - de functie van het creëren van een externe weergave. Merk op dat het type updater en weergavefuncties afhangt van de waarde van het model (met behulp van de msg-functie van de typeparameters).
Laten we nu eens kijken hoe we deze applicatie kunnen uitvoeren.
MuvRun: (ApplicatiemodelType msgType IO) -> IO a muvRun (MUV-modelupdaterweergave) = doe msg<- view model
muvRun (MUV (updater model msg) updater view)
Als externe weergave (view) hebben we gekozen voor een I/O-operatie (in Idris, net als in Haskell, zijn I/O-operaties eersteklas waarden, zodat ze zouden worden uitgevoerd, aanvullende acties moeten worden ondernomen, meestal retourneren zo'n operatie van de hoofdfunctie).
IO in één oogopslag
Bij het uitvoeren van een bewerking van het type (IO a) treedt enige invloed op de buitenwereld op, mogelijk leeg, en wordt een waarde van het type a teruggegeven aan het programma, maar de functies van de standaardbibliotheek zijn zo ontworpen dat deze alleen worden verwerkt door het genereren van een nieuwe waarde van het type IO b. Dit scheidt pure functies van functies met bijwerkingen. Dit is ongebruikelijk voor veel programmeurs, maar het helpt om betrouwbaardere code te schrijven.
Aangezien de muvRun-functie I / O voortbrengt, zou deze IO moeten retourneren, maar aangezien deze nooit zal worden voltooid, kan het type bewerking elk zijn - IO a.
Nu zullen we de soorten entiteiten beschrijven waarmee we gaan werken.
Gegevensmodel = uitgelogd | Login String data MsgOuted = Login String data MsgIned = Logout | Begroet totaal msgType: Model -> Type msgType Uitgelogd = MsgOuted msgType (Logined_) = MsgIned
Het modeltype wordt hier beschreven, wat de aanwezigheid van twee statussen van de interface weergeeft: de gebruiker is niet ingelogd en de gebruiker is ingelogd met een naam van het type String.
Vervolgens beschrijven we twee verschillende berichttypen die relevant zijn voor verschillende varianten van het model - als we zijn uitgelogd, kunnen we alleen inloggen onder een bepaalde naam, en als we al zijn ingelogd, kunnen we uitloggen of hallo zeggen. Idris is een sterk getypeerde taal die je niet toelaat om verschillende typen door elkaar te halen.
Eindelijk een functie die de overeenkomst van de modelwaarde met het berichttype instelt.
De functie wordt tot totaal verklaard - dat wil zeggen, hij mag niet crashen of vastlopen, de compiler zal proberen dit bij te houden. msgType wordt aangeroepen in de compilatiefase, wat betekent dat de totaliteit ervan betekent dat de compilatie niet vastloopt vanwege onze fout, hoewel het niet kan garanderen dat de uitvoering van deze functie de systeembronnen zal uitputten.
Het is ook gegarandeerd dat het "rm -rf /" niet zal uitvoeren omdat er geen IO in de handtekening staat.
Laten we de updater beschrijven:
Totale updater: (m: Model) -> (msgType m) -> Model updater Uitgelogd (Inlognaam) = Ingelogde naam updater (Ingelogde naam) Uitloggen = Uitgelogde updater (Ingelogde naam) Greet = Ingelogde naam
Ik denk dat de logica van deze functie duidelijk is. Ik wil de totaliteit herhalen - het betekent dat de Idris-compiler zal controleren of we alle alternatieven hebben overwogen die door het typesysteem zijn toegestaan. Elm voert ook een dergelijke controle uit, maar hij kan niet weten dat we niet kunnen uitloggen als we nog niet zijn ingelogd, en zal expliciete verwerking van de voorwaarde vereisen
Updater Uitgelogd Uitloggen = ???
Idris daarentegen zal bij een onnodige controle een typemismatch vinden.
Laten we nu naar de weergave gaan - zoals gebruikelijk in de gebruikersinterface, zal dit het moeilijkste deel van de code zijn.
Total loginPage: IO MsgOuted loginPage = do putStr "Login:" map Login getLine total genMsg: String -> MsgIned genMsg "" = Logout genMsg _ = Begroet totale workPage: String -> IO MsgIned workPage name = do putStr ("Hallo," ++ name ++ "\ n") putStr "Voer een lege string in voor uitloggen of niet-leeg voor begroeting \ n" map genMsg getLine totale weergave: (m: Model) -> IO (msgType m) weergave Afgemeld = loginPaginaweergave (Inlogde naam ) = naam werkpagina
view moet een I / O-bewerking maken die berichten retourneert, waarvan het type weer afhangt van de waarde van het model. We hebben twee opties: loginPage, die het bericht "Login:" uitvoert, een tekenreeks van het toetsenbord leest en deze in een Login-bericht en workPage verpakt met een gebruikersnaamparameter, die een begroeting afdrukt en verschillende berichten retourneert (maar van de hetzelfde type - MsgIned), afhankelijk van of de gebruiker een lege of niet-lege tekenreeks invoert. view retourneert een van deze bewerkingen, afhankelijk van de waarde van het model, en de compiler controleert hun type, ook al is het anders.
Nu kunnen we onze applicatie bouwen en uitvoeren.
App: Applicatiemodel Main.msgType IO app = MUV Uitgelogde updater view main: IO () main = muvRun app
Een subtiel punt moet hier worden opgemerkt - de muvRun-functie keert terug IO a waarbij a niet is opgegeven en main van het type is IO (), waar ()
is de naam van het type, meestal genoemd Eenheid, die een enkele waarde heeft, ook geschreven als leeg stom ()
... Maar de compiler handelt dit gemakkelijk af. vervangen in plaats van een ().
Scala- en padafhankelijke typen
Scala ondersteunt afhankelijke typen niet volledig, maar er zijn padafhankelijke typen. In de afhankelijke typetheorie kunnen ze worden beschreven als een variant van het sigmatype. Met padafhankelijke typen kunt u het toevoegen van vectoren uit verschillende vectorruimten verbieden, of beschrijven met wie u kunt kussen. Maar we zullen ze gebruiken voor eenvoudigere taken.Verzegelde abstracte klasse MsgLogouted case-klasse Login (naam: String) breidt MsgLogouted verzegelde abstract-klasse uit MsgLogined case-klasse Logout () breidt MsgLogined case-klasse uit Greet () breidt MsgLogined abstracte klasse uit View (def run (): Msg) verzegelde abstracte klasse Model (type Bericht def view (): View) case class Uitgelogd () breidt Model uit (type Message = MsgLogouted override def view (): View ....) case class Logined (name: String) breidt Model uit (type Message = MsgLogouted override def view ( ): Weergave ....)
De algebraïsche typen van Scala worden gemodelleerd door middel van overerving. Een soort ontmoet verzegelde abstracte klasse, en voor elke constructor die ervan is geërfd geval klasse... We zullen proberen ze precies als algebraïsche typen te gebruiken, waarbij we alle variabelen beschrijven als behorend tot de ouder verzegelde abstracte klasse.
De klassen MsgLogined en MsgLogouted in ons programma hebben geen gemeenschappelijke voorouder. De view-functie moest over verschillende modelklassen worden verdeeld om toegang te krijgen tot een bepaald type berichten. Dit heeft zijn voordelen, die zullen worden gewaardeerd door OO-supporters - de code blijkt gegroepeerd te zijn in overeenstemming met de bedrijfslogica, alles wat met één use case te maken heeft, is dichtbij. Maar ik had het uitzicht graag opgesplitst in een aparte functie, waarvan de ontwikkeling aan een ander zou kunnen worden gedelegeerd.
Laten we nu updater implementeren
Object Updater (def update (model: Model) (msg: model.Message): Model = (model match (case Logouted () => msg match (case Login (name) => Logined (name)) case Logined (name) => bericht match (case Uitloggen () => Uitgelogd () case Greet () => model))))
Hier beschrijven we, met behulp van padafhankelijke typen, het type van het tweede argument vanaf de waarde van het eerste. Om Scala dergelijke afhankelijkheden te laten accepteren, moeten functies worden beschreven in curried-vorm, dat wil zeggen als een functie van het eerste argument, die een functie van het tweede argument retourneert. Helaas voert Scala op dit moment niet veel van de typecontroles uit waarvoor de compiler voldoende informatie heeft.
Laten we nu de volledige implementatie van het model en de weergave geven
Case klasse Logouted () breidt Model uit (type Message = MsgLogouted override def view (): View = new View (override def run () = (println ("Voer naam in") val name = scala.io.StdIn.readLine () Login (naam)))) case class Aangemeld (naam: String) breidt Model uit (type Bericht = MsgLogined override def view (): View = new View (override def run () = (println (s "Hello, $ name") println ("Lege tekenreeks voor uitloggen, geen voor begroeting.") Scala.io.StdIn.readLine () match (case "" => Logout () case _ => Greet ())))) abstracte klasse View (def run ( ): Msg) object Viewer (def view (model: Model): View = (model.view ()))
Het retourtype van de view-functie hangt af van de instantie van het argument. Maar voor de implementatie draait het om het model.
De applicatie die op deze manier is gemaakt, wordt als volgt gestart
Object 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) = (proces (Uitgelogd ())))
De runtime-systeemcode weet dus niets over de interne structuur van de modellen en de soorten berichten, maar de compiler kan controleren of het bericht overeenkomt met het huidige model.
Hier hadden we niet alle mogelijkheden nodig die padafhankelijke typen bieden. Interessante eigenschappen zullen verschijnen als we parallel werken met verschillende instanties van Model-Updater-View-systemen, bijvoorbeeld bij het simuleren van een multi-agent wereld (een weergave zou dan de invloed van de agent op de wereld vertegenwoordigen en feedback ontvangen). In dit geval controleerde de compiler of het bericht werd verwerkt door de agent waarvoor het bedoeld was, ondanks het feit dat alle agenten van hetzelfde type zijn.
C ++
C++ is nog steeds gevoelig voor de volgorde van definities, zelfs als ze allemaal in hetzelfde bestand worden gedaan. Dit zorgt voor enige overlast. Ik zal de code verstrekken in een volgorde die handig is om ideeën te demonstreren. De gecompileerde geordende versie kan worden bekeken op GitHub.Algebraïsche typen kunnen op dezelfde manier worden geïmplementeerd als in Scala - een abstracte klasse komt overeen met een type, en concrete overerven komen overeen met constructors (laten we ze "constructorklassen" noemen, om niet te worden verward met gewone C++-constructors) van een algebraïsch type.
C ++ heeft ondersteuning voor padafhankelijke typen, maar de compiler kan dat type niet abstract gebruiken zonder het echte type te kennen waarmee het is geassocieerd. Daarom is het onmogelijk om Model-Updater-View met hun hulp te implementeren.
Maar C++ heeft een krachtig sjabloonsysteem. De afhankelijkheid van het type van de waarde van het model kan worden verborgen in de sjabloonparameter van een gespecialiseerde versie van het uitvoerende systeem.
Struct Processor (virtuele const Processor * volgende () const = 0;); sjabloon
We beschrijven een abstract uitvoerend systeem, waarbij de enige methode is om te doen wat nodig is en een nieuw uitvoerend systeem terug te geven dat geschikt is voor de volgende iteratie. De specifieke versie heeft een sjabloonparameter en zal gespecialiseerd zijn voor elke "constructorklasse" van het model. Hierbij is het van belang dat alle eigenschappen van het type CurModel tijdens sjabloonspecialisatie worden gecontroleerd met een specifieke typeparameter, en op het moment van compilatie van de sjabloon zelf niet hoeven te worden beschreven (hoewel het mogelijk is met behulp van concepten of andere methoden voor het implementeren van typeklassen). Scala heeft ook een redelijk krachtig systeem van geparametriseerde typen, maar het controleert de eigenschappen van type-parameters tijdens het compileren van het geparametriseerde type. Daar is de implementatie van een dergelijk patroon moeilijk, maar mogelijk dankzij de ondersteuning van typeklassen.
Laten we het model beschrijven.
Struct Model (virtueel ~ Model () (); virtuele const Processor * processor () const = 0;); struct Logined: public Model (struct Message (const virtual Model * process (const Logined * m) const = 0; virtual ~ Message () ();); struct Logout: public Message (const Model * process (const Logined * m) const;); struct Greet: public Message (const Model * process (const Logined * m) const;); const std :: string name; Logined (std :: string lname): name (lname) (); struct LoginedView: openbare weergave
"Klassen-constructeurs" van het model "dragen alles bij zich" - dat wil zeggen, ze bevatten gespecialiseerde klassen van berichten en weergaven, en weten ook hoe ze een uitvoerend systeem voor zichzelf kunnen creëren. Native View-typen hebben een gemeenschappelijke voorouder voor alle modellen, wat handig kan zijn bij het ontwikkelen van complexere uitvoeringssystemen. Berichttypen zijn in principe volledig geïsoleerd en hebben geen gemeenschappelijke voorouder.
De implementatie van de updater staat los van het model omdat het vereist dat het modeltype al volledig is beschreven.
Const Model * Uitgelogd :: Login :: process (const Uitgelogd * m) const (verwijder m; retourneer nieuwe Logined (naam);); const Model * Ingelogd :: Uitloggen :: process (const Ingelogd * m) const (verwijder m; retourneer nieuwe Uitgelogd ();); const Model * Ingelogd :: Greet :: process (const Ingelogd * m) const (return m;);
Laten we nu alles met betrekking tot de weergave samenvoegen, inclusief de interne entiteiten van de modellen.
Sjabloon
Laten we tot slot main schrijven
Int main (int argc, char ** argv) (const Processor * p = nieuwe ProcessorImpl
Weer Scala, met typeklassen
In structuur is deze implementatie bijna identiek aan de C++ versie.Een soortgelijk deel van de code
abstracte klasse View (def run (): Message) abstracte class Processor (def next (): Processor;) verzegelde abstracte klasse Model (def processor (): Processor) verzegelde abstracte klasse LoginedMessage-caseklasse Logout () breidt LoginedMessage-caseklasse uit Greet ( ) breidt LoginedMessage case-klasse uit Logined (naam: String) breidt Model uit (override def processor (): Processor = new ProcessorImpl (this)) verzegelde abstracte klasse LogoutedMessage case class Login (naam: String) breidt LogoutedMessage case-klasse uit Logouted () breidt Model uit (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) = (proces (nieuwe ProcessorImpl (Uitgelogd ()))))
Maar bij de implementatie van de runtime ontstaan subtiliteiten.
Class ProcessorImpl (model: M) (impliciete updater: (M, Message) => Model, view: M => View) breidt Processor uit (def next (): Processor = (val v = view (model) val msg = v. run () val newModel = updater (model, msg) newModel.processor ()))
Hier zien we mysterieuze nieuwe opties (impliciete updater: (M, Message) => Model, view: M => View)... Het impliciete sleutelwoord betekent dat de compiler bij het aanroepen van deze functie (meer bepaald de klassenconstructor) in de context zal zoeken naar objecten van geschikte typen die als impliciet zijn gemarkeerd en deze als geschikte parameters doorgeven. Dit is een vrij complex concept en een van de toepassingen ervan is de implementatie van typeclasses. Hier beloven ze de compiler dat we alle noodzakelijke functies zullen bieden voor specifieke implementaties van het model en bericht. Laten we die belofte nu houden.
Object-updaters (impliciete def logoutedUpdater (model: Logouted, msg: LogoutedMessage): Model = ((model, msg) match (case (Logouted (), Login (name)) => Logined (name))) impliciete def viewLogouted (model : Uitgelogd) = nieuwe weergave (overschrijven def run (): LogoutedMessage = (println ("Voer naam in") val naam = scala.io.StdIn.readLine () Login (naam))) impliciete def loginedUpdater (model: Logined, msg : LoginedMessage): Model = ((model, msg) match (case (Logined (name), Logout ()) => Logouted () case (Logined (name), Greet ()) => model)) impliciete def viewLogined ( model: Logined) = nieuwe weergave (val name = model.name override def run (): LoginedMessage = (println (s "Hallo, $ naam") println ("Lege tekenreeks voor uitloggen, geen teken voor begroeting.") scala.io .StdIn.readLine () match (case "" => Logout () case _ => Greet ())))) import updaters._
Haskell
Er zijn geen afhankelijke typen in de reguliere Haskell. Het mist ook overerving, die we voornamelijk gebruikten bij het implementeren van het patroon in Scala en C ++. Maar overerving op één niveau (met elementen van afhankelijke typen) kan worden gemodelleerd met behulp van min of meer standaard taalextensies -TypeFamilies en ExistentialQuantification. Voor de algemene interface van kind-OOP-klassen wordt een typeklasse gemaakt, waarin er een afhankelijk "familie" -type is, de onderliggende klassen zelf worden vertegenwoordigd door een afzonderlijk type en vervolgens verpakt in een "existentieel" type met een enkele constructor .Gegevensmodel = voor alle m. (Updatable m, Viewable m) => Model m class Updateable m where data Bericht m :: * update :: m -> (Message m) -> Model class (Updatable m) => Viewable m where view :: m -> (Bekijken (Bericht m)) gegevens Uitgelogd = Uitgelogde gegevens Ingelogd = Ingelogd String
Ik heb geprobeerd de updater en weergave zo ver mogelijk te verspreiden, dus ik heb twee verschillende typeklassen gemaakt, maar tot nu toe bleek het slecht.
De implementatie van de updater is eenvoudig
Instance Bijwerkbaar Uitgelogd waar gegevens Bericht Uitgelogd = Inloggen String update Uitgelogd (Inlognaam) = Model (Inlogde naam) instantie Bijwerkbaar Ingelogd waar gegevens Bericht Ingelogd = Uitloggen | Update begroeting m Uitloggen = Model Uitgelogde update m Begroeting = Model m
IO moest worden vastgesteld als View. Pogingen om het abstracter te maken hebben alles enorm gecompliceerd en de samenhang van de code vergroot - het modeltype moet weten welke weergave we gaan gebruiken.
Import System.IO type View a = IO een instance Zichtbaar Uitgelogd waar view Uitgelogd = do putStr "Login:" hFlush stdout fmap Login getLine instance Zichtbaar Aangemeld waar view (Logined name) = do putStr $ "Hallo" ++ name ++ " ! \ n "hFlush stdout l<- getLine
pure $ if l == ""
then
Logout
else
Greeting
Welnu, de uitvoerbare omgeving verschilt niet veel van die in Idris.
RunMUV :: Model -> IO een runMUV (Model m) = do msg<- view m runMUV $ update m msg main:: IO () main = runMUV (Model Logouted)
Velen beginnen een project te schrijven om met een enkele taak te werken, zonder te impliceren dat het kan uitgroeien tot een beheersysteem voor meerdere gebruikers, nou ja, laten we zeggen, inhoud, of God verhoede, productie. En alles lijkt cool en cool te zijn, alles werkt totdat je begint te begrijpen dat de code die wordt geschreven volledig uit krukken en hardcode bestaat. Code vermengd met lay-out, verzoeken en krukken, soms zelfs onleesbaar. Er doet zich een urgent probleem voor: bij het toevoegen van nieuwe functies moet je heel lang aan deze code sleutelen, onthoudend "wat stond daar dat was?" en vervloek jezelf in het verleden.Je hebt misschien zelfs gehoord van ontwerppatronen en zelfs door deze uitstekende boeken gebladerd:
- E. Gamma, R. Helm, R. Johnson, J. Vlissides "Objectgeoriënteerde ontwerptechnieken. Ontwerp patronen ";
- M. Fowler "Architectuur van bedrijfssoftwaretoepassingen".
Het gepresenteerde artikel zal vooral nuttig zijn voor beginners. Hoe dan ook, ik hoop dat je over een paar uur een idee zult krijgen van de implementatie van het MVC-patroon dat ten grondslag ligt aan alle moderne webframeworks, en ook wat "voedsel" krijgt om verder na te denken over "hoe te doen het." Aan het einde van het artikel vindt u een selectie van handige links die u ook zullen helpen begrijpen waar webframeworks (naast MVC) van gemaakt zijn en hoe ze werken.
Het is onwaarschijnlijk dat hard-core PHP-programmeurs iets nieuws voor zichzelf zullen vinden in dit artikel, maar hun opmerkingen en commentaar op de hoofdtekst zouden zeer nuttig zijn! Omdat zonder theorie is praktijk onmogelijk, en zonder praktijk is theorie nutteloos, dan komt er eerst een beetje theorie, en dan gaan we verder met de praktijk. Als je al bekend bent met het concept van MVC, kun je het theoriegedeelte overslaan en meteen naar de praktijk gaan.
1. Theorie
Het MVC-patroon beschrijft een eenvoudige manier om de structuur van een applicatie te bouwen, met als doel de bedrijfslogica te scheiden van de gebruikersinterface. Hierdoor is de applicatie makkelijker te schalen, te testen, te onderhouden en natuurlijk te implementeren.Laten we eens kijken naar het conceptuele schema van het MVC-patroon (naar mijn mening is dit het meest succesvolle schema dat ik heb gezien):
In MVC-architectuur biedt het model gegevens en bedrijfslogicaregels, is de view verantwoordelijk voor de gebruikersinterface en is de controller verantwoordelijk voor de interactie tussen het model en de view.
Een typische workflow voor een MVC-toepassing kan als volgt worden beschreven:
- Wanneer een gebruiker een webresource invoert, maakt het initialisatiescript een instantie van de toepassing en start deze voor uitvoering.
Dit toont een weergave van bijvoorbeeld de startpagina van de site. - De applicatie ontvangt een verzoek van de gebruiker en bepaalt de gevraagde controller en actie. In het geval van een stramienpagina is de standaardactie ( inhoudsopgave).
- De applicatie start de controller en voert de actiemethode uit,
die bijvoorbeeld modelaanroepen bevat die informatie uit de database lezen. - Daarna genereert de actie een weergave met de gegevens die uit het model zijn verkregen en geeft het resultaat aan de gebruiker weer.
Het model mag niet rechtstreeks communiceren met de gebruiker. Alle variabelen met betrekking tot het verzoek van de gebruiker moeten in de controller worden verwerkt.
Het model mag geen HTML of andere weergavecode genereren die kan veranderen op basis van de behoeften van de gebruiker. Dergelijke code moet in views worden verwerkt.
Hetzelfde model, bijvoorbeeld: het gebruikersauthenticatiemodel kan zowel in het gebruikers- als in het administratieve deel van de applicatie worden gebruikt. In dit geval kunt u de algemene code naar een aparte klasse verplaatsen en ervan erven, waarbij u subtoepassingsspecifieke methoden definieert in de erven.
Weergave- wordt gebruikt om de externe weergave van gegevens die van de controller en het model worden ontvangen, in te stellen.
Weergaven bevatten HTML-opmaak en kleine invoegingen van PHP-code om gegevens te doorkruisen, op te maken en weer te geven.
Mag niet rechtstreeks toegang krijgen tot de database. Dit moet worden gedaan door modellen.
Zou niet moeten werken met gegevens die zijn verkregen uit een gebruikersverzoek. Deze taak moet worden uitgevoerd door de controller.
Heeft direct toegang tot de eigenschappen en methoden van de controller of modellen om kant-en-klare gegevens te krijgen.
Weergaven zijn meestal onderverdeeld in een gemeenschappelijke sjabloon die de opmaak bevat die voor alle pagina's geldt (bijvoorbeeld een kop- en voettekst) en delen van de sjabloon die worden gebruikt om gegevensuitvoer van het model weer te geven of formulieren voor gegevensinvoer weer te geven.
Controller- de lijm die modellen, aanzichten en andere componenten verbindt tot een productietoepassing. De verwerkingsverantwoordelijke is verantwoordelijk voor het afhandelen van gebruikersverzoeken. De controller mag geen SQL-query's bevatten. Ze worden het best bewaard in modellen. De controller mag geen HTML of andere opmaak bevatten. Het moet in de soort worden verwijderd.
In een goed ontworpen MVC-toepassing zijn controllers meestal erg dun en bevatten ze slechts enkele tientallen regels code. Dat kan niet gezegd worden over Stupid Fat Controllers (SFC) in CMS Joomla. De logica van de controller is vrij typisch en het meeste wordt uitgevoerd in basisklassen.
Modellen daarentegen zijn erg dik en bevatten de meeste gegevensverwerkingscode. de gegevensstructuur en bedrijfslogica die het bevat, is meestal vrij toepassingsspecifiek.
1.1. Frontcontroller en paginacontroller
In de meeste gevallen vindt de interactie van de gebruiker met de webapplicatie plaats door op links te klikken. Kijk nu in de adresbalk van uw browser - via deze link heeft u deze tekst ontvangen. Andere links, zoals die aan de rechterkant van deze pagina, zullen u andere inhoud geven. De link vertegenwoordigt dus een specifieke opdracht naar de webtoepassing.Ik hoop dat je al hebt gemerkt dat verschillende sites perfecte verschillende formaten kunnen hebben voor het bouwen van de adresbalk. Elk formaat kan de architectuur van een webapplicatie vertegenwoordigen. Hoewel dit niet altijd het geval is, is het in de meeste gevallen een duidelijk feit.
Overweeg twee opties voor de adresbalk, die een soort tekst en gebruikersprofiel tonen.
Eerste optie:
- www.example.com/article.php?id=3
- www.voorbeeld.com/gebruiker.php?id=4
Tweede optie:
- www.voorbeeld.com/index.php?article=3
- www.voorbeeld.com/index.php?user=4
Je kunt de multi-touchpoint-benadering zien op de phpBB-forums. Het forum wordt bekeken via een script viewforum.php, het onderwerp bekijken via viewtopic.php enzovoort. De tweede benadering, met toegang via een enkel fysiek scriptbestand, kan worden waargenomen in mijn favoriete CMS MODX, waar alle oproepen doorlopen index.php.
Deze twee benaderingen zijn totaal verschillend. De eerste is kenmerkend voor het Page Controller-patroon en de tweede benadering wordt geïmplementeerd door het Front Controller-patroon. De paginacontroller is goed voor sites met vrij eenvoudige logica. Op zijn beurt consolideert de verzoekcontroller alle acties voor het verwerken van verzoeken op één plek, wat hem extra mogelijkheden geeft, waardoor het mogelijk is om moeilijkere taken uit te voeren dan gewoonlijk door de paginacontroller wordt afgehandeld. Ik zal niet ingaan op de details van de implementatie van de paginacontroller, maar ik zal alleen zeggen dat in het praktische gedeelte de verzoekcontroller zal worden ontwikkeld (enige schijn).
1.2. URL-routering
Met URL-routering kunt u uw applicatie configureren om verzoeken te accepteren van URL's die niet overeenkomen met de daadwerkelijke bestanden in de applicatie, en om CNC's te gebruiken die semantisch betekenisvol zijn voor gebruikers en die de voorkeur hebben voor zoekmachineoptimalisatie.Voor een normale pagina waarop een contactformulier wordt weergegeven, kan de URL er bijvoorbeeld als volgt uitzien:
http://www.example.com/contacts.php?action=feedback
Geschatte verwerkingscode in dit geval:
switch ($ _GET ["action"]) (case "about": required_once ("about.php"); // "Over ons" pagina pauze; case "contacten": required_once ("contacts.php"); // "Contacten" pagina pauze; case "feedback": required_once ("feedback.php"); // "Feedback" pagina pauze; standaard: required_once ("page404.php"); // pagina "404" pauze; )
Ik denk dat bijna iedereen dit al eens heeft gedaan.
Met behulp van de URL-routeringsengine kunt u uw toepassing configureren om deze verzoeken te accepteren om dezelfde informatie weer te geven:
http://www.voorbeeld.com/contacts/feedback
Hier is contacten een controller en feedback is een controllermethode die contacten wordt genoemd en die een feedbackformulier weergeeft, enz. In het praktijkgedeelte komen we hierop terug.
Het is ook de moeite waard om te weten dat u met veel webframework-routers willekeurige URL-routes kunt maken (specificeer wat elk deel van de URL betekent) en hoe u ermee om moet gaan.
We hebben nu voldoende theoretische kennis om over te gaan naar de praktijk.
2. Oefenen
Laten we eerst de volgende bestands- en mapstructuur maken:
Vooruitkijkend, zal ik zeggen dat de kernmap de basismodel-, weergave- en controllerklassen zal opslaan.
Hun afstammelingen worden opgeslagen in de directory's controllers, modellen en views. Bestand index.php dit is een punt in het begin van de app. Bestand bootstrap.php start het downloaden van de applicatie, verbindt alle benodigde modules, enz.
We zullen consequent gaan; open het bestand index.php en vul het met de volgende code:
ini_set ("display_errors", 1); vereisen_once "applicatie / bootstrap.php";
Er mogen geen vragen zijn.
Laten we vervolgens direct naar het bestand gaan bootstrap.php:
vereisen_once "core / model.php"; vereisen_once "core / view.php"; vereisen_once "core / controller.php"; vereisen_once "core / route.php"; Route :: begin (); // start de router
De eerste drie regels bevatten niet-bestaande kernbestanden. De laatste regels verbinden het bestand met de routerklasse en starten het voor uitvoering door de statische startmethode aan te roepen.
2.1. URL-router implementatie
Laten we voorlopig afwijken van de implementatie van het MVC-patroon en de routering behandelen. De eerste stap die we moeten doen, is de volgende code in te schrijven: .htaccess:RewriteEngine op RewriteCond% (REQUEST_FILENAME)! -F RewriteCond% (REQUEST_FILENAME)! -D RewriteRule. * Index.php [L]
Deze code zal de verwerking van alle pagina's omleiden naar: index.php, dat is wat we nodig hebben. Weet je nog dat we het in het eerste deel hadden over de Front Controller?!
We zetten de routing in een apart bestand route.php naar de kernmap. In dit bestand zullen we de Route-klasse beschrijven, die de controller-methoden zal uitvoeren, die op hun beurt de paginaweergave zullen genereren.
Inhoud van route.php-bestand
klas Route ( statisch functie start () ( // controller en standaardactie$ controller_name = "Hoofd"; $ action_name = "index"; $ routes = explode ("/", $ _SERVER ["REQUEST_URI"]); // haal de naam van de controller op if (! leeg ($ routes)) ($ controllernaam = $ routes;) // haal de naam van de actie op if (! leeg ($ routes)) ($ action_name = $ routes;) // voeg voorvoegsels toe$ model_name = "Model_" $ controller_name; $ controller_name = "Controller_" $ controller_name; $ action_name = "action_" $ action_name; // koppel het bestand aan de modelklasse (het modelbestand bestaat mogelijk niet)$ model_file = strtolower ($ model_name) ".php"; $ model_path = "toepassing / modellen /" $ model_file; if (file_exists ($ model_path)) (inclusief "application / models /". $ model_file;) // koppel het bestand aan de controllerklasse$ controller_file = strtolower ($ controller_name) ".php"; $ controller_path = "toepassing / controllers /" $ controller_file; if (file_exists ($ controller_path)) (inclusief "application / controllers /". $ controller_file;) else ( / * het zou correct zijn om hier een uitzondering te plaatsen, maar voor de eenvoud zullen we onmiddellijk doorverwijzen naar een 404-pagina * / Route :: ErrorPage404 (); ) // maak een controller$ controller = nieuw $ controllernaam; $ actie = $ actienaam; if (method_exists ($ controller, $ actie)) ( // roep de actie van de controller aan$ controller -> $ actie (); ) anders ( // het zou ook redelijker zijn om hier een uitzondering te plaatsen Route :: ErrorPage404 (); )) functie ErrorPage404 () ($ host = "http: //". $ _ SERVER ["HTTP_HOST"]. "/"; header ("HTTP / 1.1 404 niet gevonden"); header ("Status: 404 niet gevonden"); header ("Locatie:". $ host. "404"); ))
Merk op dat de klasse een zeer vereenvoudigde logica implementeert (ondanks de omvangrijke code) en zelfs beveiligingsproblemen kan hebben. Dit is met opzet gedaan, aangezien het schrijven van een volwaardige routeringsklasse verdient op zijn minst een apart artikel. Laten we eens kijken naar de belangrijkste punten ...
Het element van de globale array $ _SERVER ["REQUEST_URI"] bevat het volledige adres waarmee de gebruiker contact heeft opgenomen.
Bijvoorbeeld: voorbeeld.ru/contacts/feedback
De functie gebruiken: ontploffen het adres is opgedeeld in onderdelen. Als resultaat krijgen we de naam van de controller, voor het gegeven voorbeeld is dit de controller contacten en de naam van de actie, in ons geval - feedback.
Vervolgens wordt het modelbestand gekoppeld (het model kan afwezig zijn) en het controllerbestand, indien aanwezig, en ten slotte wordt de controller geïnstantieerd en wordt de actie aangeroepen, opnieuw als deze werd beschreven in de controllerklasse.
Dus als u bijvoorbeeld naar het adres gaat:
voorbeeld.com/portfolio
of
voorbeeld.com/portfolio/index
de router doet het volgende:
- zal het bestand model_portfolio.php uit de map modellen bevatten die de klasse Model_Portfolio bevat;
- zal het bestand controller_portfolio.php uit de map controllers bevatten die de klasse Controller_Portfolio bevat;
- zal een instantie van de klasse Controller_Portfolio maken en de standaardactie - action_index die erin wordt beschreven, aanroepen.
voorbeeld.com/ufo
dan wordt het doorgestuurd naar de "404"-pagina:
voorbeeld.com/404
Hetzelfde gebeurt als de gebruiker een actie onderneemt die niet in de controller is beschreven.
2.2. Terug naar de MVC-implementatie
Laten we naar de kernmap gaan en nog drie bestanden toevoegen aan het route.php-bestand: model.php, view.php en controller.php
Laat me je eraan herinneren dat ze basisklassen zullen bevatten, die we nu gaan schrijven.
Bestandsinhoud model.php
klassemodel ( openbaar functie get_data () (
}
}
De modelklasse bevat een enkele lege methode voor het ophalen van gegevens die zal overlappen in afstammelingenklassen. Wanneer we afstammelingenklassen maken, wordt alles duidelijker.
Bestandsinhoud bekijk.php
klas bekijken (
// public $ template_view; // hier kunt u de standaard algemene weergave specificeren.
functie genereren ( $ content_view, $ template_view, $ data = null)
{
/ * if (is_array ($ data)) (// transformeer de array-elementen in variabelen extract ($ data);) * / omvatten "applicatie / views /". $ template_view; ))
Het is niet moeilijk te raden dat de methode genereren ontworpen om een uitzicht te vormen. De volgende parameters worden eraan doorgegeven:
- $ content_file - weergaven die pagina-inhoud weergeven;
- $ template_file - sjabloon gemeenschappelijk voor alle pagina's;
- $ data is een array die pagina-inhoudsitems bevat. Meestal ingevuld in het model.
om de inhoud van een specifieke pagina weer te geven.
In ons geval bevat de algemene sjabloon koptekst, menu, zijbalk en voettekst, en de inhoud van de pagina's wordt in een apart formulier opgenomen. Nogmaals, dit is gedaan voor de eenvoud.
Bestandsinhoud controller.php
klasse Controller ( openbaar $-model; openbare $ weergave; functie __construct () ($ dit -> weergave = nieuwe weergave (); )))
Methode action_index- dit is de standaardactie, we zullen deze overschrijven bij het implementeren van de afstammelingenklassen.
2.3. Afstammelingenklassen Model en Controller implementeren, View "s . maken
Nu begint het plezier! Onze visitekaartjessite zal bestaan uit de volgende pagina's:- huis
- Diensten
- Portefeuille
- Contacten
- En ook - pagina "404"
In de vorige afbeelding is het bestand afzonderlijk gemarkeerd. template_view.php Is een sjabloon met opmaak die voor alle pagina's geldt. In het eenvoudigste geval kan het er als volgt uitzien:
<html lang = "ru">
<hoofd>
<meta charset = "utf-8"> <titel> huistitel>
hoofd>
<lichaam>
$ inhoud_weergave; ?> lichaam>
html>
Om de site een presentabel uiterlijk te geven, maken we een CSS-sjabloon en integreren deze in onze site door de structuur van de HTML-opmaak te wijzigen en CSS- en JavaScript-bestanden aan elkaar te koppelen:
<link rel = "stylesheet" type = "text / css" href = "/ css / style.css" />
<script src = "/ js / jquery-1.6.2.js" type = "tekst / javascript">script>
Aan het einde van het artikel, in de sectie "Resultaat", staat een link naar de GitHub-repository met een project waarin stappen zijn genomen om een eenvoudige sjabloon te integreren.
2.3.1. Een startpagina maken
Laten we beginnen met de controller controller_main.php, hier is de code:class Controller_Main breidt Controller uit ( functie action_index () ($ dit -> bekijken-> genereren ("main_view.php", "template_view.php"); ))
In de methode genereren een instantie van de klasse View, de namen van de bestanden van de algemene sjabloon en de weergave met de pagina-inhoud worden doorgegeven.
Naast de indexactie kan de controller natuurlijk nog andere acties bevatten.
We hebben het dossier eerder met een algemeen beeld bekeken. Overweeg een inhoudsbestand main_view.php:
<h1> Welkom!h1>
<p>
<img src = "/ afbeeldingen / office-small.jpg" align = "left">
<a href = "/"> OLOLOSHA-TEAMeen>- een team van eersteklas websitebouwers met jarenlange ervaring in het verzamelen van Mexicaanse maskers, bronzen en stenen beelden uit India en Ceylon, bas-reliëfs en sculpturen gemaakt door de meesters van Equatoriaal Afrika vijf tot zes eeuwen geleden ...p>
Het bevat eenvoudige opmaak zonder PHP-aanroepen.
Om de hoofdpagina weer te geven, kunt u een van de volgende adressen gebruiken:
- methoden van bibliotheken die data-abstractie implementeren. Bijvoorbeeld de methoden van de PEAR MDB2-bibliotheek;
- ORM-methoden;
- methoden voor het werken met NoSQL;
- en etc. Voor de eenvoud zullen we hier geen SQL-query's of ORM-statements gebruiken. In plaats daarvan zullen we echte gegevens emuleren en onmiddellijk een reeks resultaten retourneren.
- Controller_Login waarin een weergave met een formulier voor het invoeren van een login en wachtwoord wordt gegenereerd, na het invullen wordt een authenticatieprocedure uitgevoerd en, indien succesvol, wordt de gebruiker doorgestuurd naar het admin panel.
- Contorller_Admin met een indexactie, waarin wordt gecontroleerd of de gebruiker eerder als beheerder op de site was geautoriseerd (als hij dat was, wordt de weergave van het beheerderspaneel weergegeven) en een uitlogactie om uit te loggen.
- LOGIN_REDIRECT_URL: Vertelt naar welke URL de gebruiker moet worden omgeleid na het inloggen.
- LOGIN_URL: URL om de gebruiker om te leiden om in te loggen (bijvoorbeeld met behulp van een decorateur) Inloggen vereist)
- LOGOUT_URL: URL om gebruiker om te leiden naar uitloggen
- om de huidige sectie in het menu te markeren met behulp van CSS. Het toont ook de gebruikersnaam en inloglink als de gebruiker is geverifieerd, of een inloglink.
Open http://127.0.0.1:8000/account/login/ in uw browser. U zou een invoerpagina moeten zien. Voer een geldige gebruikersnaam en wachtwoord in. U ziet het volgende:
U kunt zien dat het gedeelte Mijn dashboard is gemarkeerd met CSS omdat het een klasse heeft geselecteerd... Aangezien de gebruiker is geverifieerd, wordt de gebruikersnaam weergegeven aan de rechterkant van de kop. Klik De link Uitloggen... U krijgt de volgende pagina te zien:
Op deze pagina kunt u zien dat de gebruiker is uitgelogd en daarom wordt het websitemenu niet meer weergegeven. De link aan de rechterkant van de koptekst toont nu Log in.
Als u een uitlogpagina van de Django-beheerderssite ziet en niet uw eigen uitlogpagina, controleer dan uw INSTALLED_APPS-instellingen en zorg ervoor django.contrib.admin is na rekening... Beide sjablonen bevinden zich in hetzelfde relatieve pad en de Django-sjabloonlader zal de eerste gebruiken die wordt gevonden.
Modelbestand model_portfolio.php zet het in de map modellen. Hier zijn de inhoud:
class Model_Portfolio breidt Model ( openbaar functie get_data () ( return array (array ("Year" => "2012", "Site" => "http://DunkelBeer.ru", "Description" => "Promotiesite voor donker Dunkelbier van de Duitse fabrikant Löwenbraü, geproduceerd in Rusland door brouwerij SAN InBev."), array ("Jaar" => "2012", "Site" => "http://ZopoMobile.ru", "Beschrijving" => "Russischtalige catalogus van Chinese telefoons van het bedrijf Zopo op basis van Android OS en bijbehorende accessoires."), // Te doen); ))
De modelcontrollerklasse bevindt zich in het bestand controller_portfolio.php, hier is de code:
class Controller_Portfolio breidt Controller (
functie __construct () ($ dit -> model = nieuw Model_Portfolio (); $ dit -> weergave = nieuwe weergave (); ) functie action_index () ($ data = $ dit -> model-> get_data (); $ dit -> bekijken-> genereren ("portfolio_view.php", "template_view.php", $ data); ))
In een variabele gegevens de array die door de methode wordt geretourneerd, wordt geschreven gegevens verkrijgen waar we eerder naar keken.
Verder wordt deze variabele doorgegeven als een parameter van de methode genereren, die ook worden doorgegeven: de naam van het bestand met de gemeenschappelijke sjabloon en de naam van het bestand met de weergave met de inhoud van de pagina.
De weergave met de pagina-inhoud bevindt zich in het bestand portfolio_view.php.
Portefeuille
Jaar | Project | Beschrijving | ". $ rij [" Jaar "]." | ". $ rij [" Site "]." | ". $ rij [" Beschrijving "]." | " ; }