Das Repository-Entwurfsmuster

Das von Eric Evens in seinem Buch "Domain Driven Design" definierte Repository-Entwurfsmuster ist eines der nützlichsten und am häufigsten anwendbaren Entwurfsmuster, die je erfunden wurden. Jede Anwendung muss mit Persistenz und mit einer Art Liste von Elementen arbeiten. Dies können Benutzer, Produkte, Netzwerke, Festplatten oder was auch immer Ihre Anwendung ist. Wenn Sie beispielsweise ein Blog haben, müssen Sie sich mit Listen von Blogbeiträgen und Kommentaren beschäftigen. Das Problem, das all diese Listenverwaltungslogiken gemeinsam haben, ist die Verknüpfung von Geschäftslogik, Fabriken und Persistenz.


Das Fabrik-Design-Muster

Wie im einleitenden Absatz erwähnt, verbindet ein Repository Factories mit Gateways (Persistenz). Dies sind auch Designmuster, und wenn Sie mit ihnen nicht vertraut sind, wird dieser Abschnitt das Thema beleuchten.

Eine Fabrik ist ein einfaches Entwurfsmuster, das eine bequeme Methode zum Erstellen von Objekten definiert. Es ist eine Klasse oder eine Gruppe von Klassen, die für die Erstellung der Objekte verantwortlich sind, die unsere Geschäftslogik benötigt. Eine Fabrik hat traditionell eine Methode namens "machen()" und er wird wissen, wie er alle Informationen benötigt, die zum Erstellen eines Objekts erforderlich sind, das Objekt selbst erstellt und ein einsatzbereites Objekt an die Geschäftslogik zurückgibt.

Hier ist ein wenig mehr über das Factory Pattern in einem älteren Nettuts + Tutorial: Ein Leitfaden für Anfänger zu Design Patterns. Wenn Sie eine tiefere Sicht auf das Factory-Muster bevorzugen, sehen Sie sich das erste Design-Muster im Agile Design Patterns-Kurs an, den wir für Tuts anbieten+.


Das Gateway-Muster

Auch als "Table Data Gateway" bekannt ist ein einfaches Muster, das die Verbindung zwischen der Geschäftslogik und der Datenbank selbst bietet. Seine Hauptaufgabe besteht darin, die Abfragen in der Datenbank auszuführen und die abgerufenen Daten in einer für die Programmiersprache typischen Datenstruktur bereitzustellen (wie ein Array in PHP). Diese Daten werden dann normalerweise im PHP-Code gefiltert und modifiziert, damit wir die Informationen und Variablen erhalten können, die für die Erstellung unserer Objekte erforderlich sind. Diese Informationen müssen dann an die Fabriken weitergeleitet werden.

Das Gateway-Entwurfsmuster wird in einem Nettuts + -Tutorial über die Entwicklung einer Persistenzschicht ausführlich erklärt und veranschaulicht. Im selben Agile Design Patterns-Kurs befasst sich auch die zweite Lektion für Designmuster mit diesem Thema.


Die Probleme, die wir lösen müssen

Vervielfältigung durch Datenbehandlung

Es mag auf den ersten Blick nicht offensichtlich sein, aber die Verbindung von Gateways mit Factories kann zu einer erheblichen Verdopplung führen. Jede Software von beträchtlicher Größe muss die gleichen Objekte an verschiedenen Orten erstellen. An jedem Ort müssen Sie das Gateway verwenden, um eine Reihe von Rohdaten abzurufen, diese zu filtern und zu bearbeiten, damit sie an die Factories gesendet werden können. Von all diesen Orten aus rufen Sie dieselben Fabriken mit denselben Datenstrukturen, aber offensichtlich mit unterschiedlichen Daten auf. Ihre Objekte werden von den Fabriken erstellt und Ihnen zur Verfügung gestellt. Dies führt unweigerlich zu einer erheblichen Verdopplung in der Zeit. Die Vervielfältigung wird sich auf entfernte Klassen oder Module erstrecken und wird schwer zu bemerken und zu beheben sein.

Duplizierung durch logische Reimplementierung der Datenwiederherstellung

