Entwicklung in Richtung einer Persistenzschicht

Eines der verwirrendsten Designmuster ist die Persistenz. Die Notwendigkeit, dass eine Anwendung ihren internen Status und ihre Daten beibehält, ist so enorm, dass es wahrscheinlich zehn oder sogar hunderte verschiedener Technologien gibt, um dieses einzelne Problem anzugehen. Leider ist keine Technologie eine magische Kugel. Jede Anwendung und manchmal auch jede Komponente der Anwendung ist auf ihre Weise einzigartig - daher ist eine einzigartige Lösung erforderlich.

In diesem Lernprogramm werde ich Ihnen einige bewährte Vorgehensweisen zeigen, anhand derer Sie feststellen können, welchen Ansatz Sie bei zukünftigen Anwendungen verwenden müssen. Ich werde kurz auf einige wichtige Aspekte und Prinzipien des Designs eingehen, gefolgt von einer detaillierteren Ansicht des Active Record-Entwurfsmusters, kombiniert mit einigen Worten zum Table Data Gateway-Entwurfsmuster.

Natürlich werde ich Ihnen nicht nur die Theorie hinter dem Design beibringen, sondern auch Sie durch ein Beispiel führen, das als Zufallscode beginnt und sich in eine strukturierte Persistenzlösung verwandelt.


Zwei Geschichten einer einzigen Anwendung

Die Datenbank ist für Daten und nicht für Code

Heute kann kein Programmierer dieses archaische System verstehen.

Das älteste Projekt, an dem ich arbeiten muss, begann im Jahr 2000. Damals startete ein Team von Programmierern ein neues Projekt, indem es verschiedene Anforderungen auswertete, über die Workloads nachdachte, mit denen die Anwendung umgehen musste, verschiedene Technologien testete und zu einem Ergebnis kam: alles der PHP-Code der Anwendung mit Ausnahme von index.php Datei sollte sich in einer MySQL-Datenbank befinden. Ihre Entscheidung mag heute unverschämt klingen, war aber vor zwölf Jahren akzeptabel (OK… vielleicht nicht).

Sie erstellten zunächst ihre Basistabellen und dann weitere Tabellen für jede Webseite. Die Lösung funktionierte… eine Zeit lang. Die ursprünglichen Autoren wussten, wie sie es pflegen sollten, aber dann hinterließ jeder Autor einen nach dem anderen - und hinterließ die Codebasis den anderen Neuankömmlingen.

Heute kann kein Programmierer dieses archaische System verstehen. Alles beginnt mit einer MySQL-Abfrage von index.php. Das Ergebnis dieser Abfrage gibt PHP-Code zurück, der noch mehr Abfragen ausführt. Das einfachste Szenario umfasst mindestens fünf Datenbanktabellen. Natürlich gibt es keine Tests oder Spezifikationen. Alles zu ändern ist ein No-Go, und wir müssen einfach das gesamte Modul neu schreiben, wenn etwas schief geht.

Die ursprünglichen Entwickler ignorierten die Tatsache, dass eine Datenbank nur Daten enthalten sollte, keine Geschäftslogik oder Präsentation. Sie mischten PHP- und HTML-Code mit MySQL und ignorierten Designkonzepte auf hohem Niveau.

Der HTML-Code dient nur zur Präsentation und Präsentation

Alle Anwendungen sollten sich auf ein sauberes Design auf hohem Niveau konzentrieren.

Im Laufe der Zeit mussten die neuen Programmierer dem System zusätzliche Funktionen hinzufügen und gleichzeitig alte Fehler beheben. Es gab keine Möglichkeit, MySQL-Tabellen für alles weiterzuverwenden, und alle, die an der Aufrechterhaltung des Codes beteiligt waren, waren sich einig, dass das Design furchtbar fehlerhaft war. Die neuen Programmierer bewerteten die unterschiedlichen Anforderungen, überlegten, welche Workloads die Anwendung bewältigen muss, testeten verschiedene Technologien und kamen zu einem Ergebnis: Sie beschlossen, so viel Code wie möglich in die endgültige Präsentation zu verschieben. Wieder mag diese Entscheidung heute unverschämt klingen, aber es waren Lichtjahre vom vorherigen unerhörten Entwurf.