Ein anderes Problem ist, wie wir die Fragen formulieren, die wir mit Hilfe der Gateways stellen müssen. Jedes Mal, wenn wir Informationen vom Gateway benötigen, müssen wir darüber nachdenken, was genau benötigt wird. Benötigen wir alle Daten zu einem Thema? Benötigen wir nur bestimmte Informationen? Möchten Sie eine bestimmte Gruppe aus der Datenbank abrufen und in unserer Programmiersprache sortieren oder filtern? All diese Fragen müssen jedes Mal beantwortet werden, wenn wir über unser Gateway Informationen von der Persistenzschicht abrufen. Jedes Mal, wenn wir dies tun, müssen wir eine Lösung finden. Mit der Zeit, wenn unsere Anwendung wächst, werden wir an verschiedenen Stellen unserer Anwendung mit den gleichen Dilemmas konfrontiert. Wir werden versehentlich etwas andere Lösungen für die gleichen Probleme finden. Dies erfordert nicht nur zusätzliche Zeit und Mühe, sondern führt auch zu einer subtilen, meist sehr schwer erkennbaren Vervielfältigung. Dies ist die gefährlichste Art der Vervielfältigung.

Duplizierung durch Wiederholung der Logik der Datenpersistenz

In den letzten beiden Absätzen haben wir nur über das Abrufen von Daten gesprochen. Das Gateway ist jedoch bidirektional. Unsere Geschäftslogik ist bidirektional. Wir müssen unsere Objekte irgendwie beharren. Dies führt wiederum zu vielen Wiederholungen, wenn wir diese Logik nach Bedarf in verschiedenen Modulen und Klassen unserer Anwendung implementieren möchten.


Die Hauptkonzepte

Repository für Datenabruf

Ein Repository kann auf zwei Arten funktionieren: Datenabruf und Datenpersistenz.


Beim Abrufen von Objekten von Persistenz wird ein Repository mit einer benutzerdefinierten Abfrage aufgerufen. Diese Abfrage kann eine bestimmte Methode nach Namen oder eine allgemeinere Methode mit Parametern sein. Das Repository ist dafür verantwortlich, diese Abfragemethoden bereitzustellen und zu implementieren. Wenn eine solche Methode aufgerufen wird, kontaktiert das Repository das Gateway, um die Rohdaten von der Persistenz abzurufen. Das Gateway stellt Rohobjektdaten bereit (wie ein Array mit Werten). Anschließend nimmt das Repository diese Daten an, führt die erforderlichen Umwandlungen durch und ruft die entsprechenden Factory-Methoden auf. Die Factories stellen die Objekte bereit, die mit den vom Repository bereitgestellten Daten erstellt wurden. Das Repository sammelt diese Objekte und gibt sie als Gruppe von Objekten zurück (wie ein Array von Objekten oder ein Sammlungsobjekt, wie in der Lektion "Zusammengesetztes Muster" des Kurses "Agile Design Patterns" definiert)..

Repository für Datenpersistenz

Die zweite Möglichkeit, mit der ein Repository arbeiten kann, besteht darin, die Logik bereitzustellen, die erforderlich ist, um die Informationen aus einem Objekt zu extrahieren und diese zu speichern. Dies kann so einfach sein, dass das Objekt serialisiert und die serialisierten Daten an das Gateway gesendet werden, um es zu speichern, oder so komplex wie das Erstellen von Informationsfeldern mit allen Feldern und dem Status eines Objekts.


Bei der Speicherung von Informationen ist die Client-Klasse diejenige, die direkt mit der Factory kommuniziert. Stellen Sie sich ein Szenario vor, wenn ein neuer Kommentar in einem Blogeintrag gepostet wird. Ein Comment-Objekt wird von unserer Geschäftslogik (der Client-Klasse) erstellt und dann an das Repository gesendet, um dort dauerhaft gespeichert zu werden. Das Repository speichert die Objekte mithilfe des Gateways und speichert sie optional in einer lokalen Liste im Arbeitsspeicher. Daten müssen transformiert werden, da es nur selten der Fall ist, dass reale Objekte direkt in einem Persistenzsystem gespeichert werden können.


Die Punkte verbinden

Die Abbildung unten zeigt eine übergeordnete Ansicht zur Integration des Repositorys zwischen den Factories, dem Gateway und dem Client.


Im Zentrum des Schemas steht unser Repository. Auf der linken Seite befindet sich eine Schnittstelle für das Gateway, eine Implementierung und die Persistenz. Auf der rechten Seite befindet sich eine Schnittstelle für die Factory und eine Factory-Implementierung. Zum Schluss noch die Client-Klasse.

Wie aus der Richtung der Pfeile gesehen werden kann, sind die Abhängigkeiten umgekehrt. Das Repository hängt nur von den abstrakten Schnittstellen für Factories und Gateways ab. Das Gateway ist abhängig von der Benutzeroberfläche und der von ihm angebotenen Persistenz. Die Fabrik hängt nur von ihrer Schnittstelle ab. Der Client ist vom Repository abhängig, was akzeptabel ist, da das Repository tendenziell weniger konkret ist als der Client.


In der Perspektive betrachtet respektiert der Absatz oben unsere Architektur auf hoher Ebene und die Richtung der Abhängigkeiten, die wir erreichen wollen.


Verwalten von Kommentaren zu Blogbeiträgen mit einem Repository

Nun, da wir die Theorie gesehen haben, ist es Zeit für ein praktisches Beispiel. Stellen Sie sich vor, wir haben ein Blog, in dem wir Post-Objekte und Comment-Objekte haben. Kommentare gehören zu Posts und wir müssen einen Weg finden, sie zu beharren und abzurufen.

Der Kommentar

Wir beginnen mit einem Test, der uns zwingt, darüber nachzudenken, was unser Kommentarobjekt enthalten soll.

class RepositoryTest erweitert PHPUnit_Framework_TestCase function testACommentHasAllItsComposingParts () $ postId = 1; $ commentAuthor = "Joe"; $ commentAuthorEmail = "[email protected]"; $ commentSubject = "Joe hat eine Meinung zum Repository-Muster"; $ commentBody = "Ich denke, es ist eine gute Idee, das Repository-Muster zu verwenden, um Objekte zu persistieren und abzurufen."; $ comment = neuer Kommentar ($ postId, $ commentAuthor, $ commentAuthorEmail, $ commentSubject, $ commentBody); 

Auf den ersten Blick ist ein Kommentar nur ein Datenobjekt. Es hat möglicherweise keine Funktionalität, aber das hängt vom Kontext unserer Anwendung ab. Nehmen Sie für dieses Beispiel einfach an, dass es sich um ein einfaches Datenobjekt handelt. Konstruiert mit einer Reihe von Variablen.

Klasse Kommentar 

Wenn Sie eine leere Klasse erstellen und diese im Test anfordern, wird sie bestanden.

required_once '… /Comment.php'; Klasse RepositoryTest erweitert PHPUnit_Framework_TestCase […]

Aber das ist alles andere als perfekt. Unser Test testet noch nichts. Lassen Sie uns uns zwingen, alle Getter auf die Comment-Klasse zu schreiben.

Funktion testACommentsHasAllItsComposingParts () $ postId = 1; $ commentAuthor = "Joe"; $ commentAuthorEmail = "[email protected]"; $ commentSubject = "Joe hat eine Meinung zum Repository-Muster"; $ commentBody = "Ich denke, es ist eine gute Idee, das Repository-Muster zu verwenden, um Objekte zu persistieren und abzurufen."; $ comment = neuer Kommentar ($ postId, $ commentAuthor, $ commentAuthorEmail, $ commentSubject, $ commentBody); $ this-> assertEquals ($ postId, $ comment-> getPostId ()); $ this-> assertEquals ($ commentAuthor, $ comment-> getAuthor ()); $ this-> assertEquals ($ commentAuthorEmail, $ comment-> getAuthorEmail ()); $ this-> assertEquals ($ commentSubject, $ comment-> getSubject ()); $ this-> assertEquals ($ commentBody, $ comment-> getBody ()); 

Um die Dauer des Tutorials zu kontrollieren, habe ich alle Assertions gleichzeitig geschrieben, und wir werden sie auch gleich implementieren. Nehmen Sie sie im wirklichen Leben nacheinander.

 Klasse Kommentar private $ postId; privater $ Autor; private $ authorEmail; privates $ subject; private $ body; Funktion __construct ($ postId, $ author, $ authorEmail, $ subject, $ body) $ this-> postId = $ postId; $ this-> author = $ author; $ this-> authorEmail = $ authorEmail; $ this-> subject = $ subject; $ this-> body = $ body;  public function getPostId () return $ this-> postId;  public function getAuthor () return $ this-> author;  public function getAuthorEmail () return $ this-> authorEmail;  public function getSubject () return $ this-> subject;  public function getBody () return $ this-> body; 