Die Entwickler nahmen ein Template-Framework an und stützten sich auf die Anwendung, indem sie jedes neue Feature und Modul mit einer neuen Vorlage starteten. Es war einfach; Die Vorlage war beschreibend und sie wussten, wo sie den Code finden können, der eine bestimmte Aufgabe ausführt. So endeten sie mit Vorlagendateien, die die Domain Specific Language (DSL), HTML, PHP und natürlich MySQL-Abfragen enthalten.

Heute schaut und wundert sich mein Team. Es ist ein Wunder, dass viele Ansichten tatsächlich funktionieren. Es kann sehr lange dauern, nur zu bestimmen, wie Informationen aus der Datenbank in die Ansicht gelangen. Wie sein Vorgänger ist alles ein großes Durcheinander!

Diese Entwickler ignorierten die Tatsache, dass eine Ansicht keine Geschäfts- oder Persistenzlogik enthalten sollte. Sie mischten PHP- und HTML-Code mit MySQL und ignorierten Designkonzepte auf hohem Niveau.


High-Level-Anwendungsdesign

Ein Mock ist ein Objekt, das sich wie sein echter Gegenpart verhält, den eigentlichen Code jedoch nicht ausführt.

Alle Anwendungen sollten sich auf ein sauberes Design auf hohem Niveau konzentrieren. Dies ist nicht immer erreichbar, sollte aber hohe Priorität haben. Ein gutes Design auf hohem Niveau hat eine gut isolierte Geschäftslogik. Objekterstellung, -persistenz und -lieferung liegen außerhalb des Kerns und Abhängigkeiten weisen nur auf die Geschäftslogik hin.

Durch die Isolierung der Geschäftslogik ergeben sich große Möglichkeiten, und alles wird zu einem Plugin, wenn die externen Abhängigkeiten immer auf die Geschäftslogik hinweisen. Sie könnten beispielsweise die umfangreiche MySQL-Datenbank mit einer leichtgewichtigen SQLite3-Datenbank austauschen.

  • Stellen Sie sich vor, Sie könnten Ihr aktuelles MVC-Framework verwerfen und durch ein anderes ersetzen, ohne die Geschäftslogik zu berühren.
  • Stellen Sie sich vor, Sie liefern die Ergebnisse Ihrer Anwendung über eine API eines Drittanbieters und nicht über HTTP oder ändern Sie die Technologie von Drittanbietern, die Sie heute verwenden (außer der Programmiersprache natürlich), ohne die Geschäftslogik zu berühren (oder ohne viel Aufwand)..
  • Stellen Sie sich vor, Sie nehmen all diese Änderungen vor, und Ihre Tests würden trotzdem bestehen.

Implementieren einer Arbeitslösung für das Bestehen eines Blogposts

Um die Probleme mit einem schlechten, wenn auch funktionierenden Design besser zu identifizieren, werde ich mit einem einfachen Beispiel eines Blogs beginnen. Während des gesamten Tutorials werde ich einigen testgetriebenen Entwicklungsprinzipien (TDD) folgen und die Tests leicht verständlich machen - auch wenn Sie keine Erfahrung mit TDD haben. Stellen wir uns vor, Sie verwenden ein MVC-Framework. Beim Speichern eines Blogposts hat ein Controller den Namen Blogeintrag führt a aus sparen() Methode. Diese Methode stellt eine Verbindung zu einer SQLite-Datenbank her, um einen Blogbeitrag in der Datenbank zu speichern.

Erstellen wir einen Ordner mit dem Namen Daten in unserem Code-Ordner und navigieren Sie zu diesem Verzeichnis in der Konsole. Erstellen Sie eine Datenbank und eine Tabelle wie folgt:

$ sqlite3 MyBlog SQLite Version 3.7.13 2012-06-11 02:05:22 Geben Sie ".help" für Anweisungen ein Geben Sie SQL-Anweisungen ein, die mit einem ";" sqlite> create table BlogPosts (title varchar (120) Primärschlüssel, Inhaltstext, Published_timestamp-Zeitstempel);

Unsere sparen() Die Methode ruft die Werte aus dem Formular als Array ab $ data:

Klasse BlogPostController Funktion save ($ data) $ dbhandle = neue SQLite3 ('Data / MyBlog'); $ query = 'INSERT INTOPOGPOSTWERTWERTE ("'. $ data ['title']. '", "'. $ data ['content']. '", "'. time (). '') '; $ dbhandle-> exec ($ query); 