Abgesehen von der Liste der privaten Variablen wurde der Rest des Codes von meiner IDE, NetBeans, generiert. Das Testen des automatisch generierten Codes kann daher manchmal ein wenig Overhead verursachen. Wenn Sie diese Zeilen nicht selbst schreiben, können Sie sie direkt ausführen und sich nicht mit Tests für Setter und Konstruktoren beschäftigen. Trotzdem hat uns der Test geholfen, unsere Ideen besser offen zu legen und zu dokumentieren, was unsere Kommentarklasse enthalten wird.

Wir können diese Testmethoden und Testklassen auch als unsere "Client" -Klassen aus den Schemata betrachten.


Unser Tor zur Persistenz

Um dieses Beispiel so einfach wie möglich zu halten, implementieren wir nur eine InMemoryPersistence, um unsere Existenz nicht mit Dateisystemen oder Datenbanken zu verkomplizieren.

required_once '… /InMemoryPersistence.php'; class InMemoryPersistenceTest erweitert PHPUnit_Framework_TestCase Funktion testItCanPerisistAndRetrieveASingleDataArray () $ data = array ('data'); $ persistence = neue InMemoryPersistence (); $ persistence-> persist ($ data); $ this-> assertEquals ($ data, $ persistence-> retrieve (0)); 

Wie üblich beginnen wir mit dem einfachsten Test, der möglicherweise fehlschlagen könnte, und zwingt uns auch, Code zu schreiben. Dieser Test erzeugt ein neues InMemoryPersistence object und versucht, ein Array namens Daten.

required_once __DIR__. '/Persistence.php'; class InMemoryPersistence implementiert Persistence private $ data = array (); Funktion persist ($ data) $ this-> data = $ data;  Funktion retrieve ($ id) return $ this-> data; 

Der einfachste Code, um es passieren zu lassen, besteht darin, den eingehenden Code zu behalten $ data in einer privaten Variable und geben Sie es in der abrufen Methode. Der Code, wie er jetzt ist, kümmert sich nicht um die Einsendung $ id Variable. Es ist die einfachste Sache, die den Test bestehen könnte. Wir haben uns auch die Freiheit genommen, eine so genannte Schnittstelle einzuführen und zu implementieren Beharrlichkeit.

Schnittstelle Persistence Funktion persist ($ data); Funktion abrufen ($ ids); 

Diese Schnittstelle definiert die beiden Methoden, die ein Gateway implementieren muss. Fortdauern und abrufen. Wie Sie wahrscheinlich schon erraten haben, ist unser Gateway unser InMemoryPersistence Klasse und unsere physische Persistenz ist die private Variable, die unsere Daten im Speicher hält. Kommen wir aber noch einmal zur Implementierung dieser Gedächtnispersistenz.

Funktion testItCanPerisistSeveralElementsAndRetrieveAnyOfThem () $ data1 = Array ('data1'); $ data2 = Array ('data2'); $ persistence = neue InMemoryPersistence (); $ persistence-> persist ($ data1); $ persistence-> persist ($ data2); $ this-> assertEquals ($ data1, $ persistence-> retrieve (0)); $ this-> assertEquals ($ data2, $ persistence-> retrieve (1)); 

Wir haben einen weiteren Test hinzugefügt. In diesem Fall bleiben zwei verschiedene Datenfelder erhalten. Wir erwarten, dass wir sie einzeln abrufen können.

required_once __DIR__. '/Persistence.php'; class InMemoryPersistence implementiert Persistence private $ data = array (); Funktion persist ($ data) $ this-> data [] = $ data;  function retrieve ($ id) return $ this-> data [$ id]; 

Der Test zwang uns, unseren Code leicht zu ändern. Wir müssen jetzt Daten zu unserem Array hinzufügen und nicht einfach durch die Daten ersetzen, die in gesendet werden persistiert (). Wir müssen auch das berücksichtigen $ id Parameter und geben Sie das Element an diesem Index zurück.

Das reicht für uns InMemoryPersistence. Bei Bedarf können wir es später ändern.


Unsere Fabrik

Wir haben einen Client (unsere Tests), eine Persistenz mit einem Gateway und Comment-Objekte, die bestehen bleiben. Das nächste, was fehlt, ist unsere Fabrik.

Wir haben unsere Codierung mit einem RepositoryTest Datei. Dieser Test hat jedoch tatsächlich eine erstellt Kommentar Objekt. Jetzt müssen wir Tests erstellen, um zu überprüfen, ob unsere Fabrik erstellt werden kann Kommentar Objekte. Es hat den Anschein, als hätten wir einen Irrtum begangen, und unser Test ist eher ein Test für unsere bevorstehende Fabrik als für unser Repository. Wir können es in eine andere Testdatei verschieben, CommentFactoryTest.

required_once '… /Comment.php'; class CommentFactoryTest erweitert PHPUnit_Framework_TestCase Funktion testACommentsHasAllItsComposingParts () $ postId = 1; $ commentAuthor = "Joe"; $ commentAuthorEmail = "[email protected]"; $ commentSubject = "Joe hat eine Meinung zum Repository-Muster"; $ commentBody = "Ich denke, es ist eine gute Idee, das Repository-Muster zu verwenden, um Objekte zu persistieren und abzurufen."; $ comment = neuer Kommentar ($ postId, $ commentAuthor, $ commentAuthorEmail, $ commentSubject, $ commentBody); $ this-> assertEquals ($ postId, $ comment-> getPostId ()); $ this-> assertEquals ($ commentAuthor, $ comment-> getAuthor ()); $ this-> assertEquals ($ commentAuthorEmail, $ comment-> getAuthorEmail ()); $ this-> assertEquals ($ commentSubject, $ comment-> getSubject ()); $ this-> assertEquals ($ commentBody, $ comment-> getBody ()); 

Nun ist dieser Test offensichtlich bestanden. Und obwohl es sich um einen korrekten Test handelt, sollten wir darüber nachdenken, ihn zu ändern. Wir möchten eine erstellen Fabrik Objekt, übergeben Sie ein Array und bitten Sie um die Erstellung eines Kommentar für uns.

required_once '… /CommentFactory.php'; class CommentFactoryTest erweitert PHPUnit_Framework_TestCase Funktion testACommentsHasAllItsComposingParts () $ postId = 1; $ commentAuthor = "Joe"; $ commentAuthorEmail = "[email protected]"; $ commentSubject = "Joe hat eine Meinung zum Repository-Muster"; $ commentBody = "Ich denke, es ist eine gute Idee, das Repository-Muster zu verwenden, um Objekte zu persistieren und abzurufen."; $ commentData = Array ($ postId, $ commentAuthor, $ commentAuthorEmail, $ commentSubject, $ commentBody); $ comment = (new CommentFactory ()) -> make ($ commentData); $ this-> assertEquals ($ postId, $ comment-> getPostId ()); $ this-> assertEquals ($ commentAuthor, $ comment-> getAuthor ()); $ this-> assertEquals ($ commentAuthorEmail, $ comment-> getAuthorEmail ()); $ this-> assertEquals ($ commentSubject, $ comment-> getSubject ()); $ this-> assertEquals ($ commentBody, $ comment-> getBody ()); 

Wir sollten unsere Klassen niemals anhand des von ihnen implementierten Entwurfsmusters benennen, sondern Fabrik und Repository repräsentieren mehr als nur das Designmuster selbst. Ich persönlich habe nichts dagegen, diese beiden Wörter in die Namen unserer Klasse aufzunehmen. Trotzdem empfehle und respektiere ich immer noch das Konzept, unsere Klassen nicht nach den Entwurfsmustern zu benennen, die wir für die restlichen Muster verwenden.

Dieser Test unterscheidet sich nur geringfügig vom vorherigen, aber er schlägt fehl. Es wird versucht, eine KommentarFabrik Objekt. Diese Klasse existiert noch nicht. Wir versuchen auch a anzurufen machen() Methode darauf mit einem Array, das alle Informationen eines Kommentars als Array enthält. Diese Methode ist in definiert Fabrik Schnittstelle.

Schnittstelle Factory Funktion make ($ data); 

Dies ist sehr häufig Fabrik Schnittstelle. Sie definierte die einzige erforderliche Methode für eine Fabrik, die Methode, mit der die gewünschten Objekte erstellt werden.

required_once __DIR__. "/Factory.php"; required_once __DIR__. '/Comment.php'; Klasse CommentFactory implementiert Factory Funktion make ($ components) return new Kommentar ($ components [0], $ components [1], $ components [2], $ components [3], $ components [4]); 

Und KommentarFabrik implementiert die Fabrik Schnittstelle erfolgreich, indem Sie die $ Komponenten Parameter in seiner machen() erstellt eine neue Methode und gibt sie zurück Kommentar Objekt mit den Informationen von dort.