Dieser Code funktioniert und Sie können ihn überprüfen, indem Sie ihn von einer anderen Klasse aufrufen und eine vordefinierte übergeben $ data Array wie folgt:

$ this-> object = new BlogPostController; $ data ['title'] = 'Erster Beitragstitel'; $ data ['content'] = 'Einige coole Inhalte für den ersten Beitrag'; $ data ['Published_timestamp'] = Zeit (); $ this-> object-> save ($ data);

Der Inhalt der $ data Variable wurde tatsächlich in der Datenbank gespeichert:

sqlite> select * from BlogPosts; Erster Beitragstitel | Einige coole Inhalte für den ersten Beitrag | 1345665216

Charakterisierungstests

Vererbung ist die stärkste Art von Abhängigkeit.

Ein Charakterisierungstest beschreibt und überprüft das aktuelle Verhalten von bereits vorhandenem Code. Es wird am häufigsten zur Charakterisierung von Legacy-Code verwendet und erleichtert das Refactoring des Codes.

Ein Charakterisierungstest kann ein Modul oder eine Einheit testen oder den gesamten Weg von der Benutzeroberfläche zur Datenbank gehen. es hängt alles davon ab, was wir testen wollen. In unserem Fall sollte ein solcher Test den Controller ausführen und den Inhalt der Datenbank überprüfen. Dies ist kein typischer Einheiten-, Funktions- oder Integrationstest und kann normalerweise keinem dieser Teststufen zugeordnet werden.

Charakterisierungstests sind ein vorübergehendes Sicherheitsnetz, und wir löschen sie normalerweise, nachdem der Code ordnungsgemäß umgestaltet und mit dem Gerät getestet wurde. Hier ist eine Implementierung eines Tests in der Prüfung Mappe:

required_once '… /BlogPostController.php'; Die Klasse BlogPostControllerTest erweitert PHPUnit_Framework_TestCase privates $ object. privates $ dbhandle; function setUp () $ this-> object = new BlogPostController; $ this-> dbhandle = new SQLite3 ('… / Data / MyBlog');  function testSave () $ this-> cleanUPDatabase (); $ data ['title'] = 'Erster Beitragstitel'; $ data ['content'] = 'Einige coole Inhalte für den ersten Beitrag'; $ data ['Published_timestamp'] = Zeit (); $ this-> object-> save ($ data); $ this-> assertEquals ($ data, $ this-> getPostsFromDB ());  private Funktion cleanUPDatabase () $ this-> dbhandle-> exec ('DELETE FROM BlogPosts');  private Funktion getPostsFromDB () $ result = $ this-> dbhandle-> query ('SELECT * FROM BlogPosts'); Rückgabe von $ result-> fetchArray (SQLITE3_ASSOC); 

Dieser Test erstellt ein neues Controller-Objekt und führt dessen aus sparen() Methode. Der Test liest dann die Informationen aus der Datenbank und vergleicht sie mit den vordefinierten $ data [] Array. Wir führen diesen Vergleich mit der $ this-> assertEquals () eine Behauptung, die voraussetzt, dass ihre Parameter gleich sind. Wenn sie unterschiedlich sind, schlägt der Test fehl. Wir reinigen auch die Blogeinträge Datenbanktabelle jedes Mal, wenn wir den Test ausführen.

Legacy-Code ist ungeprüfter Code. - Michael Feathers

Wenn unser Test läuft, reinigen wir den Code ein wenig. Öffnen Sie die Datenbank mit dem gesamten Verzeichnisnamen und verwenden Sie sie sprintf () um die Abfragezeichenfolge zu erstellen. Dies führt zu wesentlich einfacherem Code:

class BlogPostController function save ($ data) $ dbhandle = neue SQLite3 (__ DIR__. '/ Data / MyBlog'); $ query = sprintf ('INSERT INTOPOG' gibt WERTE ("% s", "% s", "% s") ', $ data [' title '], $ data [' content '], time ()); $ dbhandle-> exec ($ query); 

Das Table Data Gateway Pattern

Wir sind uns bewusst, dass unser Code vom Controller in die Geschäftslogik- und Persistenzschicht verschoben werden muss. Das Gateway-Muster kann uns dabei helfen, diesen Weg zu gehen. Hier ist das überarbeitet testSave () Methode:

 Funktion testItCanPersistABlogPost () $ data = array ('title' => 'Erster Beitragstitel', 'content' => 'Einige Inhalte.', 'timestamp' => time ()); $ blogPost = new BlogPost ($ data ['title'], $ data ['content'], $ data ['timestamp']); $ mockedPersistence = $ this-> getMock ('SqlitePost'); $ mockedPersistence-> erwartet ($ this-> once ()) -> Methode ('persist') -> mit ($ blogPost); $ controller = neuer BlogPostController ($ mockedPersistence); $ controller-> save ($ data); 

Dies zeigt, wie wir das nutzen wollen sparen() Methode auf dem Controller. Wir erwarten, dass der Controller eine Methode namens benennt persist ($ blogPostObject) auf dem Gateway-Objekt. Lass uns unser ändern BlogPostController das zu tun:

Klasse BlogPostController privates $ gateway; Funktion __construct (Gateway $ gateway = null) $ this-> gateway = $ gateway? : neuer SqlitePost ();  function save ($ data) $ this-> gateway-> persist (neuer BlogPost ($ data ['title'], $ data ['content'], $ data ['timestamp'])); 

Ein gutes Design auf hohem Niveau hat eine gut isolierte Geschäftslogik.

Nett! Unsere BlogPostController wurde viel einfacher. Es verwendet das Gateway (entweder bereitgestellt oder instanziiert), um die Daten durch Aufrufen seiner Daten zu erhalten fortdauern() Methode. Es ist absolut nicht bekannt, wie die Daten gespeichert werden. Die Persistenzlogik wurde modular.

Im vorherigen Test haben wir den Controller mit einem erstellt spotten Persistenzobjekt, um sicherzustellen, dass beim Ausführen des Tests niemals Daten in die Datenbank geschrieben werden. Im Produktionscode erstellt der Controller ein eigenes persistentes Objekt, um die Daten mithilfe von a zu speichern SqlitePost Objekt. Ein Mock ist ein Objekt, das sich wie sein echter Gegenpart verhält, aber den echten Code nicht ausführt.

Lassen Sie uns jetzt einen Blogbeitrag aus dem Datenspeicher abrufen. Es ist genauso einfach wie das Speichern von Daten. Beachten Sie jedoch, dass ich den Test ein wenig überarbeitet habe.

required_once '… /BlogPostController.php'; required_once '… /BlogPost.php'; required_once '… /SqlitePost.php'; Die Klasse BlogPostControllerTest erweitert PHPUnit_Framework_TestCase private $ mockedPersistence. privater $ Controller; private $ -Daten; function setUp () $ this-> mockedPersistence = $ this-> getMock ('SqlitePost'); $ this-> controller = neuer BlogPostController ($ this-> mockedPersistence); $ this-> data = array ('title' => 'Erster Beitragstitel', 'content' => 'Einige Inhalte.', 'timestamp' => time ());  Funktion testItCanPersistABlogPost () $ blogPost = $ this-> aBlogPost (); $ this-> mockedPersistence-> erwartet ($ this-> once ()) -> Methode ('persist') -> mit ($ blogPost); $ this-> Controller-> save ($ this-> data);  Funktion testItCanRetrievABlogPostByTitle () $ expectedBlogpost = $ this-> aBlogPost (); $ this-> mockedPersistence-> erwartet ($ this-> once ()) -> Methode ('findByTitle') -> mit ($ this-> data ['title']) -> wird ($ this-> returnValue ( $ expectedBlogpost)); $ this-> assertEquals ($ expectedBlogpost, $ this-> Controller-> findByTitle ($ this-> data ['title']));  public function aBlogPost () return new BlogPost ($ this-> data ['title'], $ this-> data ['content'], $ this-> data ['timestamp']); 

Und die Umsetzung in der BlogPostController ist nur eine Anweisung mit einer Anweisung:

 Funktion findByTitle ($ title) return $ this-> gateway-> findByTitle ($ title); 

Ist das nicht cool? Das Blogeintrag Die Klasse ist jetzt Teil der Geschäftslogik (denken Sie an das Designschema auf höherer Ebene von oben). Die UI / MVC wird erstellt Blogeintrag Gegenstände und verwendet Beton Tor Implementierungen, um die Daten zu erhalten. Alle Abhängigkeiten verweisen auf die Geschäftslogik.

Es bleibt nur noch ein Schritt: Erstellen Sie eine konkrete Implementierung von Tor. Folgendes ist das SqlitePost Klasse:

required_once 'Gateway.php'; Die Klasse SqlitePost implementiert das Gateway private $ dbhandle; Funktion __construct ($ dbhandle = null) $ this-> dbhandle = $ dbhandle? : neue SQLite3 (__ DIR__. '/ Data / MyBlog');  public function persist (BlogPost $ blogPost) $ query = sprintf ('INSERT INTO BlogPosts VALUES ("% s", "% s", "% s") ", $ blogPost-> title, $ blogPost-> content, $ blogPost-> timestamp); $ this-> dbhandle-> exec ($ query);  public function findByTitle ($ title) $ SqliteResult = $ this-> dbhandle-> query (sprintf ('SELECT * FROM BlogPosts WHERE title = "% s"', $ title)); $ blogPostAsString = $ SqliteResult-> fetchArray (SQLITE3_ASSOC); neuen BlogPost zurückgeben ($ blogPostAsString ['title'], $ blogPostAsString ['content'], $ blogPostAsString ['timestamp']); 

Hinweis: Der Test für diese Implementierung ist auch im Quellcode verfügbar, aber aufgrund seiner Komplexität und Länge habe ich ihn hier nicht aufgeführt.


Auf dem Weg zum aktiven Aufzeichnungsmuster

Active Record ist eines der umstrittensten Muster. Einige umarmen es (wie Rails und CakePHP) und andere vermeiden es. Viele Objektbezogene Zuordnung (ORM) -Anwendungen verwenden dieses Muster, um Objekte in Tabellen zu speichern. Hier ist das Schema:

Wie Sie sehen, können auf Active Record basierende Objekte persistieren und sich selbst abrufen. Dies wird in der Regel durch eine Erweiterung erreicht ActiveRecordBase Klasse, eine Klasse, die mit der Datenbank arbeiten kann.

Das größte Problem bei Active Record ist das erweitert Abhängigkeit. Wie wir alle wissen, ist Vererbung die stärkste Art von Abhängigkeit, und es ist am besten, sie meistens zu vermeiden.

Bevor wir weiter gehen, sind wir jetzt hier:

Die Gateway-Schnittstelle gehört zur Geschäftslogik und ihre konkreten Implementierungen gehören zur Persistenzschicht. Unsere BlogPostController hat zwei Abhängigkeiten, die beide auf die Geschäftslogik hinweisen: die SqlitePost Gateway und Blogeintrag Klasse.

Aktive Aufzeichnung wählen

Es gibt viele andere Muster, wie das Proxy-Muster, das ist eng mit Persistenz verbunden.

Wenn wir dem Active Record-Muster genau so folgen würden, wie es Martin Fowler in seinem 2003 erschienenen Buch Patterns of Enterprise Application Architecture vorstellt, müssten wir die SQL-Abfragen in das Verzeichnis verschieben Blogeintrag Klasse. Dies hat jedoch das Problem, beide zu verletzen Prinzip der Inversion der Abhängigkeit und das Offenes geschlossenes Prinzip. Das Prinzip der Inversion der Abhängigkeit lautet:

  • High-Level-Module sollten nicht von Low-Level-Modulen abhängen. Beides sollte von Abstraktionen abhängen.
  • Abstraktionen sollten nicht von Details abhängen. Details sollten von Abstraktionen abhängen.

Das Open Closed-Prinzip besagt: Software-Entitäten (Klassen, Module, Funktionen usw.) sollten für die Erweiterung geöffnet sein, für die Modifikation jedoch geschlossen sein. Wir werden einen interessanteren Ansatz verfolgen und das Gateway in unsere Active Record-Lösung integrieren.

Wenn Sie versuchen, dies selbst zu tun, haben Sie wahrscheinlich bereits erkannt, dass das Hinzufügen des Active Record-Musters zum Code die Dinge durcheinander bringt. Aus diesem Grund habe ich die Option gewählt, den Controller und zu deaktivieren SqlitePost Tests, um sich nur auf die Blogeintrag Klasse. Die ersten Schritte sind: machen Blogeintrag Laden Sie sich selbst, indem Sie den Konstruktor als privat festlegen und mit der Gateway-Schnittstelle verbinden. Hier ist die erste Version des BlogPostTest Datei:

required_once '… /BlogPost.php'; required_once '… /InMemoryPost.php'; required_once '… /ActiveRecordBase.php'; Die Klasse BlogPostTest erweitert PHPUnit_Framework_TestCase function testItCanConnectPostToGateway () $ blogPost = BlogPost :: load (); $ blogPost-> setGateway ($ this-> inMemoryPost ()); $ this-> assertEquals ($ blogPost-> getGateway (), $ this-> inMemoryPost ());  Funktion testItCanCreateANewAndEmptyBlogPost () $ blogPost = BlogPost :: load (); $ this-> assertNull ($ blogPost-> title); $ this-> assertNull ($ blogPost-> content); $ this-> assertNull ($ blogPost-> timestamp); $ this-> assertInstanceOf ('Gateway', $ blogPost-> getGateway ());  private Funktion inMemoryPost () return new InMemoryPost (); 

Es wird geprüft, ob ein Blogbeitrag korrekt initialisiert wurde und dass ein Gateway eingerichtet werden kann. Es empfiehlt sich, mehrere Zusicherungen zu verwenden, wenn alle dasselbe Konzept und dieselbe Logik testen.

Unser zweiter Test hat mehrere Behauptungen, aber alle beziehen sich auf das gleiche Konzept von leerer Blogeintrag. Natürlich die Blogeintrag Klasse wurde auch geändert:

Klasse BlogPost privater $ title; privater $ Inhalt; private $ timestamp; privates statisches $ Gateway; private Funktion __construct ($ title = null, $ content = null, $ timestamp = null) $ this-> title = $ title; $ this-> content = $ content; $ this-> timestamp = $ timestamp;  function __get ($ name) return $ this -> $ name;  Funktion setGateway ($ gateway) self :: $ gateway = $ gateway;  function getGateway () return self :: $ gateway;  statische Funktion load () if (! self :: $ gateway) self :: $ gateway = new SqlitePost (); kehre neues selbst zurück; 

Es hat jetzt eine Belastung() Methode, die ein neues Objekt mit einem gültigen Gateway zurückgibt. Ab diesem Zeitpunkt werden wir mit der Implementierung von a fortfahren laden ($ title) Methode zum Erstellen eines neuen Blogeintrag mit Informationen aus der Datenbank. Zum einfachen Testen habe ich eine implementiert InMemoryPost Klasse für Ausdauer. Es speichert nur eine Liste von Objekten im Speicher und gibt Informationen nach Wunsch zurück:

Klasse InMemoryPost implementiert Gateway private $ blogPosts = array (); öffentliche Funktion findByTitle ($ blogPostTitle) return array ('title' => $ this-> blogPosts [$ blogPostTitle] -> titel, 'content' => $ this-> blogPosts [$ blogPostTitle] -> content, 'timestamp' => $ this-> blogPosts [$ blogPostTitle] -> Zeitstempel);  public function persist (BlogPost $ blogPostObject) $ this-> blogPosts [$ blogPostObject-> title] = $ blogPostObject; 

Als Nächstes erkannte ich, dass die ursprüngliche Idee der Verbindung von Blogeintrag zu einem Gateway über eine separate Methode war nutzlos. Also habe ich die Tests entsprechend modifiziert:

Klasse BlogPostTest erweitert PHPUnit_Framework_TestCase Funktion testItCanCreateANewAndEmptyBlogPost () $ blogPost = BlogPost :: load (); $ this-> assertNull ($ blogPost-> title); $ this-> assertNull ($ blogPost-> content); $ this-> assertNull ($ blogPost-> timestamp);  Funktion testItCanLoadABlogPostByTitle () $ gateway = $ this-> inMemoryPost (); $ aBlogPosWithData = $ this-> aBlogPostWithData ($ gateway); $ gateway-> persist ($ aBlogPosWithData); $ this-> assertEquals ($ aBlogPosWithData, BlogPost :: load ('some_title', null, null, $ gateway));  private Funktion inMemoryPost () return new InMemoryPost ();  private Funktion aBlogPostWithData ($ gateway = null) return BlogPost :: load ('some_title', 'some content', '123', $ gateway); 

Wie Sie sehen, habe ich den Weg radikal geändert Blogeintrag wird eingesetzt.

Klasse BlogPost privater $ title; privater $ Inhalt; private $ timestamp; private Funktion __construct ($ title = null, $ content = null, $ timestamp = null) $ this-> title = $ title; $ this-> content = $ content; $ this-> timestamp = $ timestamp;  function __get ($ name) return $ this -> $ name;  statische Funktion laden ($ title = null, $ content = null, $ timestamp = null, $ gateway = null) $ gateway = $ gateway? : neuer SqlitePost (); if (! $ content) $ postArray = $ gateway-> findByTitle ($ title); if ($ postArray) neues Eigenes zurückgeben ($ postArray ['title'], $ postArray ['content'], $ postArray ['timestamp']);  neues Eigenes zurückgeben ($ title, $ content, $ timestamp); 

Das Belastung() Methode überprüft das $ content Parameter für einen Wert und erstellt einen neuen Blogeintrag wenn ein Wert angegeben wurde. Wenn nicht, versucht die Methode, einen Blogbeitrag mit dem angegebenen Titel zu finden. Wenn ein Beitrag gefunden wird, wird er zurückgegeben. Wenn keine vorhanden ist, erstellt die Methode ein leeres Blogeintrag Objekt.

Damit dieser Code funktioniert, müssen wir auch die Funktionsweise des Gateways ändern. Unsere Implementierung muss ein assoziatives Array mit zurückgeben Titel, Inhalt, und Zeitstempel Elemente anstelle des Objekts selbst. Dies ist eine Konvention, die ich gewählt habe. Andere Varianten, wie beispielsweise ein einfaches Array, könnten attraktiver sein. Hier sind die Modifikationen in SqlitePostTest:

 Funktion testItCanRetrieveABlogPostByItsTitle () […] // Wir erwarten ein Array anstelle eines Objekts $ this-> assertEquals ($ this-> blogPostAsArray, $ gateway-> findByTitle ($ this-> blogPostAsArray ['title']));  private Funktion aBlogPostWithValues ​​() // Wir verwenden statisches Laden anstelle des Konstruktoraufrufs return $ blogPost = blogPost :: load ($ this-> blogPostAsArray ['title'], $ this-> blogPostAsArray ['content'], $ this -> blogPostAsArray ['Zeitstempel']); 

Und die Implementierungsänderungen sind:

 öffentliche Funktion findByTitle ($ title) $ SqliteResult = $ this-> dbhandle-> query (sprintf ('SELECT * FROM BlogPosts WHERE title = "% s"', $ title)); // das Ergebnis direkt zurückgeben, nicht das Objekt erstellen return $ SqliteResult-> fetchArray (SQLITE3_ASSOC); 

Wir sind fast fertig. Füge hinzu ein fortdauern() Methode zum Blogeintrag und rufen Sie alle neu implementierten Methoden vom Controller auf. Hier ist der fortdauern() Methode, die nur das Gateway verwendet fortdauern() Methode:

 private Funktion persist () $ this-> gateway-> persist ($ this); 

Und der Controller:

Klasse BlogPostController Funktion save ($ data) $ blogPost = BlogPost :: load ($ data ['title'], $ data ['content'], $ data ['timestamp']); $ blogPost-> persist ();  function findByTitle ($ title) return BlogPost :: load ($ title); 

Das BlogPostController wurde so einfach, dass ich alle Tests entfernte. Es nennt einfach die Blogeintrag Objekt ist fortdauern() Methode. Natürlich sollten Sie Tests hinzufügen, wenn und wann mehr Code im Controller vorhanden ist. Der Code-Download enthält noch eine Testdatei für die BlogPostController, aber sein Inhalt wird kommentiert.


Fazit

Dies ist nur die Spitze des Eisbergs.

Sie haben zwei verschiedene Persistenzimplementierungen gesehen: die Tor und Aktiver Rekord Muster. Ab diesem Punkt können Sie ein implementieren ActiveRecordBase abstrakte Klasse für alle Klassen, die Ausdauer benötigen. Diese abstrakte Klasse kann verschiedene Gateways verwenden, um Daten zu erhalten, und jede Implementierung kann sogar eine andere Logik verwenden, um Ihre Anforderungen zu erfüllen.

Dies ist aber nur die Spitze des Eisbergs. Es gibt viele andere Muster, wie das Proxy-Muster, die eng mit Persistenz zusammenhängen; Jedes Muster funktioniert für eine bestimmte Situation. Ich empfehle, dass Sie immer zuerst die einfachste Lösung implementieren und dann ein anderes Muster implementieren, wenn sich Ihre Anforderungen ändern.

Ich hoffe, Ihnen hat dieses Tutorial gefallen, und ich freue mich sehr auf Ihre Meinungen und alternativen Implementierungen zu meiner Lösung in den folgenden Kommentaren.