Wir werden unsere Persistenz- und Objekterstellungslogik so einfach wie möglich halten. In diesem Lernprogramm können wir die Fehlerbehandlung, Validierung und das Auslösen von Ausnahmen sicher ignorieren. Wir werden hier mit der Implementierung von Persistenz und Objekterstellung aufhören.


Verwenden eines Repository zum Behalten von Kommentaren

Wie wir oben gesehen haben, können wir ein Repository auf zwei Arten verwenden. Informationen aus der Persistenz abrufen und Informationen auf der Persistenzschicht beibehalten. Bei der Verwendung von TDD ist es meistens einfacher, mit dem Sichern (dauerhaften) Teil der Logik zu beginnen und diese vorhandene Implementierung dann zum Testen des Datenabrufs zu verwenden.

required_once '… /… /… /vendor/autoload.php'; required_once '… /CommentRepository.php'; required_once '… /CommentFactory.php'; class RepositoryTest erweitert PHPUnit_Framework_TestCase protected function tearDown () \ Mockery :: close ();  Funktion testItCallsThePersistenceWhenAddingAComment () $ persistanceGateway = \ Mockery :: mock ('Persistence'); $ commentRepository = new CommentRepository ($ persistanceGateway); $ commentData = Array (1, 'x', 'x', 'x', 'x'); $ comment = (new CommentFactory ()) -> make ($ commentData); $ persistanceGateway-> sollteReceive ('persist') -> einmal () -> mit ($ commentData); $ commentRepository-> add ($ comment); 

Wir verwenden Mockery, um unsere Beharrlichkeit zu verspotten und das gespielte Objekt in das Repository zu injizieren. Dann rufen wir an hinzufügen() auf dem Repository. Diese Methode hat einen Parameter vom Typ Kommentar. Wir erwarten, dass die Persistenz mit einem Datenfeld ähnlich wie aufgerufen wird $ commentData.

required_once __DIR__. '/InMemoryPersistence.php'; class CommentRepository private $ persistence; Funktion __construct (Persistenz $ persistence = null) $ this-> persistence = $ persistence? : neue InMemoryPersistence ();  Funktion add (Kommentar $ comment) $ this-> persistence-> persist (array ($ comment-> getPostId (), $ comment-> getAuthor (), $ comment-> getAuthorEmail (), $ comment-> getSubject ( ), $ comment-> getBody ())); 

Wie Sie sehen können, die hinzufügen() Methode ist ziemlich schlau. Es kapselt das Wissen darüber, wie ein PHP-Objekt in ein einfaches Array umgewandelt wird, das von der Persistenz verwendet werden kann. Denken Sie daran, dass unser Persistenz-Gateway normalerweise ein allgemeines Objekt für alle unsere Daten ist. Es kann und wird alle Daten unserer Anwendung beibehalten, sodass das Senden von Objekten zu viel Aufwand bedeutet: sowohl die Konvertierung als auch die effektive Persistenz.

Wenn du einen hast InMemoryPersistence Klasse wie wir, ist es sehr schnell. Wir können es als Alternative zum Verspotten des Gateways verwenden.

Funktion testAPersistedCommentCanBeRetrievedFromTheGateway () $ persistanceGateway = new InMemoryPersistence (); $ commentRepository = new CommentRepository ($ persistanceGateway); $ commentData = Array (1, 'x', 'x', 'x', 'x'); $ comment = (new CommentFactory ()) -> make ($ commentData); $ commentRepository-> add ($ comment); $ this-> assertEquals ($ commentData, $ persistanceGateway-> retrieve (0)); 

Wenn Sie nicht über eine In-Memory-Implementierung Ihrer Persistenz verfügen, ist das Spotting natürlich der einzig vernünftige Weg. Andernfalls ist Ihr Test zu langsam, um praktisch zu sein.

function testItCanAddMultipleCommentsAtOnce () $ persistanceGateway = \ Mockery :: mock ('Persistence'); $ commentRepository = new CommentRepository ($ persistanceGateway); $ commentData1 = Array (1, 'x', 'x', 'x', 'x'); $ comment1 = (new CommentFactory ()) -> make ($ commentData1); $ commentData2 = Array (2, 'y', 'y', 'y', 'y'); $ comment2 = (new CommentFactory ()) -> make ($ commentData2); $ persistanceGateway-> sollteReceive ('persist') -> einmal () -> mit ($ commentData1); $ persistanceGateway-> sollteReceive ('persist') -> einmal () -> mit ($ commentData2); $ commentRepository-> add (Array ($ comment1, $ comment2)); 

Unser nächster logischer Schritt ist die Implementierung einer Möglichkeit, mehrere Kommentare gleichzeitig hinzuzufügen. Für Ihr Projekt ist diese Funktion möglicherweise nicht erforderlich, und das Muster ist nicht erforderlich. Tatsächlich sagt das Repository-Muster nur aus, dass es eine benutzerdefinierte Abfrage- und Persistenzsprache für unsere Geschäftslogik bereitstellt. Wenn also unsere Busch-Logik das Bedürfnis hat, mehrere Kommentare auf einmal hinzuzufügen, ist das Repository der Ort, an dem sich die Logik befinden sollte.

Funktion add ($ commentData) if (is_array ($ commentData)) foreach ($ commentData als $ comment) $ this-> persistence-> persist (array ($ comment-> getPostId (), $ comment-> getAuthor (), $ comment-> getAuthorEmail (), $ comment-> getSubject (), $ comment-> getBody ())); else $ this-> persistence-> persist (array ($ commentData-> getPostId (), $ commentData-> getAuthor (), $ commentData-> getAuthorEmail (), $ commentData-> getSubject (), $ commentData-> getBody ( ))); 

Die einfachste Möglichkeit, den Test zu bestehen, besteht darin, einfach zu überprüfen, ob der Parameter, den wir erhalten, ein Array ist oder nicht. Wenn es sich um ein Array handelt, durchlaufen wir jedes Element und rufen die Persistenz mit dem Array auf, das wir aus einem einzigen Element generieren Kommentar Objekt. Und obwohl dieser Code syntaktisch korrekt ist und den Test bestanden hat, führt er eine leichte Duplizierung ein, die wir leicht loswerden können.

Funktion add ($ commentData) if (is_array ($ commentData)) foreach ($ commentData als $ comment) $ this-> addOne ($ comment); else $ this-> addOne ($ commentData);  private Funktion addOne (Kommentar $ comment) $ this-> persistence-> persist (array ($ comment-> getPostId (), $ comment-> getAuthor (), $ comment-> getAuthorEmail (), $ comment-> getSubject) (), $ comment-> getBody ())); 

Wenn alle Tests grün sind, ist es immer Zeit für das Refactoring, bevor wir mit dem nächsten fehlgeschlagenen Test fortfahren. Und genau das haben wir mit gemacht hinzufügen() Methode. Wir haben die Hinzufügung eines einzelnen Kommentars zu einer privaten Methode extrahiert und von zwei verschiedenen Stellen in unserer Öffentlichkeit aufgerufen hinzufügen() Methode. Dies reduzierte nicht nur die Duplizierung, sondern eröffnete auch die Möglichkeit, die füge eins hinzu() Methode public und lässt die Geschäftslogik entscheiden, ob sie einen oder mehrere Kommentare gleichzeitig hinzufügen möchte. Dies würde zu einer anderen Implementierung unseres Repository führen, mit einem füge eins hinzu() und ein anderer addMany () Methoden. Dies wäre eine absolut legitime Implementierung des Repository Pattern.


Kommentare mit unserem Repository abrufen

Ein Repository stellt eine benutzerdefinierte Abfragesprache für die Geschäftslogik bereit. Die Namen und Funktionalitäten der Abfragemethoden eines Repositorys entsprechen also den Anforderungen der Geschäftslogik. Sie erstellen Ihr Repository, während Sie Ihre Geschäftslogik erstellen, da Sie eine andere benutzerdefinierte Abfragemethode benötigen. Es gibt jedoch mindestens eine oder zwei Methoden, die Sie in fast jedem Repository finden.

function testItCanFindAllComments () $ repository = new CommentRepository (); $ commentData1 = Array (1, 'x', 'x', 'x', 'x'); $ comment1 = (new CommentFactory ()) -> make ($ commentData1); $ commentData2 = Array (2, 'y', 'y', 'y', 'y'); $ comment2 = (new CommentFactory ()) -> make ($ commentData2); $ repository-> add ($ comment1); $ repository-> add ($ comment2); $ this-> assertEquals (array ($ comment1, $ comment2), $ repository-> findAll ()); 

Die erste solche Methode wird aufgerufen finde alle(). In diesem Fall sollten alle Objekte zurückgegeben werden, für die das Repository verantwortlich ist Bemerkungen. Der Test ist einfach, wir fügen einen Kommentar hinzu, dann noch einen, und schließlich möchten wir anrufen finde alle() und erhalten Sie eine Liste mit beiden Kommentaren. Dies ist jedoch mit unserem nicht zu erreichen InMemoryPersistence wie es an dieser Stelle ist. Ein kleines Update ist erforderlich.

Funktion retrieveAll () return $ this-> data; 

Das ist es. Wir haben a hinzugefügt retrieveAll () Methode, die nur das Ganze zurückgibt $ data Array aus der Klasse. Einfach und effektiv. Es ist Zeit zu implementieren finde alle() auf der CommentRepository jetzt.

function findAll () $ allCommentsData = $ this-> persistence-> retrieveAll (); $ comments = array (); foreach ($ allCommentsData als $ commentData) $ comments [] = $ this-> commentFactory-> make ($ commentData); $ Kommentare zurückgeben; 

finde alle() wird anrufen retrieveAll () Methode auf unserer Beharrlichkeit. Diese Methode stellt ein unformatiertes Datenfeld bereit. finde alle() durchläuft jedes Element und verwendet die Daten, die zur Übergabe an die Fabrik erforderlich sind. Die Fabrik wird eine liefern Kommentar eine Zeit. Ein Array mit diesen Kommentaren wird am Ende von erstellt und zurückgegeben finde alle(). Einfach und effektiv.

Eine andere übliche Methode, die Sie in Repositories finden, ist die Suche nach einem bestimmten Objekt oder einer Gruppe von Objekten basierend auf ihrem charakteristischen Schlüssel. Zum Beispiel sind alle unsere Kommentare durch einen mit einem Blogbeitrag verbunden $ postId interne Variable. Ich kann mir vorstellen, dass wir in der Geschäftslogik unseres Blogs fast immer alle Kommentare finden möchten, die sich auf einen Blogbeitrag beziehen, wenn dieser Blogbeitrag angezeigt wird. Also eine Methode namens findByPostId ($ id) klingt für mich vernünftig.

function testItCanFindCommentsByBlogPostId () $ repository = new CommentRepository (); $ commentData1 = Array (1, 'x', 'x', 'x', 'x'); $ comment1 = (new CommentFactory ()) -> make ($ commentData1); $ commentData2 = Array (1, 'y', 'y', 'y', 'y'); $ comment2 = (new CommentFactory ()) -> make ($ commentData2); $ commentData3 = Array (3, 'y', 'y', 'y', 'y'); $ comment3 = (new CommentFactory ()) -> make ($ commentData3); $ repository-> add (array ($ comment1, $ comment2)); $ repository-> add ($ comment3); $ this-> assertEquals (array ($ comment1, $ comment2), $ repository-> findByPostId (1)); 

Wir erstellen nur drei einfache Kommentare. Die ersten beiden haben dasselbe $ postId = 1, der dritte hat $ postID = 3. Wir fügen alle zum Repository hinzu und erwarten dann ein Array mit den ersten beiden, wenn wir ein findByPostId () für die $ postId = 1.

function findByPostId ($ postId) return array_filter ($ this-> findAll (), Funktion ($ comment) use ($ postId) return $ comment-> getPostId () == $ postId;); 

Die Implementierung könnte nicht einfacher sein. Wir finden alle Kommentare mit unseren bereits implementierten finde alle() Methode und wir filtern das Array. Wir haben keine Möglichkeit, um die Beharrlichkeit zu bitten, die Filterung für uns durchzuführen, also werden wir es hier tun. Der Code wird jeweils abgefragt Kommentar Objekt und vergleichen Sie seine $ postId mit dem Parameter, den wir als Parameter gesendet haben. Großartig. Der Test ist bestanden. Aber ich habe das Gefühl, dass wir etwas vermisst haben.

function testItCanFindCommentsByBlogPostId () $ repository = new CommentRepository (); $ commentData1 = Array (1, 'x', 'x', 'x', 'x'); $ comment1 = (new CommentFactory ()) -> make ($ commentData1); $ commentData2 = Array (1, 'y', 'y', 'y', 'y'); $ comment2 = (new CommentFactory ()) -> make ($ commentData2); $ commentData3 = Array (3, 'y', 'y', 'y', 'y'); $ comment3 = (new CommentFactory ()) -> ma